noteloom

Custom select field types

createSelectFieldType(config) builds a full, ready-to-register inline type from a plain config object — this is how you add your own named dropdown ("Assignee", "Status", "Priority", …) without writing a React component.

js
import { useEditor, NoteloomEditor, registerBuiltInInlineTypes, createSelectFieldType } from 'noteloom';

const statusFieldType = createSelectFieldType({
  type: 'status', // must match the key you register it under
  label: 'Status', // shown in the "/" menu and as the search box's aria-label
  placeholder: 'Set status…',
  variant: 'tag', // 'tag' = Notion-style colored pill; 'default' = plain bordered dropdown
  options: [
    { value: 'todo', label: 'To do', color: { bg: '#e9e9e7', text: '#37352f' } },
    { value: 'doing', label: 'In progress', color: { bg: '#fdecc8', text: '#a06400' } },
    { value: 'done', label: 'Done', color: { bg: '#dbeddb', text: '#2f7a2f' } },
  ],
});

function Editor() {
  const editor = useEditor({
    registerInlineTypes: (inlineRegistry) => {
      registerBuiltInInlineTypes(inlineRegistry); // keep every built-in type...
      inlineRegistry.register('status', statusFieldType); // ...plus your own
    },
  });
  return <NoteloomEditor editor={editor} />;
}

Dynamic / API-backed options

options can also be a function instead of a plain array — (query) => Option[] | Promise<Option[]> — for a real database/API-backed search (React Select's loadOptions, essentially):

js
inlineRegistry.register(
  'assignee',
  createSelectFieldType({
    type: 'assignee',
    label: 'Assignee',
    placeholder: 'Assign to…',
    variant: 'tag',
    triggers: ['slash', 'at'], // reachable via "/assignee" AND by typing "@" directly
    options: async (query) => {
      const res = await fetch(`/api/users?search=${encodeURIComponent(query)}`);
      const users = await res.json();
      return users.map((u) => ({ value: u.id, label: u.name }));
    },
  }),
);
  • Your function is called fresh on every keystroke, debounced ~250ms — there's no built-in caching layer, so memoize inside your own function if you want one.
  • Only the resolved pick{ value, label } (plus color for the tag variant) — is ever written onto the document. The live options list is never persisted, so a chip never embeds a stale snapshot of your database; re-opening it always calls your function again.
  • triggers (default ['slash']) decides whether the type shows up under "/", "@", or both. A field that doesn't read naturally after "@" (e.g. "Priority") should usually stay slash-only.

There's no separate "mention" type

An @name chip is just an ordinary createSelectFieldType with triggers: ['slash', 'at'] — see the "Assignee" example above. There is no hardcoded mention type, since a real app's roster/search always needs to be host-supplied anyway.

Letting end users create their own field types, in-editor

Everything above is for types you define in code. To let a non-technical end user create new (always static — there's no way to author a fetch function through a UI) select types from inside the editor itself, mount FieldTypeEditorModal once and wire a button to it:

jsx
import { useEditor, NoteloomEditor, FieldTypeEditorModal, useFieldTypeEditor } from 'noteloom';

function NewFieldTypeButton() {
  const { openCreate } = useFieldTypeEditor();
  return <button onClick={openCreate}>+ New field type</button>;
}

function Editor() {
  const editor = useEditor();
  return (
    <NoteloomEditor editor={editor}>
      <NewFieldTypeButton />
      <FieldTypeEditorModal />
    </NoteloomEditor>
  );
}

User-created types are persisted in the document's own fieldTypes collection (so they survive reload) and are automatically rehydrated back into your inline registry by FieldTypeEditorModal itself — you don't need to call anything extra. Each chip's popover also gets a "Manage options…" entry that reopens this same modal, pre-filled, for renaming/editing/deleting the type it belongs to.

To rehydrate persisted field types on load without mounting the modal (e.g. server-side, or in a test), call registerStoredFieldTypes(store, inlineRegistry) directly right after constructing your store — see the API reference.