Model for a todo list using new constraints system

I’m currently experimenting with the new constraints system and am trying to create a model for a nestable todo list. The goal is to support the same operations as bullet lists (e.g. wrapping multiple paragraphs) and list items (sink and lift).

This is the code I have so far (I am testing it in demo.js):

var model = require("../dist/model");
var InputRule = require("../dist/inputrules").InputRule

class TodoList extends model.Block {}
TodoList.prototype.serializeDOM = (node, s) => s.renderAs(node, "ul", {class: 'task-list'})

TodoList.register('autoInput', 'insert', new InputRule(
  /^task $/,
  ' ',
  function(pm, match, pos) {
    const todoItem = pm.schema.node("task_item", {}, [ pm.schema.node("checkbox"), pm.schema.node("paragraph")])
    const todoList = this.create({}, todoItem);

    const $pos = pm.doc.resolve(pos);$pos.before($pos.depth), $pos.after($pos.depth), todoList)

class TodoItem extends model.ListItem {}
TodoItem.prototype.serializeDOM = (node, s) => s.renderAs(node, "li", {class: 'task-item'})

class Checkbox extends model.Inline {
  get attrs() { return { done: new model.Attribute({default: false}) } }
Checkbox.prototype.serializeDOM = (node, s) => {
  return s.elt('input', {
    type: 'checkbox',
    checked: node.attrs.done ? '' : undefined,

const schema = new model.Schema({
  nodes: {
    doc: {type: model.Doc, content: "block+"},
    text: {type: model.Text},
    paragraph: {type: model.Paragraph, content: "inline*"},

    task_list: {type: TodoList, content: "task_item+"},
    task_item: {type: TodoItem, content: "checkbox paragraph task_list?"},
    checkbox: {type: Checkbox},

  groups: {
    block: ["paragraph", "task_list"],
    inline: ["text"]

So far the following questions have come up:

  • Trying to simply use this.create({}) inside the TodoList input rule does not work, which is why I explicitly create the required child node. At first I expected the system to automatically create the required content as defined in the schema using default attributes. Would it be possible/desired to do this automatically? It’s not a big problem in this case, but might be helpful for other generic commands (maybe splitting).
  • Creating a TodoList using the input rule when there is no other content correctly places the cursor in the paragraph of the list item. Doing the same thing when there is a paragraph below, the cursor is placed in the paragraph below the created TodoList. I suppose this is the wanted default behavior when creating block nodes, right? In this case I would want the cursor to be positioned in the paragraph of the TodoItem. How is this handled in BulletLists?
  • Trying to indent the TodoItem (which extends ListItem) throws an Uncaught RangeError: Wrap not possible. I hoped that this would work without adjustments as the structure is exactly the same as with a BulletList and ListItems. (To try this, I created two empty paragraphs, created two TodoLists using the input rule, joined the two lists, and pressed Ctrl+] inside the second TodoItem).
  • How is it possible to delete such a TodoItem? The constraints correctly enforce keeping a Checkbox and Paragraph node. But I would still like to allow the deletion of such an item using backspace when in front of the checkbox. Is creating a custom Backspace handler the right approach?
  • How would splitting a TodoItem be implemented? I would like to append a new TodoItem to the TodoList similar to the behavior of ListItem. I suppose that creating a custom Enter handler for TodoItems is the way to go?

Ideally, I would like to share as much logic as possible with the BulletList and ListItem and to derive all the nestable/wrappable/splittable properties they have. I would also be happy about feedback on the overall structure of the model. Please let me know if there is a better way to approach this.

Have you considered not creating a separate node for the checkbox, but just making checked a property of a task_item node? Seems that would reduce the amount of node-shuffling involved.

Node creation doesn’t check or fix content – that’d be too expensive. You can call nodeType.fixContent(Fragment.empty) to synthesize valid content for a node. Or just create and pass it yourself.

You’re executing a replace step, which will simply insert the content and move any positions (including the cursor) after the insertion point forward. You can pass a selection option to .apply to set your own custom selection after the transform. The input rules for the default list change type of the current textblock, rather than inserting a new node.

Not sure what’s going on there. Try to debug checkWrap to figure out why it considers this invalid.

Possibly. Not making the checkbox its own node might already make this simpler.

Probably best done with a custom enter handler, yes.

Thanks for the answers, they clear up quite a few things for me!

Yes, this definitely sounds like a good idea. The main reason I tried the different approach was because of click handling. Handling a click on the rendered checkbox without it being a node isn’t that straightforward with the current API. But in this case it sounds like it might actually be worth the extra effort if it reduces the complexity of the schema.
I will try this approach and see if I encounter any other issues.