Skip to content

Commit 22b2e0c

Browse files
authored
Merge pull request #1532 from meirzamoodle/MDL-87765_react_autoinit
MDL-87765 [docs] Add Mustache React helper + react_autoinit integration
2 parents 9448879 + 36cb56c commit 22b2e0c

2 files changed

Lines changed: 219 additions & 0 deletions

File tree

Lines changed: 218 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,218 @@
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": "@moodle/lms/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": "@moodle/lms/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 be a fully-qualified ESM import specifier in the form:
92+
93+
```text
94+
@moodle/lms/<component>/<path>
95+
```
96+
97+
Examples:
98+
99+
```html
100+
<div data-react-component="@moodle/lms/mod_book/viewer"></div>
101+
<div data-react-component="@moodle/lms/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="@moodle/lms/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+
The specifier in `data-react-component` is passed directly to a dynamic `import()` call. The browser resolves it through the Moodle import map, which maps `@moodle/lms/<component>/<path>` to the built JS file under the component's `js/esm/build/` directory.
120+
121+
For example, `@moodle/lms/mod_book/viewer` resolves to `mod/book/js/esm/build/viewer.js`.
122+
123+
If the import fails, mounting is skipped and an error is logged to the console.
124+
125+
## Export contract
126+
127+
`react_autoinit` expects a **default-exported React function component**:
128+
129+
```tsx title="mod/book/js/esm/src/viewer.tsx"
130+
type Props = {
131+
title?: string;
132+
chapter?: string;
133+
};
134+
135+
export default function Viewer({title = 'Book', chapter = 'Chapter 1'}: Props) {
136+
return (
137+
<div>
138+
<h1>{title}</h1>
139+
<p>{chapter}</p>
140+
</div>
141+
);
142+
}
143+
```
144+
145+
The component is mounted with `react-dom/client` `createRoot`. If `module.default` is not found, `react_autoinit` logs a warning and skips mounting.
146+
147+
## Lifecycle internals
148+
149+
### Initial run
150+
151+
`react_autoinit` calls `init()` automatically when the bundle loads.
152+
153+
Sequence:
154+
155+
1. Wait for `DOMContentLoaded` (or resolve immediately if the DOM is already ready).
156+
2. Scan all `[data-react-component]` elements in the document.
157+
3. Mount each one.
158+
4. Install a single global `MutationObserver`.
159+
160+
### Mount guard
161+
162+
Each successfully mounted element receives `dataset.reactMounted = "1"`. This prevents duplicate mounting when the same region is rescanned.
163+
164+
### Unmount tracking
165+
166+
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.
167+
168+
## Dynamic content (AJAX and fragments)
169+
170+
A `MutationObserver` watches `document.documentElement` with `childList: true` and `subtree: true`.
171+
172+
When content is added:
173+
174+
- If the added node matches `[data-react-component]`, it is mounted.
175+
- If the added node contains matching descendants, each descendant is mounted.
176+
177+
When content is removed:
178+
179+
- If the removed node matches, it is unmounted.
180+
- If the removed node contains matching descendants, each descendant is unmounted.
181+
182+
This means React components inside AJAX-loaded fragments or dynamic regions are handled automatically without any additional initializer call.
183+
184+
## Building components
185+
186+
Run the Grunt `react` task from the Moodle root:
187+
188+
```bash
189+
# Production build — minified, no source maps
190+
grunt react
191+
192+
# Development build — readable output, inline source maps
193+
grunt react:dev
194+
195+
# Watch mode — rebuilds changed files automatically
196+
grunt react:watch
197+
```
198+
199+
The build tool discovers all `js/esm/src/**/*.{ts,tsx}` files across core and plugins automatically. No registration is required.
200+
201+
## Debugging checklist
202+
203+
If a component does not render:
204+
205+
1. Check that `data-react-component` uses the `@moodle/lms/<component>/<path>` format.
206+
2. Confirm the built file exists under `js/esm/build/`.
207+
3. Confirm the module has a default-exported function component.
208+
4. Check the browser console for messages prefixed with `[react_autoinit]`.
209+
210+
If a component mounts multiple times:
211+
212+
1. Ensure the container element is not recreated on every re-render by the surrounding template.
213+
2. Do not call `createRoot` manually on an element already managed by `react_autoinit`.
214+
215+
## See also
216+
217+
- [Mustache templates](../../templates)
218+
- [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)