ApostropheCMS 3.0 Sprint Recap #3: doing it in context, doing it right

With the fundamentals of in-context, on-page editing behind us, the next logical step is to tackle on-page text editing.

sprint 3 in context

In our last sprint recap, Bob Clewell and I waxed poetic about the magic of Vue. We turned simple schemas into powerful forms and solved tricky problems, like ensuring unique URLs, within the patterns of Vue. It felt good.

One sprint further on, Vue still feels good. Sure, we hit snags new and old — you should definitely read the official documentation on change detection caveats in Vue — but we moved through those to take a feature we’ve always delivered in a… shall we say… ad hoc way, and really get it right.

A Rube Goldberg machine (but it’s really pretty)

One of the best features of ApostropheCMS is in-context editing. You can edit many things right on the page, minimizing the time you spend in a maze of dialog boxes and forms. You can experience it for yourself on our demo site.

And one of the coolest things you can do with that in-context editing capability is build page layouts on the fly.

ApostropheCMS: drag-and-drop editing, including layout widgets

Truly, the prettiest Rube Goldberg machine you ever did see.

Yes, it’s common to give users a choice of page templates that are fairly “locked down” in what you can do and where you can do it. That’s a good way to prevent them from getting lost and make sure the designer’s intentions shine through on every page. But it’s also true that every website must evolve to meet new needs. And that’s where the ability to create dynamic layouts is so useful.

And by creating “layout widgets,” such as “two-column,” “three-column” and “hero” widgets, we can give users the best of both worlds: the ability to mix it up in the flow of a single page between one-column and multi-column layouts, while continuing to benefit from thoughtfully made design choices.

It’s simple, if you’re a front-end developer: those “layout widgets” come with one, two or three content areas nested inside them. And the user can add a series of widgets to each area. Nested widget nirvana!

And it totally works! We have a handy tutorial on how to achieve it in your own Apostrophe 2.x site today.

Except… while it does work, it’s a bit of a hack. A hack that works. But a hack nevertheless.

To save changes across all those nested areas, we have to give each of them a trail of bread crumbs in the DOM that identifies it. That trail of bread crumbs has evolved in a very ad-hoc way. And our jQuery-based area editor must then find nested areas inside itself via DOM queries and take them into account when saving content.

It works rather well… but it’s a bit Rube Goldberg. A little like this, in fact:

THE PIZZA MACHINE! (Pizza Making Rube Goldberg Machine)

Apostrophe 3.x is our chance to make this process better.

So for our third sprint, we focused on in-context editing.

This time, we have a simple Vue component, ApostropheAreaEditor, that represents a single area on the page during the editing experience. Nested in that, we have ApostropheWidgetEditor components which are responsible for editing the fields of each widget.

We also have ApostropheWidget components, responsible for viewing each widget… but since we’ve pledged not to require Vue on the front end for ordinary website visitors, those widgets simply “phone home” to the server for a rendered version of the content. Just for the editor’s previewing convenience.

We use v-for to loop through the widgets, adding in an  ApostropheAddWidgetMenu between widgets to let users add additional content in between. And two-way data binding, the big star of our previous sprint, again comes to the rescue by allowing ApostropheAreaEditor to simply hand off the widget objects to be edited.

For bonus points, we bind them to both ApostropheWidgetEditor  and ApostropheWidget… which will be great when we’re ready for the new rich text editor, as that edits directly on the page and doesn’t require a dialog box at all. But that’s the subject of our next sprint.

Wait, what happened to nested layout widgets?

This all sounds simple and elegant, but weren’t we talking about nesting widgets in layout widgets a minute ago? How do we reconcile that with this simple story?

The problem is made tougher by the fact that Vue apps can’t nest components inside parts of the DOM that Vue doesn’t control. If our widgets are rendered by the server and popped into the DOM with v-html directives, then that space is basically a black box to Vue. Or it should be, anyway. Separation of concerns is a good thing.

To answer that riddle, we built a special frontend application we call ApostropheAreas. Technically, it’s not a Vue app; instead, it spawns Vue apps every time new editable areas pop up in the DOM.

sprint 3a edit image

The beauty of 2.x (but Rube Goldberg lurks inside).

sprint 3b add widget

The raw, unstyled glory of 3.x (but it’s beautiful inside).

Wait a minute, you said you were getting away from the DOM!

Well, not quite entirely. Front end developers writing page templates in Nunjucks want to keep the convenience of just writing:

{{ apos.area(data.page, 'body', { ... }) }}

… To pop an editable area on the page. And we don’t want to take it away from them.

But, that means the only way to know where the areas are is to find them in the DOM.

And when you think about it… this is what every Vue application does! Most standalone Vue apps start like this:

new Vue({
  el: '#app',
  ... now the fun can begin ...
});

However in this case, rather than a single, fixed ID in the DOM, we want to “power up” every instance of <div class="apos-area"></div> to be a fully editable experience.

So the job of our “meta-app” is simply to find those divs as they pop up, extract JSON props from their data- HTML attributes, and create Vue apps as needed… one per area.

OK, but if the areas can’t “see” their own nested child areas… how do they save them?

The answer, again, lies in the “meta-app.” When an area changes, it emits an event to its parent, which is the meta-app. The meta-app in turn updates its understanding of that area’s content and its relationship to any parent areas, or actual documents, currently referenced on the page.

Then the meta-app is able to build a complete copy of a “top-level” area, like the main area of a page, made up of layout widgets that contain their own sub-areas in turn. And it can include those sub-areas when saving the top-level content back to the document on the server.

This allows us to maintain a clear separation of concerns in which:

  • Each area is responsible only for itself,
  • Frontend markup is a “black box” as far as Vue is concerned, and
  • A single, top-level meta-application is responsible for understanding the “big picture” state, including the nested relationships of areas.

Whoa, you said the S-word! Shouldn’t you be using VueX to manage that state?

Hey, it’s possible. But so far it seems both different in kind from the problems that VueX usually solves, due to the nesting issues, and simple enough to solve well in a single-file meta-app that serves as the “store.” Besides, we’re emitting change events directly to a parent (the meta-app) and the child components have zero interest in what happens in any of the other areas — which means the most important trigger for VueX and other flux-like stores just isn’t in play here.

But we’re open to contributions and input on that, especially from those ready to dig into the 3.0-in-context-editing branch and really understand the issues we’re working through.

Drag and drop: the finish line! (Sort of)

This sprint went so well that we were able to move on from more basic functionality to full drag and drop support. Apostrophe 2.x supports dragging any widget, not just within an area but between areas. Even if levels of nesting differ. Obviously, we can’t drop that ball in 3.x! So how should we approach it?

Vue has several implementations of drag and drop. The best known is probably SortableJS/Vue.Draggable. We tried that, and it worked well when dragging widgets within a single area. But when dragging between areas, it just didn’t seem to want to play.

So we tried vddl… which worked immediately. Except, there’s a really weird Chrome bug. Pick up a widget in one area and the nested bits of the nested layout area beneath it just… disappear until you drop the widget.

And then everything is fine. It works, even. But that’s disconcerting.

This bug might not be the fault of vddl; after all, it works perfectly in Firefox. But, we do need a solution to it in order to commit permanently to this library.

So I wouldn’t say we have necessarily found our perfect drag and drop solution yet. But we’re pleased that we got this far in a single sprint.

Next up: compose your prose

With the fundamentals of in-context, on-page editing behind us, the next logical step is to tackle on-page text editing. And as we mentioned in our last wrapup, that’s going to be an interesting process. We need to make a final choice of editor between CKEditor, Slate, Quill and any late-breaking challengers that can meet our cross-browser testing and MIT-license-friendly expectations… we need to address the need to convert back and forth between HTML and the newfangled schemas of those editors… and oh yeah, we need to actually implement them on the page and bind them to the widget’s data. But like I said, Vue is going to make that easy for us. I think.

Looking forward to Sprint #4!