prosemirror-commands provides many very helpful commands for joining/lifting/etc. In the process of building complex commands, I find myself wanting to use these builtin commands as building blocks. Unfortunately their API is to take state and onAction and actually apply the changes. I propose instead to refactor these commands into methods that operate on a transform and returns either false (could not execute command) or the new transform. This would allow for easier chaining of builtin commands in creating custom commands.
As an example of one of these custom commands, I have a command that will lift all blocks within selection and then wrap selection in provided block type. This works by operating on a transform to lift all blocks and then calling wrapIn on the transform.selection. Iād like to call the wrapIn provided but that takes a state and I need to operate on my transform which has everything lifted. With this command I can turn a selection into a blockquote and remove bullets & headings.
Note that while this definitely holds for some of the stuff that the commands package does, joining and lifting, specifically, are based on primitives from the transform package which are exported.
This, for example, really shouldnāt be hard to write on top of liftTarget, Transform.lift, and Transform.setBlockType.
Returning a transform from a command isnāt going to work, because commands, as they work now, can create any action, not just transformations. What you could do, in some cases, is pass a custom onAction callback to a recursively called command, but thatās not super straightforward.
The idea is that commands are interface units that you bind to keys or menu items, which do one thing and perform all the checking of whether that thing makes sense, and for that the current interface works well. Utilities to be used by commands are a separate thing, and Iām open to factoring some of those into a more reusable form.
Absolutely. Iām not suggesting the commands interface changes. More like, the commands that exist now would be wrappers around transform utility functions.
Does the introduction of Transaction change the landscape enough for this to be reconsidered? I can appreciate that commands are more powerful than transforms, because transforms donāt encapsulate all the state thatās in play (e.g. stored marks), but from what Iāve gathered so far transactions do?
Ultimately I think my use-case is the same as @wes-r ā I want composable mutations. I think I want this so that I have full control over where history events (by treating each transaction as a history event, and being responsible for building up a single transaction of appropriate changes before dispatching it).
What I have in mind is an interface like this:
interface Mutator {
// Apply the mutation to a transaction, returning the resulting transaction.
// If the mutation can't be applied, returning the original transaction unchanged.
apply(tr: Transaction): Transaction;
// Return true if the mutation can be applied.
canApply(tr: Transaction): boolean;
}
Itās very similar to a command as they exist today, but it doesnāt know anything about the view and dispatching.
The command interface isnāt going to be overhauled again, for the sake finally stabilizing but also because I think their current form expresses what they do better (they may have side effects, beyond dispatching a transaction).
You can extend commands with this slightly awkward but unproblematic pattern:
Iām happy for commands to stay as they are, but what Iād propose is decomposing the implementation into āmutatorsā (open to a better name) that are composable. The end result should be commands that maintain their current interface, but whose internals are probably just a couple of lines combining mutators and calling dispatch.
I think Iām missing something regarding the difference between transactions and editor state, specifically regarding what state they encapsulate. Whatās missing from transactions that prevent commands from only working with transactions? Itās surprising to me that itās not an explicit goal for commands to work on transactions, so I believe thereās some consideration that Iām overlooking.
I suspect we made a mistake with this thread in conflating transforms with commands ā commands can continue to live as-is, but (I think what @wes-r and) I want is something new thatās similar to commands, but is a lower-level composable construct. In practice Iād probably end up never using commands, and always build my own because of specific user-experience requirements that Iām aiming for.
Having two functions, one to check whether something applies and one to execute it, is how things initially worked but it was really hard to keep those two consistent, so I moved to the one-function-with-optional-dispatch form, and it made the code much simpler and less error prone.
Adding another concept of helpers that implement the functionality of the commands is possible, but Iām not really convinced the added value is worth the extra API surface.
FWIW I came across this thread as I was running into the exact same problem - Iāve been using the built in commands, and theyāre great up until I need something slightly different, or I need to compose a few of them together, but ultimately Iām having to just copy the internals of them out of prosemirror-commands and reimplement locally.
@bradleyayers idea of a lower level composable interface would be ideal for me, and I think Iām in a similar boat - I doubt Iād end up actually using any of the built in commands, as nearly all the behaviours Iām after are just slightly different to the built in commands.
For me, I think itās worthwhile to have the extra API surface - IMO I donāt think it would be too much cognitive overhead for people coming to ProseMirror, and it would be incredibly worthwhile. I found it really confusing that I couldnāt chain together commands (until I properly understood their inner workings) - indeed thatās what I thought the chainCommands helper initially did. Having a different set of āutilitiesā would help clear this up I think.
Ping @marijn - I hit this again! The liftListItem implementation in prosemirror-schema-list is awesome as it will do the right thing for any nested list items. The issue I ran into is wanting to invoke it several times to lift an item ALL THE WAY out of the list (a āremoveā command). Presently there is no good way to do this. I had to copy the implementation and modify it to operate on a transaction so that I can invoke it in a loop.
All of this great functionality is locked away behind dispatched commands. Instead they should be building blocks! Seems many of us long term users have solved this by copying the innards of the commands we want to build upon. This is not good! It makes managing our code harder and it adds a barrier to entry for beginners as they will inevitably run into the same desire and the same less-than-perfect-solution.
I definitely agree that there are a few utility-type functions that Iāve either copied and pasted from the core lib or written from scratch and subsequently realised that Iād reimplemented something in that was already in the core library.
Also ā thereās no reason to drastically increase the API surface area. If all of the existing commands operated on transactions and returned one, then there could be a new high level command helper dispatcher() or something like that, so dispatcher(joinBackward()) would be equivalent to the joinBackward that exists today.
You usually donāt want the exact command as a helper, though. Where a command will act on the selection, a helper tends to work better when it takes arguments that tell it where to apply its effect.
But Iām okay with growing the API surface a little to expose reusable code where itās appropriate. Do you want to propose a pull request that splits the guts of liftListItem into an exported utility function?
Passing a selection makes sense. Or it could just be a pair of positions. Also, Iād include itemType directly in the parameters of the functionāif we donāt have to correspond to the command function signature, thereās not need for the wrapping factory function.
I also run into this issue now and then, wanting to combine the functionality of several commands. However, that use case doesnāt seem to be supported since the commands immediately dispatch the transactions and Iām left with copy-pasting the code from prosemirror-commands.
Iāll try employing the pattern suggested by @marijn, but I feel that the API could be improved here to avoid code duplication. I donāt think this discussion has lead to changes in the code, has it?
I donāt know the design decisions behind the current API, but Iāll take the liberty to present the following, possibly foolish suggestion.
Looking at the prosemirror-commands source code, commands only use the state to get the current document, selection and storedMarks. I believe these can also be retrieved from a transaction and the interface of commands could possibly be changed to accept only a transaction instead of the editor state?
Considering if (dispatch) dispatch(...) is (almost) always followed by return true, perhaps commands can return a transaction when the command can be applied, or null otherwise (equivalent to a return value of false now). This does however mean that transactions are constructed, even if they are not going to be dispatched by the caller. Is that too wasteful?
This kind of interface allows chaining commands, AFAICS. Instead of changing the commands interface, an intermediate level of mutators can be introduced, as suggested by @bradleyayers. Backwards-compatible commands could be constructed automatically from these:
function createCommand(mutator) {
return function(state, dispatch) {
let tr = mutator(state.tr)
if (tr === null)
return false
dispatch(tr)
return true
}
}
That may go for the commands in that package, but is not generally true. For example, the undo/redo commands will access the stateās history field.
As I pointed out in other messages, I donāt think it is possible to compose commands, in general, and having use-case-specific helper functions that implement the difficult parts some commands would be the way to go.
You can pass this state to multiple commands with an empty dispatch function and then dispatch the tr so that all the updates are composed onto the same transaction.