Help us improve
Share bugs, ideas, or general feedback.
From moodle-dev
Integrates Moodle plugins with the Ionic/Angular Moodle Mobile app via db/mobile.php remote templates, addons, delegates, push notifications, and offline support.
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-mobile-appThe summary Claude sees in its skill listing — used to decide when to auto-load this skill
Moodle Mobile app (Ionic/Angular) loads plugin UI as **remote templates** declared in `db/mobile.php`. The server returns Mustache-like templates + JS that the app renders inline. No app rebuild required — works on any user's installed app once the site declares the addon.
Guides Moodle plugin development: version.php, DB install/upgrade, capabilities, web services, PSR-4 autoloading, hooks, settings, privacy provider, and coding standards.
Guides creation of custom external web service APIs for Moodle LMS using the external API framework and PHP coding standards. For plugins, REST/AJAX endpoints, quizzes, and mobile backends.
Guides integrating web frameworks like Next.js, React, Vue, Angular, Svelte with Capacitor for mobile apps via static exports. Use when converting web apps to hybrid native mobile.
Share bugs, ideas, or general feedback.
Moodle Mobile app (Ionic/Angular) loads plugin UI as remote templates declared in db/mobile.php. The server returns Mustache-like templates + JS that the app renders inline. No app rebuild required — works on any user's installed app once the site declares the addon.
Skip when: plugin only used in browser admin UI.
Moodle server Moodle Mobile app
───────────── ─────────────────
db/mobile.php ────────────► Discovers addons via
classes/output/mobile.php tool_mobile_get_plugins_supporting_mobile
Returns ────────────► Renders Ionic components
templates (.html)
initial JS
styles
App calls a server function which returns:
<?php
defined('MOODLE_INTERNAL') || die();
$addons = [
'local_example' => [
'handlers' => [
'mainmenu' => [
'displaydata' => [
'title' => 'pluginname',
'icon' => 'list',
'class' => '',
],
'delegate' => 'CoreMainMenuDelegate',
'method' => 'mobile_main_menu_view',
'styles' => [
'url' => '/local/example/mobile/styles.css',
'version' => 1,
],
'offlinefunctions' => [
'mobile_main_menu_view' => [],
],
],
'courseoption' => [
'displaydata' => [
'title' => 'attendance',
'class' => '',
],
'delegate' => 'CoreCourseOptionsDelegate',
'method' => 'mobile_course_view',
],
],
'lang' => [
['pluginname', 'local_example'],
['attendance', 'local_example'],
],
],
];
| Delegate | Where it shows |
|---|---|
CoreMainMenuDelegate | Main menu (bottom tab bar) |
CoreUserDelegate | User profile page |
CoreCourseOptionsDelegate | Course menu |
CoreCourseModuleDelegate | Activity in course (for mod_* plugins) |
CoreBlockDelegate | Sidebar block (for block_*) |
CoreSettingsDelegate | App settings page |
CoreMessageOutputDelegate | Message output handler |
<?php
namespace local_example\output;
defined('MOODLE_INTERNAL') || die();
class mobile {
public static function mobile_main_menu_view(array $args): array {
global $DB, $USER;
$items = $DB->get_records('local_example_items',
['userid' => $USER->id], 'timecreated DESC', '*', 0, 20);
return [
'templates' => [
[
'id' => 'main',
'html' => self::render_main_template(),
],
],
'javascript' => self::get_javascript(),
'otherdata' => [
'items' => json_encode(array_values($items)),
'sesskey' => sesskey(),
],
'files' => [],
];
}
private static function render_main_template(): string {
return '
<ion-list>
<ion-item-divider><ion-label>{{ \'plugin.local_example.attendance\' | translate }}</ion-label></ion-item-divider>
<ion-item *ngFor="let item of CONTENT_OTHERDATA.items">
<ion-label>
<h2>{{ item.name }}</h2>
<p>{{ item.timecreated | coreFormatDate }}</p>
</ion-label>
<ion-button slot="end" (click)="markPresent(item.id)">
{{ \'plugin.local_example.markpresent\' | translate }}
</ion-button>
</ion-item>
</ion-list>';
}
private static function get_javascript(): string {
return "
this.markPresent = (id) => {
const params = {sessionid: id, userid: this.CoreSitesProvider.getCurrentSite().getUserId()};
return this.CoreSitesProvider.getCurrentSite()
.write('local_example_mark_present', params)
.then((result) => {
this.CoreDomUtilsProvider.showToast('plugin.local_example.marked', true, 2000);
});
};";
}
}
The function name in db/mobile.php ('method' => 'mobile_main_menu_view') must be exposed as a web service in db/services.php:
$functions = [
'local_example_mobile_main_menu_view' => [
'classname' => 'local_example\output\mobile',
'methodname' => 'mobile_main_menu_view',
'description' => 'Main menu mobile view',
'type' => 'read',
'capabilities' => '',
'services' => [MOODLE_OFFICIAL_MOBILE_SERVICE],
],
];
Plus the AJAX/REST functions used inside the template (local_example_mark_present).
Mobile templates use Angular + Ionic with Moodle directives:
| Directive | Use |
|---|---|
{{ 'plugin.local_example.foo' | translate }} | Lang string |
{{ value | coreFormatDate }} | Format unix timestamp |
{{ html | coreFormatText }} | Format Moodle text |
<core-format-text [text]="html" /> | Same, component form |
*ngFor="let item of CONTENT_OTHERDATA.items" | Loop |
*ngIf="condition" | Conditional |
(click)="handler()" | Event |
<ion-item>, <ion-list>, <ion-button> | Ionic UI |
<core-empty-box> | "Nothing here" placeholder |
CONTENT_OTHERDATA = data passed via 'otherdata'. Strings JSON-decoded automatically.
Helpers injected into JS scope:
| Object | Use |
|---|---|
this.CoreSitesProvider | Current site, current user, web service calls |
this.CoreDomUtilsProvider | Toasts, alerts, modals |
this.CoreFilepoolProvider | Cache files for offline |
this.CoreUtilsProvider | Misc helpers |
this.refreshContent(false) | Reload current view |
Add functions to 'offlinefunctions':
'offlinefunctions' => [
'mobile_main_menu_view' => [],
'local_example_get_items' => [],
],
App pre-fetches results during sync — they survive offline. For mutations (mark present), use the offline write API:
this.CoreSitesProvider.getCurrentSite().write(
'local_example_mark_present',
params,
{forceOffline: false, getFromCache: false}
).catch((error) => {
if (this.CoreUtilsProvider.isWebServiceError(error)) {
return Promise.reject(error);
}
// Queue for later sync
return this.CoreCourseProvider.storeOfflineAction(...);
});
Push delivery via Moodle's airnotifier service. Plugin events trigger notifications via \core\message\manager::send_message(). Mobile app receives them automatically when:
http://10.0.2.2:8000 (Android emulator) or your LAN IPnpx ionic serve against moodle-mobile-app source pointing at your sitelocal/example/mobile/styles.css (referenced from db/mobile.php):
.local_example-marked {
color: var(--ion-color-success);
font-weight: bold;
}
Bumping 'version' => N in db/mobile.php invalidates app's cached styles.
| Mistake | Fix |
|---|---|
| Method not exposed as web service | Declare in db/services.php with MOODLE_OFFICIAL_MOBILE_SERVICE |
Lang strings not declared in db/mobile.php | App can't render `{{ 'plugin...' |
<div> instead of Ionic <ion-*> | Use Ionic components for native look |
Forgetting MOODLE_OFFICIAL_MOBILE_SERVICE | Web service callable but not for mobile |
| Heavy DB queries on every refresh | Cache via MUC + invalidate on update |
| Hand-formatting timestamps | Use coreFormatDate filter |
| Embedding HTML directly | Use coreFormatText for format_text-equivalent escaping |
No offlinefunctions for read views | Pre-fetch fails — declare them |
Not bumping styles version | App keeps old CSS |
Calling fetch() directly | Use CoreSitesProvider — handles auth + tokens |