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.
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):
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 }(pluscolorfor 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:
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.
registerStoredFieldTypes(store, inlineRegistry) directly right after constructing your store — see the API reference.