From a3e83858a4a714c29803a68e185ac3dc634eb1d2 Mon Sep 17 00:00:00 2001 From: Robert Means Date: Thu, 27 Aug 2026 08:29:52 -0400 Subject: [PATCH 1/2] Changes custom widget to a card widget --- docs/guide/custom-widgets.md | 194 ++++++++++++++++++++--------------- docs/guide/jsx-templates.md | 29 ++++-- 2 files changed, 132 insertions(+), 91 deletions(-) diff --git a/docs/guide/custom-widgets.md b/docs/guide/custom-widgets.md index 3ce9873a..91937887 100644 --- a/docs/guide/custom-widgets.md +++ b/docs/guide/custom-widgets.md @@ -6,50 +6,55 @@ Creating new widget options in addition to the [core widgets](/guide/core-widget Adding a new widget type involves creating a new module that extends the `@apostrophecms/widget-type` module. It also requires a template to render the editor input. The module configuration file will include a [field schema](/guide/content-schema.md) with the appropriate fields. -We will use the example of a two column **"layout widget."** It is a fairly common and relatively simple use case that allows editors to visually align content in a row. This version of a layout widget consists of two areas next to one another. Each will allow either rich text and image widgets nested inside. +We will use the example of a **"feature card" widget**. It is a common and relatively simple use case: a headline, a linked image, and a body that editors can fill with nested widgets. It shows off the two things most custom widgets need — a mix of schema field types, and a sub-area that can hold other widgets. -First create the module configuration file, extend the core widget type module, and add a widget label for editors. If you do not add a label, Apostrophe will attempt to generate one for the UI based on the module's name. - -**The module's name must end in `-widget`.** It is a convention that supports core business logic around widgets and can help keep project code organized. This two-column widget is named `two-column-widget`. +::: info +If you are looking for a way to place content in columns, you do not need a custom widget. Apostrophe ships a grid-based [layout widget](/guide/core-widgets.md#layout-widget) that lets editors add, remove, and resize columns in context. +::: -::: tip -Generate the starter code using the [official CLI](/guide/development-setup.md#installing-the-apostrophe-cli) with the command: +First create the module configuration file, extend the core widget type module, and add a widget label for editors. If you do not add a label, Apostrophe will attempt to generate one for the UI based on the module's name. -```bash -apos add widget two-column -``` -::: +**The module's name must end in `-widget`.** It is a convention that supports core business logic around widgets and can help keep project code organized. This feature card widget is named `feature-card-widget`. ``` js -module.exports = { +export default { extend: '@apostrophecms/widget-type', options: { - label: 'Two column' + label: 'Feature card' }, // 👇 The widget type's field schema fields: { add: { - // 👇 The first column area - columnOne: { - type: 'area', - label: 'Column One', - options: { - widgets: { - '@apostrophecms/rich-text': {}, - '@apostrophecms/image': {} - } - } + title: { + type: 'string', + label: 'Title', + required: true + }, + // 👇 A relationship to a piece of the core image type + _image: { + type: 'relationship', + label: 'Image', + withType: '@apostrophecms/image', + max: 1 + }, + link: { + type: 'url', + label: 'Link URL' }, - // 👇 The second column area - columnTwo: { + linkLabel: { + type: 'string', + label: 'Link Label' + }, + // 👇 A sub-area, so editors can nest other widgets inside this one + body: { type: 'area', - label: 'Column Two', + label: 'Body', options: { widgets: { '@apostrophecms/rich-text': {}, - '@apostrophecms/image': {} + '@apostrophecms/video': {} } } } @@ -59,7 +64,7 @@ module.exports = { ``` @@ -69,10 +74,13 @@ You can then add this module to the `app.js` file to instantiate it. ``` js -require('apostrophe')({ +import apostrophe from 'apostrophe'; + +apostrophe({ + root: import.meta, shortName: 'my-website', modules: { - 'two-column-widget': {} + 'feature-card-widget': {} } }); ``` @@ -129,7 +137,7 @@ Much like the core widgets, you can add placeholder content for many of the fiel ``` js -module.exports = { +export default { extend: '@apostrophecms/widget-type', options: { label: 'Custom Widget', @@ -251,47 +259,66 @@ This approach allows you to implement complex validation rules that can access t ## Widget templates -Before using the new widget type, it needs a template file, `widget.html`, in the module's `views` directory. A simple template for the two column widget might look like: +Before using the new widget type, it needs a template file, `widget.jsx`, in the module's `views` directory. A simple template for the feature card widget might look like: -``` nunjucks -
-
- {% area data.widget, 'columnOne' %} -
-
- {% area data.widget, 'columnTwo' %} -
-
+``` jsx +export default function ({ widget }, { apos, Area }) { + /* Relationship fields always come back as arrays. `apos.image.first` + safely returns the first attachment, or nothing at all. */ + const attachment = apos.image.first(widget._image); + + return ( +
+ {attachment && ( + {widget._image[0].alt + )} +

{widget.title}

+ {/* 👇 The sub-area renders whatever widgets the editor nested inside */} +
+ +
+ {widget.link && ( + + {widget.linkLabel || 'Learn more'} + + )} +
+ ); +} ```
-**Widget field values are available on `data.widget` in templates.** [Context options](/guide/areas-and-widgets.md#passing-context-options) passed in are available on `data.contextOptions`. +**Widget field values arrive as the `widget` property of the template's first argument.** [Context options](/guide/areas-and-widgets.md#passing-context-options) passed in are available as `contextOptions`. See the [JSX templates guide](/guide/jsx-templates.md) for the full set of helpers, and [Nunjucks templating](/guide/templating.md) if you prefer to write `widget.html` instead. ::: info -Here are some two-column styles for people following along. +Here are some feature card styles for people following along. ```css -.two-col { +.feature-card { display: flex; - flex-flow: row wrap; - width: 100%; + flex-direction: column; + gap: 8px; + max-width: 32rem; } -.two-col__column { - display: flex; - flex-direction: column; - flex: 1; +.feature-card__image { + width: 100%; + height: auto; } ``` @@ -303,21 +330,13 @@ When adding client-side JavaScript for widget interaction, add a widget "player" We can use the example of a basic collapsible section widget, `collapse-widget` (also known as an "accordion" or "disclosure" widget). It will hide detail text until a user clicks the header/button. -::: tip -When using the [official CLI](/guide/development-setup.md#installing-the-apostrophe-cli) to create a widget type, include widget player starter code with the `--player` option. - -```bash -apos add widget collapse --player -``` -::: - ::: details Example collapsible widget code **Module configuration** ```javascript -module.exports = { +export default { extend: '@apostrophecms/widget-type', options: { label: 'Collapsible section' @@ -345,21 +364,32 @@ modules/collapse-widget/index.js **Module template** -``` nunjucks -
-

- -

- -
+``` jsx +export default function ({ widget }) { + return ( +
+

+ +

+ +
+ ); +} ```
@@ -443,13 +473,13 @@ For example, we could change our collapse widget to include a `color` field valu -``` nunjucks -
- {# The rest of the code is the same... #} +``` jsx +
+ {/* The rest of the code is the same... */}
``` @@ -469,18 +499,18 @@ export default () => { }; ``` The player *does* have access to the widget's wrapping element, so we use `el.dataset.color` to access the color data we stored on `data-color`. ::: tip -We can pass a string, number, or boolean value with a data attribute using the method shown above. If the value we need to use in the widget player is an array or object, it will need to become a properly escaped string first. Use the `jsonAttribute` template filter to do this. +We can pass a string, number, or boolean value with a data attribute using the method shown above. If the value we need to use in the widget player is an array or object, it will need to become a JSON string first — passing the object itself renders the useless string `[object Object]`. -``` nunjucks -
+``` jsx +
``` -The value will be converted to a JSON string and escaped. The original value can be retrieved in the player with `JSON.parse`. +Apostrophe escapes attribute values for you, so no further filtering is needed. The original value can be retrieved in the player with `JSON.parse`. In a Nunjucks template, the equivalent is the `jsonAttribute` filter. ::: diff --git a/docs/guide/jsx-templates.md b/docs/guide/jsx-templates.md index 56e8c4a6..0eb2328c 100644 --- a/docs/guide/jsx-templates.md +++ b/docs/guide/jsx-templates.md @@ -26,7 +26,7 @@ JSX templates live in the same `views/` directories as Nunjucks templates, and A ``` modules/default-page/views/page.jsx -modules/two-column-widget/views/widget.jsx +modules/feature-card-widget/views/widget.jsx modules/blog/views/newest.jsx views/layout.jsx ``` @@ -373,21 +373,32 @@ Widget templates work the same way as page templates: a default-exported functio ```jsx -export default function({ widget, contextOptions }, { Area }) { +export default function({ widget, contextOptions }, { apos, Area }) { + const attachment = apos.image.first(widget._image); return ( -
-
- -
-
- +
+ {attachment && ( + {widget._image[0].alt + )} +

{widget.title}

+
+
+ {widget.link && ( + + {widget.linkLabel || 'Learn more'} + + )}
); } ``` From 48c7859664abe079de3dc0ae2a30ed429f8490e9 Mon Sep 17 00:00:00 2001 From: Robert Means Date: Thu, 27 Aug 2026 09:22:25 -0400 Subject: [PATCH 2/2] Updates incorrect key addition --- docs/guide/custom-widgets.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/guide/custom-widgets.md b/docs/guide/custom-widgets.md index 91937887..d5745d59 100644 --- a/docs/guide/custom-widgets.md +++ b/docs/guide/custom-widgets.md @@ -378,10 +378,10 @@ export default function ({ widget }) { newlines instead — this keeps the text escaped, so there is no need for the `safe` filter's counterpart either. */} {widget.detail.split('\n').map((line, i) => ( - + <> + {i > 0 &&
} {line} -
-
+ ))}