Content-Kit: Programmatic Editing

Wow! This takes me back! Please check the date this post was authored, as it may no longer be relevant in a modern context.

Last month, I blogged about the initial release of Content-Kit. Content-Kit is a WYSIWYG editing library for the web, built with Bustle Labs. Instead of saving edited posts as HTML, Content-Kit saves posts as an easy-to-render Mobiledoc format. Since that post, we’ve been talking to some other parties interested in our early alpha-quality work.

Some of the feedback we got from developers looking into Content-Kit is:

  • The content they publish includes rich fragments that can take advantage of Mobiledoc’s cards API. This is often more important to them than real-time editing, for instance.
  • Their previous experiences with the quirks of Content Editable have made them eager to explore other alternatives.
  • They demand customization of the UI. Not only of button style and placement, but of inline experiences. One use-case was to allow Markdown-ish content entry (using ** to start bold markup), another was to display a drop-down of user handles when @ is entered.
  • At times, they want to limit how and what a user can edit in a post.

Our original suspicion was that Content-Kit needed to ship with an out-of-the-box ready-to-use UI in order to gain adoption. After these discussions it is clear no users are particularly excited about a stock user-interface, and most are very excited about increased programmatic control over the editor.

This marks a slight change in direction for Content-Kit. We plan to port much of the existing Content-Kit interface to an Ember Addon. Content-Kit itself should slowly lose UI features, and gain APIs for programmatic editing of a post.

Last week we started working with these new assumptions, and began fleshing out APIs. Let’s take a look.

Post Editing API

Content-Kit maintains a representation of the current post in what we call the “post abstract”. When a post changes, we need to re-render the editing surface and position the cursor correctly. For example, when a user hits “enter”, several operations to the post may take place. A few markers may be split, possibly some may be deleted, and a new section may be created.

Rendering speed is extremely important for an editing surface. We want to update the UI as quickly as the user taps a key. Fast typists hit about 325 characters per minute, which means a max render budget of 184ms to update the screen before rendering falls behind. To make the UX feel smooth, 100ms is a good target budget for most UI responsiveness. With editing and typing interfaces, we should likely be even faster.

Content-Kit must provide an editing API that allows flexibility, but flush the changed post abstract to DOM only once.

To do this, we have the user batch all editing operations in a run method. This in turn flushes work queues after any methods are called. For example, this code removes the section under the cursor and replaces it with a card section:

let {headSection: sectionWithCursor} = editor.cursor.offsets;
editor.run(postEditor => {
  let cardSection = editor.builder.createCardSection('fancy-card' {payload: 'data'});
  postEditor.insertSectionBefore(cardSection, sectionWithCursor.next);
  postEditor.removeSection(sectionWithCursor);
});

Note that only code mutating the post must use the postEditor and run method. Fetching cursor position, for example, is not a mutating call and thus can happen at any time.

Further initial documentation on the post editing API can be found in the README and editor/post.js file.

Content-Kit Lifecycle Hooks

Some kinds of UI require notification when the post abstract is changed, or when the editing surface is re-rendered. For example, to display a list of usernames after @ is typed the input to Content-Kit must be observed.

One strategy we considered was to punt this responsibility to native browser event handling. Force developers to respond to key and mouse events themselves when wrapping Content-Kit. However we think this approach would be error prone and likely cause poor performance if the resulting handler needed to call run to make its own updates.

In #84 we propose three lifecycle hooks:

  • didUpdatePost is called before post changes are rendered. This allows it to make changes with the postEditor, which it is passed, before rendering. You could use this hook to implement text commands, for example converting ** into a bold marker to emulate Markdown syntax.
  • willRender is called at the start of rendering, when the post is guaranteed updated but the DOM has not been changed.
  • didRender is called after rendering, when the post and DOM are synchronized.

For example, didRender could be used to silently save the post after each successful change:

editor.didRender(() => {
  let mobiledoc = editor.serialize();
  savePostDebounced(mobiledoc);
});

These hooks will ship with Content-Kit this week. To accompany them, we also plan iteration APIs for sections and markers (and possibly the tree of the post abstract). These combined will provide a rich API to build features like validation on top of.

Disabling Input

Limiting what users may input into a post (what cards, what markup) can be achieved by limiting their UI options. Content-Kit will continue to push that concern to UI implementations, like the Ember Addon, and to API consumers.

However, Content-Kit must offer a way to suspend text editing capability itself to enable the strictest limitations on content. At Bustle, we’re using non-user-editable Content-Kit instances to organize content based entirely on cards.

In #82, two methods on the editor were introduced:

  • editor.disableInput() - disallow edits via the cursor
  • editor.enableInput() - resume standard text editing behaviors

Extracting the UI

Content-Kit currently offers a rough user interface largely modeled on the excellent work of Medium. We’ve found that many developers are not interested in using the provided interface. Even with customization APIs, there was not enough flexibility to cover all use-cases.

The above APIs are intended to power external UI libraries wrapping Content-Kit.

With Bustle, we will be prototyping a new UI implementation based on Ember components.

  • A UI library based on Ember will be more customizable and flexible than what we could reasonably build in plain JavaScript
  • An Ember library will also allow cards to be implemented as Ember components, making new content experiences easier and faster to implement, as well as easily tested.
  • Building this library forces us to “dogfood” the public API for Content-Kit, making it more robust and well-considered.

An Ember Addon will be released in early September. We already have an internal prototype, and are using it to shape the public APIs in Content-Kit as we flesh it out.

Despite the selection of Ember for our improved Content-Kit UI, we encourage the use of Content-Kit with other interface frameworks. You can find us in the Gitter chat if you want to kick around ideas.

Big thanks to Bustle Labs for continued sponsorship of our work on Content-Kit. We’re having a great summer working with their smart and fun team here in NYC. Bustle is looking for senior software engineers and senior UX designers to join their crew.