question-mark
Stuck on an issue?

Lightrun Answers was designed to reduce the constant googling that comes with debugging 3rd party libraries. It collects links to all the places you might be looking at while hunting down a tough bug.

And, if you’re still stuck at the end, we’re happy to hop on a call to see how we can help out.

simplify the "Change vs. Editor" question

See original GitHub issue

Do you want to request a feature or report a bug?

Idea.

What’s the current behavior?

This is something I was discussing with @alanctkc in Slack. Right now, there are two “main” instances that you care about when developing plugins: the Editor and the Change.

This issue is that they are used semi-interchangeably, but they’re different. For example commands receive a change object. Whereas queries receive an editor object. And event handlers receive a change object. But components should receive an editor.

That alone isn’t a huge deal, but it does complicate things.

But things get worse when you consider that there is confusion between editor.value and change.value—people might not even realize they are different. But editor.value is the currently rendered value of the editor, and it only changes after new props are passed in. Whereas change.value is the most-up-to-date value object that has the newest operations in the change applied to it.

This distinction is a bit complex. It means that you can’t just access editor.value in your queries and have everything work as expected.

What’s the expected behavior?

A fairly simple solution that @alanctkc came up with is to change queries to actually be passed a change object, which will default to the currently active change. This seems strange, but it’s actually kind of ingenious because it means that queries can always access change.value as the most-up-to-date.

This could work.


I was wondering though, if there might be a slightly different solution that we could use, by getting rid of the current Change object altogether. Sounds drastic, but it might actually result in a simpler mental model.

It would entail…

  • Registering commands and queries directly on editor. Right now if you want to invoke a command on the editor you do editor.command('insertText', 'word'). Whereas if you want to invoke it on a change you do change.insertText('word'). Under the new architecture, the commands and the queries would be available as top-level methods on the editor itself:
editor.insertText('word').splitBlock()
if (editor.isQuoteAllowed()) {
  editor.setQuoteBlock()
}
  • Tweak changes would be emitted asynchronously. To make this possible, we’d tweak the current logic to emit changes via onChange in an asynchronous way, more similarly to how setState works. (The current synchronous architecture might already be causing problems.) Any command would queue a change (just like React state does) and then the queue is flushed on the next tick. That way you can continue chaining commands synchronously just like you can now.

  • Change editor.value and editor.operations to update synchronously. As you invoke commands, the editor.operations will now hold all of the operations that have been created, and editor.value will now update to reflect those operations being applied. This means that you can always grab editor.value and know that it is the most up-to-date state, without worrying about any sort of race conditions. Although changes are emitted asynchronously, the editor.value stays up to date, so testing remains straightforward:

it('my test', () => {
  ...
  fn(editor)
  assert.deepEqual(editor.value.toJSON(), expected.toJSON())
})
  • Remove the need for editor.change. With the commands registered directly on the editor, and the changes emitted asynchronously, you no longer need to do editor.change(change => change.splitBlock()), because you can call editor.splitBlock() directly. This would reduce a decent amount of verbosity in the codebase.

  • Remove the current Change object. With all of that, you no longer need the Change object as we know it today, since all of its behaviors are present on the editor. Instead, we’d introduce a super-super-thin Change model which holds value/operations and is emitted to onChange. This is the only place we still benefit from having a separate “change” that can be referred to on its own.

  • Pass editor to commands and handlers. With the change objects eliminated, all of the middleware can now receive the editor directly, so there’s no confusion. And commands, queries, and event handlers become even simpler to reason about:

onKeyDown(event, editor, next) {
  if (event.key === 'Enter' && event.shiftKey && editor.isParagraphActive()) {
    event.preventDefault()
    editor.insertText('\n')
  }
}
  • Allow editor.command/query to take functions. One thing that is useful on the current change object is the change.call method. This functionality can easily be added to the editor.command and editor.query methods instead, which actually makes plugin development even easier because users can supply commands as name strings or as custom functions:
Linkify({
  when: 'isLinkAllowed',
  command: 'setLink',
})
Linkify({
  when: editor => ...,
  command: (editor, url) => ...,
})
  • Remove the editor.event method. This is low-level, but eliminating the need for Change objects allows us to remove one more method on the editor, because the editor.event method was only needed to create an interim change object. Without them, we can use editor.run directly in tests:
editor.run('onKeyDown', { key: 'Tab', ... })
  • Potentially introduce a “flags” concept. Right now there are some native flags that are special-cased: withoutNormalizing, withoutSaving, etc. These can be implemented in userland, but it’s sort of hacky. We could choose to add a “flags” concept, which could allow them to be “registered” just like commands and queries are:
onConstruct(editor, next) {
  editor.registerFlag('saving')
  next()
}

Issue Analytics

  • State:closed
  • Created 5 years ago
  • Reactions:4
  • Comments:16 (11 by maintainers)

github_iconTop GitHub Comments

2reactions
ianstormtaylorcommented, Oct 26, 2018

Thinking back on learning Slate, I think dealing with the Change model was the most confusing concept to understand. That makes this already pretty compelling.

@ericedem that’s great to hear! I didn’t realize that actually 😄

Do you still wonder if this overloads Editor a bit, or is there enough trimming out fat in the process that it feels right to you? It is somewhat busy that commands and queries all live at the top level alongside the core Editor methods.

@alanctkc yup, that is one of my main reservations with this right now. I’ve tried to eliminate more of the editor’s top-level methods to make room for the commands/queries and keep the collisions to a minimum, but it’s still a bit scary.

Currently we have:

class Editor {
  get readOnly()
  setReadOnly(readOnly)

  get value()
  setValue(value, options = {})

  get operations()
  applyOperation(operation)

  run(key, ...args)

  addCommand(command)
  hasCommand(command)
  command(type, ...args)

  addQuery(query)
  hasQuery(query)
  query(type, ...args)

  normalize()
  withoutNormalizing(fn)
}

So it’s pretty minimal. But already the setValue collides with the setValue command. So we’ll need to figure out a new name for that command, or a new way to set the value, if you have ideas.

(Side note: I was already thinking about breaking “decorations” out into addDecoration and removeDecoration commands for other UX reasons to make them easier to use, so this isn’t that bad of a thing, since it just leaves value.data. Which we can call setData I guess.)

Right now on changes we have top-level dynamic methods. So if we’re removing the change objects, keeping the dynamic methods is the easiest migration path. For that reason, although I’m not 100% convinced it’s the right decision, I think we should keep them. (I’m like 75/25 right now.) We can always remove them later and it will be the same amount of work for people, but this way we defer it.

Another thing I wonder about, and this actually touches on @grsmvg’s point. Since commands and queries both extend the top-level API of Editor, and both receive the editor instances as args, in user land, what is the real distinction between commands and queries besides good practice? Especially if it’s possible to execute a command in the context of a query, they start to be pretty blurry.

@alanctkc you’re right, the only distinction is the return value. Commands always return the editor for chaining, whereas queries return Any.

But, for some reason, this feels a little better:

editor.command('splitBlock').command('insertText', 'foo').query('getFoos')

In the latter, it has the side benefit of making onCommand and onQuery hooks much more understandable to me. Like, “oh, I know exactly when a command is getting called” since there is an explicit command() call versus some ambiguity when it’s just on the Editor top level.

Passing strings is a bit iffy, though, especially for tooling. Granted, they’re just object keys (which are strings) in the plugin object, so it’s not too crazy of a stretch.

This one doesn’t deal with the slight API discrepancy that commands return an editor while queries return a value, but that part seems totally understandable when you’re explicitly calling them differently.

@alanctkc I agree with you there. the command() and query() methods are nice.

This is the underlying mechanism already right now. You could choose to only ever use these in your codebase if you wanted to:

editor.command('insertText', 'word')
editor.query('isVoid', node)

And everything would work fine. If we do end up removing top-level dynamic methods, I think this is what we’d fall back to. But I agree that using string method names isn’t very nice, which is why we’d ideally be able to get away with the dynamic methods.

1reaction
alanchrtcommented, Oct 26, 2018

I don’t think I even thought about editor.command() and editor.query() already existing like that! This is most likely what I would do in my own code, as long as chaining worked as expected.

Read more comments on GitHub >

github_iconTop Results From Across the Web

Simplified Question Editor - Write Your Own Questions - Derivita
You can now create your own Derivita questions using the Simplified Question Editor right from within Assignment Assembly.
Read more >
LeetCode - Visual Studio Marketplace
Extension for Visual Studio Code - Solve LeetCode problems in VS Code.
Read more >
Visual Studio Code how to resolve merge conflicts with git?
You should first resolve the un-merged changes before committing your changes. I've tried googling it but I can't find out why it won't...
Read more >
Editor settings in Outlook.com and Outlook on the web
Microsoft Editor to correct spelling and grammar in Outlook.com and Outlook ... doesn't solve your problem, scroll down to Still need help? and...
Read more >
How to edit and reshape paths in Illustrator - Adobe Support
Note: The Pen tool changes to the Add Anchor Point tool as you position it over a selected path. Click over the path...
Read more >

github_iconTop Related Medium Post

No results found

github_iconTop Related StackOverflow Question

No results found

github_iconTroubleshoot Live Code

Lightrun enables developers to add logs, metrics and snapshots to live code - no restarts or redeploys required.
Start Free

github_iconTop Related Reddit Thread

No results found

github_iconTop Related Hackernoon Post

No results found

github_iconTop Related Tweet

No results found

github_iconTop Related Dev.to Post

No results found

github_iconTop Related Hashnode Post

No results found