Editing custom widgets in context ​
INFO
Be sure to read customizing the user interface first before proceeding with this guide. In this guide we will assume you are already familiar with APOS_DEV=1
, ui/apos/components
and other concepts covered there.
Most custom widgets, and indeed most core widgets, are edited in a dialog box when the user clicks on the edit button. But there is another possibility: in-context editing on the page. The standard rich text widget is an example of this. The user types text directly on the page.
While editing a schema of fields in the provided dialog box is convenient for developers, sometimes editing directly on the page provides a better user experience. This guide explains how to do that.
WARNING
Editing on the page is not always the best path. Always consider the smallest space in which the widget might be placed when making this decision. Also bear in mind that the appearance of the widget will be different on different devices. The "what you see is what you get" experience can be misleading in some situations.
To implement on-page editing for a custom widget type, we must implement the following pattern:
The basics of contextual editing ​
Module configuration ​
The contextual
option of the widget module must be set to true
. This is the trigger for contextual editing. Otherwise the normal editing dialog box is displayed for the widget.
The components
option of the widget module determines the Vue components to be used for the editing experience. Currently there is just one subproperty, widgetEditor
, which is required for contextual editing.
The defaultData
option of the widget module may be set to a default value for newly created widgets of this type.
The widget editor ​
The Vue component name configured via the widgetEditor
subproperty of the components
option (the "widget editor") must display the content of the widget in an editable form. To provide a contextual editing experience, that interface should be similar to the ordinary read-only view generated by widget.html
.
The widget editor receives a modelValue
Vue prop containing the existing value of the widget (an object), and emits an update
Vue event with a new object whenever appropriate. The component should not modify the modelValue
prop given to it.
The widgetEditor must not attempt to save changes by itself. Instead it must emit the update
event and let Apostrophe takes care of the rest. Never assume the widget will be in a particular document type. It may be in any area, nested in any document type. Apostrophe will handle this for you.
Debouncing update events ​
If the value will be changing quickly, for instance as the user types, performance can suffer if an update
event is generated for every keystroke. The widget editor can speed up the interface by emitting an update
event no more than once per second. This is called "debouncing."
As a hint that input might not yet be saved, a widget that "debounces" update
events should emit a context-editing
event on every change, even if update
is intentionally delayed. However this should only be done if the docId
prop passed to the widget editor is equal to windows.apos.adminBar.contextId
.
If the focused
prop of the widget editor becomes false
and an update
event has been delayed for debouncing purposes, the widget should cancel its timer and immediately emit the update. Use Vue's watch
feature to monitor focused
.
Saving the content ​
By default the fields
section of a contextually edited widget has no effect on the user interface. Emitting a reasonable value is the task of the custom widget editor component. However configuring fields is still necessary since the server will still use them to sanitize the data before saving it.
If no fields
section is configured, no data will be saved at all unless the sanitize
method of the module is overridden to provide an alternative way to verify user input, as is done in the case of the core rich text widget.
Example ​
Here is a simple "hero" heading widget. Both the heading and the width of the heading can be adjusted directly on the page. This gives the user immediate visual feedback.
What is happening here?
This index.js
file configures the hero-widget
module. The contextual: true
option enables contextual editing, defaultData
provides a default value that satisfies the schema, and components
configures the Vue component name to be used for editing.
The fields
section provides a schema of fields to sanitize and store the data in the widget. Even though the interface does not use a traditional form, this is still a convenient way to verify the content meets expectations on the server side. Remember: it is never OK for the server to trust the browser.
WARNING
Don't forget to also enable the module in app.js
, like any other module.
What is happening here?
This is a straightforward widget.html
template, as might be found in any custom widget. The presentation is intentionally similar to that used in the widget editor component, below.
What is happening here?
This is HeroWidgetEditor
, the custom widget editor Vue component for this widget type. The focused
prop is used to decide whether to show the resizing buttons for the hero heading, so they don't clutter the page when the user is not editing this widget. When the user changes either the width or the heading text, the widget editor emits an update
event with a new object containing the width
and heading
fields, in addition to any pre-existing properties of the original modelValue
prop.
WARNING
When emitting an update, be sure to include any properties of modelValue
that your editor does not directly edit, such as type
. Otherwise a JavaScript error will occur. The easiest way to do this is with ...this.modelValue
, as shown here.
What is happening here?
Last but not least, we still need styles for the widget's normal appearance when it is not being edited. A ui/src/index.scss
file for the hero-widget
module is a good place to do that and will automatically be loaded all the time, unlike the admin UI components in ui/apos
.