Yeah exactly, don’t think it’s intended to be used this way, and I wish I understood why, but I found a pattern that has been working well – allowing anything async to ‘fall through’ to the view.update phase. I wrap my Action objects (function name and parameters) as Transactions via meta field
export function createTypeaheadPlugin(): Plugin<TypeaheadState> {
return new Plugin({
key: typeaheadPluginKey,
state: {
init: initialState,
/** set state, determine actionsToInvoke which can dispatch at a later time */
apply: (transaction, state) => {
if (isTypeaheadTransaction(transaction)) {
const newState = onTransaction(transaction, state);
return clearStaleActions(newState, state);
} else {
const newState = onEditorChange(transaction, state);
return clearStaleActions(newState, state);
}
},
},
view: () => ({
/** invoke actions which can dispatch at a later time */
update: (view) => {
const state = typeaheadPluginKey.getState(view.state);
state?.actionsToInvoke.forEach((action) => {
onAction(action, view);
});
},
}),
props: {
decorations(editorState) {
return (
typeaheadPluginKey.getState(editorState)?.decorationSet ||
DecorationSet.empty
);
},
},
});
}
clearStaleActions
helper clears actionsToInvoke
if it hasn’t changed since the last one
export const typeaheadPluginKey = new PluginKey<TypeaheadState>('typeahead');
export const enum TT {
AcceptEntireSuggestion = 'TT_ACCEPT_ENTIRE_SUGGESTION',
AcceptOneWord = 'TT_ACCEPT_ONE_WORD',
CycleSelectionIndex = 'TT_CYCLE_SELECTION_INDEX',
FetchSuggestionsSuccess = 'TT_FETCH_SUGGESTIONS_SUCCESS',
HideTypeahead = 'TT_HIDE_TYPEAHEAD',
}
type TypeaheadTransactions = {
[TT.AcceptEntireSuggestion]: AcceptEntireSuggestion;
[TT.AcceptOneWord]: AcceptOneWord;
[TT.CycleSelectionIndex]: CycleSelectionIndex;
[TT.FetchSuggestionsSuccess]: FetchSuggestionsSuccess;
[TT.HideTypeahead]: HideTypeahead;
};
export type GenericTypeaheadTransaction<Action> = Transaction & {
getMeta(key: PluginKey<TypeaheadState>): Action;
};
export type TypeaheadTransaction = TypeaheadTransactions[keyof TypeaheadTransactions]; // prettier-ignore
export function getAction<T extends TypeaheadTransaction>(tx: T) {
return tx.getMeta(typeaheadPluginKey) as ReturnType<T['getMeta']>;
}
export function isTypeaheadTransaction(
transaction: Transaction
): transaction is TypeaheadTransaction {
return !!transaction.getMeta(typeaheadPluginKey);
}
export const enum TA {
FetchSuggestions = 'TA_FETCH_SUGGESTIONS',
WriteText = 'TA_WRITE_TEXT',
}
type TypeaheadActions = {
[TA.FetchSuggestions]: FetchSuggestions;
[TA.WriteText]: WriteText;
};
/** used to enforce actionsToInvoke: TypeaheadAction[] */
export type TypeaheadAction =
| TypeaheadActions[keyof TypeaheadActions] // dispatch Action -> onAction
| ReturnType<TypeaheadTransaction['getMeta']> // dispatch Action -> onTransaction;
export function dispatch(view: EditorView, action: TypeaheadAction) {
view.dispatch(view.state.tr.setMeta(typeaheadPluginKey, action));
}
Sorry for so much code, but hopefully it helps other people. It just allows state.apply
/ view.update
to invoke onTransaction
and onAction
at later times, via actionsToInvoke
/ dispatch
, respectively