Help us improve
Share bugs, ideas, or general feedback.
From moodle-dev
Writes JavaScript for Moodle plugins using AMD modules, ES6 source, grunt build, RequireJS loading, core/ajax, core/str, core/templates, core/modal, core/notification, Mustache template hydration, and AMD unit tests.
npx claudepluginhub saadrahman01/claude-moodle-dev --plugin moodle-devHow this skill is triggered — by the user, by Claude, or both
Slash command
/moodle-dev:moodle-amd-javascriptThe summary Claude sees in its skill listing — used to decide when to auto-load this skill
Moodle uses AMD (RequireJS) for browser JS. Source goes in `<plugin>/amd/src/*.js` (ES6+), built into minified AMD in `amd/build/*.min.js` via `grunt`. Never edit `amd/build/` directly. Modules import Moodle core helpers via string IDs (`core/ajax`, `core/str`, `core/templates`).
Guides Moodle plugin development: version.php, DB install/upgrade, capabilities, web services, PSR-4 autoloading, hooks, settings, privacy provider, and coding standards.
Creates custom external web service APIs for Moodle LMS following the three-method pattern (parameters, execute, returns). Covers class structure, parameter types, and Moodle coding standards.
Enforces opinionated conventions for modern JavaScript and Node.js: ES2023+ features, async/await patterns, ESM/CJS modules, Web Workers, browser APIs, performance optimization, and Node patterns.
Share bugs, ideas, or general feedback.
Moodle uses AMD (RequireJS) for browser JS. Source goes in <plugin>/amd/src/*.js (ES6+), built into minified AMD in amd/build/*.min.js via grunt. Never edit amd/build/ directly. Modules import Moodle core helpers via string IDs (core/ajax, core/str, core/templates).
core/ajax)Skip when: writing pure backend PHP — use moodle-plugin-development.
<plugin>/
amd/
src/
foo.js # ES6 source — edit this
bar.js
build/ # generated; commit but never hand-edit
foo.min.js
foo.min.js.map
Module ID at runtime = <component>/<filename> (no extension):
local/example/amd/src/marker.js → local_example/markermod/quiz/amd/src/preflight.js → mod_quiz/preflight// amd/src/marker.js
import Ajax from 'core/ajax';
import Notification from 'core/notification';
import {get_string as getString} from 'core/str';
import Templates from 'core/templates';
const SELECTORS = {
BUTTON: '[data-action="mark-present"]',
ROW: '[data-region="attendance-row"]',
};
/**
* Initialise: bind click handlers.
*
* @param {number} sessionid
*/
export const init = (sessionid) => {
document.addEventListener('click', async (e) => {
const btn = e.target.closest(SELECTORS.BUTTON);
if (!btn) {
return;
}
e.preventDefault();
const userid = parseInt(btn.dataset.userid, 10);
try {
const result = await markPresent(sessionid, userid);
const html = await Templates.render('local_example/row', result);
const row = btn.closest(SELECTORS.ROW);
Templates.replaceNode(row, html, '');
} catch (err) {
Notification.exception(err);
}
});
};
const markPresent = (sessionid, userid) => {
const request = Ajax.call([{
methodname: 'local_example_mark_present',
args: {sessionid, userid},
}]);
return request[0];
};
$PAGE->requires->js_call_amd('local_example/marker', 'init', [$sessionid]);
The third argument array is JSON-encoded and passed to your init function.
Moodle ships a root Gruntfile.js. From Moodle root:
nvm use # uses .nvmrc — match Moodle's required Node
npm ci
npx grunt amd # build all AMD
npx grunt amd --root=local/example # only your plugin
npx grunt watch # rebuild on change
Build also runs ESLint + minification. amd/build/*.min.js and *.min.js.map are commit-required (Moodle plugin policy).
| Module | Use |
|---|---|
core/ajax | Call web services |
core/str | Load lang strings (getString('foo', 'local_example')) |
core/templates | Render Mustache (render, renderForPromise, replaceNode, appendNodeContents) |
core/notification | Toasts, alerts, exception handling (Notification.exception(err)) |
core/modal (Moodle 4.3+) | Replaces core/modal_factory |
core/modal_factory | (deprecated 4.3+) — use core/modal |
core/toast | Bootstrap-style ephemeral toasts (add(msg, {type})) |
core/pending | Mark async work pending — required for Behat to wait |
core/event | Custom DOM events for cross-module pub/sub |
core/log | Console logging (silenced in production) |
core/fragment | Server-rendered HTML fragments via mod_x_output_fragment_* |
core/url | Build Moodle URLs |
core/config | Access M.cfg (wwwroot, sesskey) safely |
// PHP
$data = ['items' => $items, 'sesskey' => sesskey()];
echo $OUTPUT->render_from_template('local_example/list', $data);
$PAGE->requires->js_call_amd('local_example/list', 'init');
{{! templates/list.mustache }}
<ul data-region="item-list">
{{#items}}
<li data-id="{{id}}">{{name}}</li>
{{/items}}
</ul>
// amd/src/list.js
import {get_strings as getStrings} from 'core/str';
export const init = async () => {
const [confirmTitle, confirmBody] = await getStrings([
{key: 'confirm', component: 'core'},
{key: 'confirmdelete', component: 'local_example'},
]);
// ...
};
Whenever JS does async work, mark it pending so Behat's I wait until the page is ready actually waits:
import Pending from 'core/pending';
const pending = new Pending('local_example/marker:save');
try {
await markPresent(...);
} finally {
pending.resolve();
}
Without Pending, Behat scenarios are flaky.
import Modal from 'core/modal';
import ModalEvents from 'core/modal_events';
const modal = await Modal.create({
title: 'Confirm',
body: Templates.render('local_example/confirm', data),
show: true,
removeOnClose: true,
});
modal.getRoot().on(ModalEvents.save, () => { /* handler */ });
Moodle ships .eslintrc in root. Run:
npx grunt eslint --root=local/example
Rules:
const/let, never var@param, @return)console.log (use core/log)| Mistake | Fix |
|---|---|
Editing amd/build/*.min.js | Edit amd/src/*.js, run npx grunt amd |
Forgetting to commit amd/build/ | Plugin policy — commit both src + build |
Wrong module ID (local_example/amd/marker) | Use local_example/marker (no amd/) |
require(['jquery'], ...) syntax | Use ES6 imports — grunt transpiles to AMD |
No Pending around async | Behat scenarios flake — wrap with core/pending |
| Hard-coded strings | Load via core/str so they translate |
core/modal_factory in 4.3+ | Switch to core/modal |
Not passing args to js_call_amd | Args become init(arg1, arg2) parameters |
| Building with wrong Node version | nvm use from Moodle root respects .nvmrc |
M.cfg.wwwroot directly | Import core/config for safe access |
import Ajax from 'core/ajax';
const requests = Ajax.call([
{methodname: 'local_example_get_items', args: {courseid: 5}},
{methodname: 'local_example_get_users', args: {courseid: 5}},
]);
const [items, users] = await Promise.all(requests);
Methods must be declared 'ajax' => true in db/services.php. The web service token is auto-attached from session (no manual sesskey for AJAX).
Moodle's AMD has limited unit-test infrastructure. Options:
Notification.exception surfaces errors)@javascript — full browser testingnpx grunt jest runs tests/jest/*.test.js if present