Skip to content

Commit

Permalink
Documentation - Add initial Stimulus docs
Browse files Browse the repository at this point in the history
- Move extending React to new extending client-side page within advanced topics
- Add general extending JavaScript / client-side overview
- Prepare initial Stimulus usage documentation
- Resolves wagtail#10197
- Apply suggestions from code review - Co-authored-by: Thibaud Colas <thibaudcolas@gmail.com>
  • Loading branch information
lb- authored and thibaudcolas committed Oct 19, 2023
1 parent 1da3e5f commit 8002e75
Show file tree
Hide file tree
Showing 9 changed files with 300 additions and 44 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.txt
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ Changelog
* Do not render minimap if there are no panel anchors (Sage Abdullah)
* Use dropdown buttons on listings in dashboard panels (Sage Abdullah)
* Implement breadcrumbs design refinements (Thibaud Colas)
* Support extending Wagtail client-side with Stimulus (LB (Ben) Johnston)
* Fix: Ensure that StreamField's `FieldBlock`s correctly set the `required` and `aria-describedby` attributes (Storm Heg)
* Fix: Avoid an error when the moderation panel (admin dashboard) contains both snippets and private pages (Matt Westcott)
* Fix: When deleting collections, ensure the collection name is correctly shown in the success message (LB (Ben) Johnston)
Expand Down
46 changes: 2 additions & 44 deletions docs/advanced_topics/customisation/admin_templates.md
Original file line number Diff line number Diff line change
Expand Up @@ -232,48 +232,6 @@ To add extra buttons to the password reset form, override the `submit_buttons` b
{% endblock %}
```

(extending_client_side_react)=

## Extending client-side React components

Some of Wagtail’s admin interface is written as client-side JavaScript with [React](https://reactjs.org/).
In order to customise or extend those components, you may need to use React too, as well as other related libraries.
To make this easier, Wagtail exposes its React-related dependencies as global variables within the admin. Here are the available packages:

```javascript
// 'focus-trap-react'
window.FocusTrapReact;
// 'react'
window.React;
// 'react-dom'
window.ReactDOM;
// 'react-transition-group/CSSTransitionGroup'
window.CSSTransitionGroup;
```

Wagtail also exposes some of its own React components. You can reuse:
## Extending client-side JavaScript

```javascript
window.wagtail.components.Icon;
window.wagtail.components.Portal;
```

Pages containing rich text editors also have access to:

```javascript
// 'draft-js'
window.DraftJS;
// 'draftail'
window.Draftail;

// Wagtail’s Draftail-related APIs and components.
window.draftail;
window.draftail.DraftUtils;
window.draftail.ModalWorkflowSource;
window.draftail.ImageModalWorkflowSource;
window.draftail.EmbedModalWorkflowSource;
window.draftail.LinkModalWorkflowSource;
window.draftail.DocumentModalWorkflowSource;
window.draftail.Tooltip;
window.draftail.TooltipEntity;
```
Wagtail provides multiple ways to [extend client-side JavaScript](extending_client_side).
2 changes: 2 additions & 0 deletions docs/advanced_topics/customisation/streamfield_blocks.md
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,8 @@ A form template for a StructBlock must include the output of `render_form` for e
</div>
```

(custom_streamfield_blocks_media)=

## Additional JavaScript on `StructBlock` forms

Often it may be desirable to attach custom JavaScript behaviour to a StructBlock form. For example, given a block such as:
Expand Down
2 changes: 2 additions & 0 deletions docs/advanced_topics/documents/title_generation_on_upload.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
(docs_title_generation_on_upload)=

# Title generation on upload

When uploading a file (document), Wagtail takes the filename, removes the file extension, and populates the title field. This section is about how to customise this filename to title conversion.
Expand Down
2 changes: 2 additions & 0 deletions docs/advanced_topics/images/title_generation_on_upload.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
(images_title_generation_on_upload)=

# Title generation on upload

When uploading an image, Wagtail takes the filename, removes the file extension, and populates the title field. This section is about how to customise this filename to title conversion.
Expand Down
277 changes: 277 additions & 0 deletions docs/extending/extending_client_side.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,277 @@
(extending_client_side)=

# Extending client-side behaviour

Many kinds of common customisations can be done without reaching into JavaScript, but depending on what parts of the client-side interaction you want to leverage or customise, you may need to employ React, Stimulus, or plain (vanilla) JS.

[React](https://reactjs.org/) is used for more complex parts of Wagtail, such as the sidebar, commenting system, and the Draftail rich-text editor.
For basic JavaScript-driven interaction, Wagtail is migrating towards [Stimulus](https://stimulus.hotwired.dev/).

You don't need to know or use these libraries to add your custom behaviour to elements, and in many cases, simple JavaScript will work fine, but Stimulus is the recommended approach for more complex use cases.

You don't need to have Node.js tooling running for your custom Wagtail installation for many customisations built on these libraries, but in some cases, such as building packages, it may make more complex development easier.

```{note}
Avoid using jQuery and undocumented jQuery plugins, as they will be removed in a future version of Wagtail.
```

(extending_client_side_injecting_javascript)=

## Adding custom JavaScript

Within Wagtail's admin interface, there are a few ways to add JavaScript.

The simplest way is to add global JavaScript files via hooks, see [](insert_editor_js) and [](insert_global_admin_js).

For JavaScript added when a specific Widget is used you can add an inner `Media` class to ensure that the file is loaded when the widget is used, see [Django's docs on their form `Media` class](https://docs.djangoproject.com/en/stable/topics/forms/media/#assets-as-a-static-definition).

In a similar way, Wagtail's [template components](template_components) provide a `media` property or `Media` class to add scripts when rendered.

These will ensure the added files are used in the admin after the core JavaScript admin files are already loaded.

(extending_client_side_using_events)=

## Extending with DOM events

When approaching client-side customisations or adopting new components, try to keep the implementation simple first, you may not need any knowledge of Stimulus, React, JavaScript Modules or a build system to achieve your goals.

The simplest way to attach behaviour to the browser is via [DOM Events](https://developer.mozilla.org/en-US/docs/Learn/JavaScript/Building_blocks/Events) and plain (vanilla) JavaScript.

### Wagtail's custom DOM events

Wagtail supports some custom behaviour via listening or dispatching custom DOM events.

- See [Images title generation on upload](images_title_generation_on_upload).
- See [Documents title generation on upload](docs_title_generation_on_upload).

(extending_client_side_stimulus)=

## Extending with Stimulus

Wagtail uses [Stimulus](https://stimulus.hotwired.dev/) as a way to provide lightweight client-side interactivity or custom JavaScript widgets within the admin interface.

The key benefit of using Stimulus is that your code can avoid the need for manual initialisation when widgets appear dynamically, such as within modals, `InlinePanel`, or `StreamField` panels.

The [Stimulus handbook](https://stimulus.hotwired.dev/handbook/introduction) is the best source on how to work with and understand Stimulus.

### Adding a custom Stimulus controller

Wagtail exposes two client-side globals for using Stimulus.

1. `window.wagtail.app` the core admin Stimulus application instance.
2. `window.StimulusModule` Stimulus module as exported from `@hotwired/stimulus`.

First, create a custom [Stimulus controller](https://stimulus.hotwired.dev/reference/controllers) that extends the base `window.StimulusModule.Controller` using [JavaScript class inheritance](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Classes). If you are using a build tool you can import your base controller via `import { Controller } from '@hotwired/stimulus';`.

Once you have created your custom controller, you will need to [register your Stimulus controllers manually](https://stimulus.hotwired.dev/reference/controllers#registering-controllers-manually) via the `window.wagtail.app.register` method.

#### A simple controller example

First, create your HTML so that appears somewhere within the Wagtail admin.

```html
<!-- Will log 'My controller has connected: hi' to the console -->
<div data-controller="my-controller">Hi</div>
<!-- Will log 'My controller has connected: hello' to the console, with the span element-->
<div data-controller="my-controller">
Hello <span data-my-controller-target="label"></span>
</div>
```

Second, create a JavaScript file that will contain your controller code. This controller logs a simple message on `connect`, which is once the controller has been created and connected to an HTML element with the matching `data-controller` attribute.

```javascript
// myapp/static/js/example.js

class MyController extends window.StimulusModule.Controller {
static targets = ['label'];
connect() {
console.log(
'My controller has connected:',
this.element.innerText,
this.labelTargets,
);
}
}

window.wagtail.app.register('my-controller', MyController);
```

Finally, load the JavaScript file into Wagtail's admin with a hook.

```python
# myapp/wagtail_hooks.py
from django.templatetags.static import static
from django.utils.safestring import mark_safe

from wagtail import hooks

@hooks.register('insert_global_admin_js')
def global_admin_js():
return mark_safe(
f'<script src="{static("js/example.js")}"></script>',
)
```

You should now be able to refresh your admin that was showing the HTML and see two logs in the console.

#### A more complex controller example

Now we will create a `WordCountController` that adds a small `output` element next to the controlled `input` element that shows a count of how many words have been entered.

```javascript
// myapp/static/js/word-count-controller.js
class WordCountController extends window.StimulusModule.Controller {
static values = { max: { default: 10, type: Number } };

connect() {
this.setupOutput();
this.updateCount();
}

setupOutput() {
if (this.output) return;
const template = document.createElement('template');
template.innerHTML = `<output name='word-count' for='${this.element.id}' class='output-label'></output>`;
const output = template.content.firstChild;
this.element.insertAdjacentElement('beforebegin', output);
this.output = output;
}

updateCount(event) {
const value = event ? event.target.value : this.element.value;
const words = (value || '').split(' ');
this.output.textContent = `${words.length} / ${this.maxValue} words`;
}

disconnect() {
this.output && this.output.remove();
}
}
window.wagtail.app.register('word-count', WordCountController);
```

This lets the data attribute `data-word-count-max-value` determine the 'configuration' of this controller and the data attribute actions to determine the 'triggers' for the updates to the output element.

```python
# models.py
from django import forms

from wagtail.admin.panels import FieldPanel
from wagtail.models import Page


class BlogPage(Page):
# ...
content_panels = Page.content_panels + [
FieldPanel('subtitle', classname="full"),
FieldPanel(
'introduction',
classname="full",
widget=forms.TextInput(
attrs={
'data-controller': 'word-count',
# allow the max number to be determined with attributes
# note we can use Python values here, Django will handle the string conversion (including escaping if applicable)
'data-word-count-max-value': 5,
# decide when you want the count to update with data-action
# (e.g. 'blur->word-count#updateCount' will only update when field loses focus)
'data-action': 'word-count#updateCount paste->word-count#updateCount',
}
)
),
#...
```

This next code snippet shows a more advanced version of the `insert_editor_js` hook usage which is set up to append additional scripts for future controllers.

```python
# wagtail_hooks.py
from django.utils.html import format_html_join
from django.templatetags.static import static

from wagtail import hooks


@hooks.register('insert_editor_js')
def editor_js():
# add more controller code as needed
js_files = ['js/word-count-controller.js',]
return format_html_join('\n', '<script src="{0}"></script>',
((static(filename),) for filename in js_files)
)
```

You should be able to see that on your Blog Pages, the introduction field will now have a small `output` element showing the count and max words being used.

#### Using a build system

You will need ensure your build output is ES6/ES2015 or higher. You can use the exposed global module at `window.StimulusModule` or provide your own using the npm module `@hotwired/stimulus`.

```javascript
// myapp/static/js/word-count-controller.js
import { Controller } from '@hotwired/stimulus';

class WordCountController extends Controller {
// ... the same as above
}

window.wagtail.app.register('word-count', WordCountController);
```

You may want to avoid bundling Stimulus with your JavaScript output and treat the global as an external/alias module, refer to your build system documentation for instructions on how to do this.

## Extending with React

To customise or extend the [React](https://reactjs.org/) components, you may need to use React too, as well as other related libraries.

To make this easier, Wagtail exposes its React-related dependencies as global variables within the admin. Here are the available packages:

```javascript
// 'focus-trap-react'
window.FocusTrapReact;
// 'react'
window.React;
// 'react-dom'
window.ReactDOM;
// 'react-transition-group/CSSTransitionGroup'
window.CSSTransitionGroup;
```

Wagtail also exposes some of its own React components. You can reuse:

```javascript
window.wagtail.components.Icon;
window.wagtail.components.Portal;
```

Pages containing rich text editors also have access to:

```javascript
// 'draft-js'
window.DraftJS;
// 'draftail'
window.Draftail;

// Wagtail’s Draftail-related APIs and components.
window.draftail;
window.draftail.DraftUtils;
window.draftail.ModalWorkflowSource;
window.draftail.ImageModalWorkflowSource;
window.draftail.EmbedModalWorkflowSource;
window.draftail.LinkModalWorkflowSource;
window.draftail.DocumentModalWorkflowSource;
window.draftail.Tooltip;
window.draftail.TooltipEntity;
```

## Extending Draftail

- [](extending_the_draftail_editor)

## Extending StreamField

- [](streamfield_widget_api)
- [](custom_streamfield_blocks_media)

(extending_client_side_react)=
1 change: 1 addition & 0 deletions docs/extending/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ audit_log
custom_account_settings
customising_group_views
custom_image_filters
extending_client_side
rich_text_internals
extending_draftail
custom_bulk_actions
Expand Down
2 changes: 2 additions & 0 deletions docs/reference/streamfield/widget_api.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@

In order for the StreamField editing interface to dynamically create form fields, any Django form widgets used within StreamField blocks must have an accompanying JavaScript implementation, defining how the widget is rendered client-side and populated with data, and how to extract data from that field. Wagtail provides this implementation for widgets inheriting from `django.forms.widgets.Input`, `django.forms.Textarea`, `django.forms.Select` and `django.forms.RadioSelect`. For any other widget types, or ones which require custom client-side behaviour, you will need to provide your own implementation.

This implementation can be driven by [Stimulus](extending_client_side_stimulus) or for deeper integrations you can leverage telepath.

The [telepath](https://wagtail.github.io/telepath/) library is used to set up mappings between Python widget classes and their corresponding JavaScript implementations. To create a mapping, define a subclass of `wagtail.widget_adapters.WidgetAdapter` and register it with `wagtail.telepath.register`.

```python
Expand Down
11 changes: 11 additions & 0 deletions docs/releases/5.2.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,17 @@ We expect those changes to greatly reduce the weight of images for all Wagtail s

This feature was developed by Paarth Agarwal and Thibaud Colas as part of the Google Summer of Code program and a [partnership with the Green Web Foundation](https://www.thegreenwebfoundation.org/news/working-with-the-wagtail-community-on-the-summer-of-code/) and Green Coding Berlin, with support from Dan Braghiș, Thibaud Colas, Sage Abdullah, Arne Tarara (Green Coding Berlin), and Chris Adams (Green Web Foundation). We also thank Aman Pandey for introducing [AVIF support](image_file_formats) in Wagtail 5.1, Andy Babic for creating [`AbstractImage.get_renditions()`](image_renditions_multiple) in the same release; and Storm Heg, Mitchel Cabuloy, Coen van der Kamp, Tom Dyson, and Chris Lawton for their feedback on [RFC 71](https://github.com/wagtail/rfcs/pull/71).

### Support extending Wagtail client-side with Stimulus

Wagtail now officially supports client-side admin customisations with [Stimulus](https://stimulus.hotwired.dev/). The developer documentation has a dedicated page about [](extending_client_side). This covers fundamental topics of client-side extensibility, such as:

* Adding custom JavaScript
* Extending with DOM events and Wagtail's custom DOM events
* Extending with Stimulus
* Extending with React

Thank you to core contributor (LB (Ben) Johnston) for writing this documentation.

### Other features

* Add support for Python 3.12 (Matt Westcott)
Expand Down

0 comments on commit 8002e75

Please sign in to comment.