Skip to content

Commit bfeaa53

Browse files
committed
MDL-87765 [docs] Add Mustache React helper + react_autoinit integration guide
Add docs/guides/javascript/react/reactautoinit.md documenting: - {{#react}} helper JSON contract and parsing behavior - data-react-component / data-react-props DOM contract - module resolution rules to @moodle/lms/... - default export expectations, mount/unmount lifecycle, and MutationObserver behavior - build commands and debugging checklist Add autoinit to project-words.txt
1 parent 9448879 commit bfeaa53

2 files changed

Lines changed: 238 additions & 0 deletions

File tree

Lines changed: 237 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,237 @@
1+
---
2+
title: Mustache Helper and Autoinit
3+
tags:
4+
- react
5+
- javascript
6+
description: How Moodle's Mustache React helper and react_autoinit work together to render, mount, and manage React components.
7+
---
8+
9+
import { Since } from '@site/src/components';
10+
11+
<Since version="5.2" issueNumber="MDL-87765" />
12+
13+
This page explains the combined developer contract between `mustache_react_helper` and `react_autoinit`, including how template JSON is converted into markup, how modules are resolved, and how components mount and unmount.
14+
15+
## Purpose
16+
17+
Use this integration when UI is produced by Mustache or fragment HTML and React components should mount automatically without manual bootstrap code.
18+
19+
Source files:
20+
21+
- `public/lib/classes/output/mustache_react_helper.php`
22+
- `public/lib/js/esm/src/react_autoinit.ts`
23+
24+
## End-to-end flow
25+
26+
1. You write a `{{#react}} ... {{/react}}` block in a Mustache template.
27+
2. `mustache_react_helper` converts the JSON config into a `<div>` with `data-react-component` and `data-react-props` attributes.
28+
3. `react_autoinit` finds the element, imports the module via the browser import map, and mounts it.
29+
4. If the region is replaced later (AJAX/fragments), components are mounted or unmounted automatically.
30+
31+
## Mustache helper (`{{#react}}`)
32+
33+
The `{{#react}}` block accepts a JSON object followed by optional fallback HTML content:
34+
35+
```mustache title="mod/book/templates/view.mustache"
36+
{{#react}}
37+
{
38+
"component": "@mod_book/viewer",
39+
"props": {
40+
"title": "{{title}}",
41+
"chapter": "{{chapter}}"
42+
},
43+
"id": "book-viewer",
44+
"class": "book-viewer-wrapper"
45+
}
46+
<p>Loading…</p>
47+
{{/react}}
48+
```
49+
50+
### JSON keys
51+
52+
| Key | Required | Maps to |
53+
|-----|----------|---------|
54+
| `component` | Yes | `data-react-component` attribute |
55+
| `props` | No | `data-react-props` attribute (JSON-encoded) |
56+
| Any other key | No | Regular HTML attribute (`id`, `class`, `aria-*`, etc.) |
57+
58+
### Mustache tags inside the block
59+
60+
The entire block is passed through the Mustache renderer before the JSON is parsed. This means any Mustache tag — including `{{#str}}`, `{{#quote}}`, template variables, and other helpers — can appear inside the JSON values:
61+
62+
```mustache
63+
{{#react}}
64+
{
65+
"component": "@mod_book/viewer",
66+
"props": {
67+
"title": "{{title}}",
68+
"confirmLabel": "{{#str}}confirm, core{{/str}}",
69+
"cancelLabel": "{{#str}}cancel, core{{/str}}"
70+
}
71+
}
72+
{{/react}}
73+
```
74+
75+
The rendered output is a plain string before `mustache_react_helper` attempts JSON parsing, so any valid Mustache syntax is supported.
76+
77+
### Parsing behaviour
78+
79+
- Mustache variables inside the block are rendered before the JSON is parsed.
80+
- Trailing commas in the JSON object are stripped automatically.
81+
- If the JSON is invalid but fallback HTML content is present, a plain `<div>` with the fallback is rendered.
82+
- If the JSON is invalid and there is no fallback content, an empty string is returned.
83+
- Invalid JSON is reported via `debugging()` at `DEBUG_DEVELOPER` level.
84+
85+
Boolean attribute values: if a key's value is `true`, the attribute name is emitted without a value. If `false`, the attribute is omitted.
86+
87+
## The DOM contract
88+
89+
### `data-react-component`
90+
91+
The component specifier must follow the format:
92+
93+
```text
94+
@namespace/module_path
95+
```
96+
97+
Examples:
98+
99+
```html
100+
<div data-react-component="@mod_book/viewer"></div>
101+
<div data-react-component="@core_calendar/event_chip"></div>
102+
```
103+
104+
### `data-react-props`
105+
106+
An optional JSON object passed as the props to the React component:
107+
108+
```html
109+
<div
110+
data-react-component="@mod_book/viewer"
111+
data-react-props='{"title":"My Book","chapter":"Chapter 1"}'
112+
></div>
113+
```
114+
115+
If the value is not valid JSON, `react_autoinit` logs an error and falls back to `{}`.
116+
117+
## How module resolution works
118+
119+
Given `@namespace/path`, `react_autoinit` builds an ESM specifier of the form:
120+
121+
```text
122+
@moodle/lms/<component>/<path>
123+
```
124+
125+
Namespace conversion rules:
126+
127+
| Namespace | Rule | Result |
128+
|-----------|------|--------|
129+
| `core` | Used as-is | `@moodle/lms/core/…` |
130+
| `core_*` | Used as-is | `@moodle/lms/core_calendar/…` |
131+
| Contains `_` | Treated as a frankenstyle plugin name | `@moodle/lms/mod_book/…` |
132+
| Anything else | Prefixed with `core_` | `@moodle/lms/core_calendar/…` |
133+
134+
Examples:
135+
136+
| Specifier | Resolved to |
137+
|-----------|-------------|
138+
| `@mod_book/viewer` | `@moodle/lms/mod_book/viewer` |
139+
| `@core_calendar/event_chip` | `@moodle/lms/core_calendar/event_chip` |
140+
| `@calendar/event_chip` | `@moodle/lms/core_calendar/event_chip` |
141+
142+
The browser resolves the resulting specifier through the import map. If the import fails, mounting is skipped and an error is logged to the console.
143+
144+
## Export contract
145+
146+
`react_autoinit` expects a **default-exported React function component**:
147+
148+
```tsx title="mod/book/js/esm/src/viewer.tsx"
149+
type Props = {
150+
title?: string;
151+
chapter?: string;
152+
};
153+
154+
export default function Viewer({title = 'Book', chapter = 'Chapter 1'}: Props) {
155+
return (
156+
<div>
157+
<h1>{title}</h1>
158+
<p>{chapter}</p>
159+
</div>
160+
);
161+
}
162+
```
163+
164+
The component is mounted with `react-dom/client` `createRoot`. If `module.default` is not found, `react_autoinit` logs a warning and skips mounting.
165+
166+
## Lifecycle internals
167+
168+
### Initial run
169+
170+
`react_autoinit` calls `init()` automatically when the bundle loads.
171+
172+
Sequence:
173+
174+
1. Wait for `DOMContentLoaded` (or resolve immediately if the DOM is already ready).
175+
2. Scan all `[data-react-component]` elements in the target root.
176+
3. Mount each one.
177+
4. Install a single global `MutationObserver`.
178+
179+
### Mount guard
180+
181+
Each successfully mounted element receives `dataset.reactMounted = "1"`. This prevents duplicate mounting when the same region is rescanned.
182+
183+
### Unmount tracking
184+
185+
The cleanup function returned by `createRoot().unmount` is stored in a `WeakMap<Element, () => void>`. When the element is removed from the DOM, the cleanup function is called automatically.
186+
187+
## Dynamic content (AJAX and fragments)
188+
189+
A `MutationObserver` watches `document.documentElement` with `childList: true` and `subtree: true`.
190+
191+
When content is added:
192+
193+
- If the added node matches `[data-react-component]`, it is mounted.
194+
- If the added node contains matching descendants, each descendant is mounted.
195+
196+
When content is removed:
197+
198+
- If the removed node matches, it is unmounted.
199+
- If the removed node contains matching descendants, each descendant is unmounted.
200+
201+
This means React components inside AJAX-loaded fragments or dynamic regions are handled automatically without any additional initializer call.
202+
203+
## Building components
204+
205+
Run the Grunt `react` task from the Moodle root:
206+
207+
```bash
208+
# Production build — minified, no source maps
209+
grunt react
210+
211+
# Development build — readable output, inline source maps
212+
grunt react:dev
213+
214+
# Watch mode — rebuilds changed files automatically
215+
grunt react:watch
216+
```
217+
218+
The build tool discovers all `js/esm/src/**/*.{ts,tsx}` files across core and plugins automatically. No registration is required.
219+
220+
## Debugging checklist
221+
222+
If a component does not render:
223+
224+
1. Check that `data-react-component` uses the `@namespace/path` format.
225+
2. Confirm the built file exists under `js/esm/build/`.
226+
3. Confirm the module has a default-exported function component.
227+
4. Check the browser console for messages prefixed with `[react_autoinit]`.
228+
229+
If a component mounts multiple times:
230+
231+
1. Ensure the container element is not recreated on every re-render by the surrounding template.
232+
2. Do not call `createRoot` manually on an element already managed by `react_autoinit`.
233+
234+
## See also
235+
236+
- [Mustache templates](../../templates)
237+
- [JavaScript modules](../modules)

project-words.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -360,3 +360,4 @@ modvisible
360360
savechanges
361361
hideif
362362
formslib
363+
autoinit

0 commit comments

Comments
 (0)