Note:

If you want to create a new page for developers, you should create it on the Moodle Developer Resource site.

Moodle App Plugins Development Guide: Difference between revisions

From MoodleDocs
(Update migration status and path)
 
(37 intermediate revisions by 5 users not shown)
Line 1: Line 1:
{{Moodle Mobile}}
{{Template:Migrated|newDocId=/general/app/development/plugins-development-guide}}
{{Moodle Mobile 3.5}}
{{Moodle App (Ionic 5)}}
If you want to add mobile support to your Moodle plugin, you can achieve it by extending different areas of the app using ''just PHP server side code'' and providing templates written with [https://ionicframework.com/docs/components Ionic] and custom components.


==Before 3.5==
You will have to:
# Create a <code>db/mobile.php</code> file in your plugin. In this file, you will be able to indicate which areas of the app you want to extend. For example, adding a new option in the main menu, implementing support for a new activity module, including a new option in the course menu, including a new option in the user profile, etc. All the areas supported are described further in this document.
# Create new functions in a reserved namespace that will return the content of the new options. The content should be returned rendered as html. This html should use Ionic components so that it looks native, but it can be generated using mustache templates.


Since Moodle 3.1 Moodle plugins could be supported in the Mobile app, but only by writing an Angular JS/Ionic module, compiling it to a zip, and including that in your plugin. See [[Moodle_Mobile_2_(Ionic_1)_Remote_add-ons|Remote add-ons]] for details.


In Moodle 3.5 the app switched to a new way to support plugins that was much easier for developers.
Let’s clarify some points:
* This new way will allow developers to support plugins using PHP code, templates and Ionic markup (html components).
* You don’t need to create new Web Service functions (although you will be able to use them for advanced features). You just need plain php functions that will be placed in a reserved namespace.
* The use of JavaScript is optional (but some type of advanced plugins may require it)
* Those functions will be exported via the Web Service function <code>tool_mobile_get_content</code>.
* Developers won’t need to set up a Mobile development environment, they will be able to test using the latest version of the official app (although setting up a local Mobile environment is recommended for complex plugins).
* As arguments of your functions you will always receive the <code>userid</code>, some relevant details of the app (like the app version or the current language in the app), and some specific data depending on the type of plugin (<code>courseid</code>, <code>cmid</code>, ...).
* The mobile app also implements a list of custom Ionic components and directives that provide dynamic behaviour; like indicating that you are linking a file that can be downloaded, allowing a transition to new pages into the app calling a specific function in the server, submitting form data to the server, etc.


This means that remote add-ons won’t be necessary anymore, and developers won’t have to learn Ionic 3 / Angular and set up a new mobile development environment to migrate them. Plugins using the old Remote add-ons mechanism will have to be migrated to the new simpler way (following this documentation)


This new way is natively supported in Moodle 3.5. For previous versions you will need to install the Moodle Mobile Additional Features plugin.
==Getting started==
If you only want to write a plugin, it is not necessary that you set up your environment to work with the Moodle App. In fact, you don't even need to compile it. You can just [[Using the Moodle App in a browser|use a Chromium-based browser]] to add mobile support to your plugins!


==How it works==
You can use the app from one of the hosted versions on [https://master.apps.moodledemo.net master.apps.moodledemo.net] (the latest stable version) and [https://integration.apps.moodledemo.net integration.apps.moodledemo.net] (the latest development version). If you need any specific environment (hosted versions are deployed with a ''production'' environment), you can also use [[Moodle App Docker Images|Docker images]]. And if you need to test your plugin in a native device, you can always use [https://download.moodle.org/mobile Moodle HQ's application].


The overall idea is to allow Moodle plugins to extend different areas in the app with ''just PHP server side'' code and Ionic 3 markup (custom html elements that are called components) using a set of custom Ionic directives and components.
This should suffice for developing plugins. However, if you are working on advanced functionality and you need to run the application from the source code, you can find more information in the [[Moodle App Development Guide]].
===Development workflow===
Before getting into the specifics of your plugin, we recommend that you start adding a simple "Hello World" button in the app to see that everything works properly.


Developers will have to:
Let's say your plugin is called <code>local_hello</code>, you can start by adding the following files:
# Create a db/mobile.php file in their plugins. In this file developers will be able to indicate which areas of the app they want to extend, for example, adding a new option in the main menu, implementing an activity module not supported, including a new option in the course menu, including a new option in the user profile, etc. All the areas supported are described further in this document.
# Create new functions in a reserved namespace that will return the content of the new options. The content should be returned rendered (html). The template should use [https://ionicframework.com/docs/components/ Ionic components] so that it looks native (custom html elements) but it can be generated using mustache templates.


Let’s clarify some points:
<code>db/mobile.php</code>
<syntaxhighlight lang="php">
<?php
 
$addons = [
    'local_hello' => [
        'handlers' => [
            'hello' => [
                'delegate' => 'CoreMainMenuDelegate',
                'method' => 'view_hello',
                'displaydata' => [
                    'title' => 'hello',
                    'icon' => 'earth',
                ],
            ],
        ],
        'lang' => [
            ['hello', 'local_hello'],
        ],
    ],
];
</syntaxhighlight>
<code>classes/output/mobile.php</code>
<syntaxhighlight lang="php">
<?php


* You don’t need to create new Web Service functions (although you will be able to use them for advanced features). You just need plain php functions that will be placed in a reserved namespace.
namespace local_hello\output;
* Those functions will be exported via the Web Service function tool_mobile_get_content
* As arguments of your functions you will always receive the userid, some relevant details of the app (app version, current language in the app, etc…) and some specific data depending on the type of plugin (courseid, cmid, …).
* We provide a list of custom Ionic components and directives (html tags) that will provide dynamic behaviour, like indicating that you are linking a file that can be downloaded, or to allow a transition to new pages into the app calling a specific function in the server, submit form data to the server  etc..


==Make your plugin work in Ionic 5==
defined('MOODLE_INTERNAL') || die();


If you added mobile support to your plugin for the Ionic 3 version of the app (previous to June 2021) you will probably need to make some changes to the plugin to make it look fine in the Ionic 5 version. Please check the following guide for more details on how to do it:
class mobile {


[[Adapt_your_Mobile_plugins_to_Ionic_5|Adapt your Mobile plugins to Ionic 5]]
    public static function view_hello() {
        return [
            'templates' => [
                [
                    'id' => 'main',
                    'html' => '<h1 class="text-center">{{ "plugin.local_hello.hello" | translate }}</h1>',
                ],
            ],
        ];
    }


If you added the mobile support after June 2021 then your plugin was probably tested on Ionic 5 so you probably won't need to do anything.
}
</syntaxhighlight>
<code>lang/en/local_hello.php</code>
<syntaxhighlight lang="php">
<?php


==Types of plugins==
$string['hello'] = 'Hello World';
</syntaxhighlight>
Once you've done that, try logging into your site in the app and you should see a new button in the main menu or more menu (depending on the device) saying "Hello World". If you press this button, you should see a page saying "Hello World!".


We could classify all the plugins in 3 different types:
Congratualtions, you have written your first Moodle plugin with moodle support!


You can read the rest of this page to learn more about mobile plugins and start working on your plugin. Here's some things to keep in mind:
* If you change the <code>mobile.php</code> file, you will have to refresh the browser. And remember to [https://developer.chrome.com/docs/devtools/network/reference/#disable-cache disable the network cache].
* If you change an existing template or function, you won’t have to refresh the browser. In most cases, doing a PTR (Pull To Refresh) in the page that displays the template will suffice.
* If any of these doesn't show your changes, you may need to [https://docs.moodle.org/311/en/Developer_tools#Purge_all_caches purge all caches] to avoid problems with the auto-loading cache.
* Ultimately, if that doesn't work either, you may have to log out from the site and log in again. If any changes affect plugin installation, you may also need to increase the version in your plugin's <code>version.php</code> file and upgrade it in the site.
==Types of plugins==
There are 3 types of plugins:
===Templates generated and downloaded when the user opens the plugins===
===Templates generated and downloaded when the user opens the plugins===
[[File:Templates_downloaded_when_requested.png|thumb]]
[[File:Templates_downloaded_when_requested.png|thumb]]
 
With this type of plugin, the template of your plugin will be generated and downloaded when the user opens the plugin in the app. This means that your function will receive some context parameters. For example, if you're developing a course module plugin you will receive the <code>courseid</code> and the <code>cmid</code> (course module ID). You can see the list of delegates that support this type of plugin in the [[#Delegates|Delegates]] section.
With this type of plugin, the template of your plugin will be generated and downloaded when the user opens your plugin in the app. This means that your function will receive some context params. For example, if you're developing a course module plugin you will receive the courseid and the cmid (course module ID). You can see the list of delegates that support this type of plugin in the [[Mobile_support_for_plugins#Delegates|Delegates]] section.
 
===Templates downloaded on login and rendered using JS data===
===Templates downloaded on login and rendered using JS data===
[[File:Templates_downloaded_on_login.png|thumb]]
[[File:Templates_downloaded_on_login.png|thumb]]
 
With this type of plugin, the template for your plugin will be downloaded when the user logs in into the app and will be stored in the device. This means that your function will not receive any context parameters, and you need to return a generic template that will be built with JS data like the ones in the Moodle App. When the user opens a page that includes your plugin, your template will receive the required JS data and your template will be rendered. You can see the list of delegates that support this type of plugin in the [[#Delegates|Delegates]] section.
With this type of plugin, the template for your plugin will be downloaded when the user logins in the app and will be stored in the device. This means that your function will not receive any context params, and you need to return a generic template that will be built with JS data like the ones in the Mobile app. When the user opens a page that includes your plugin, your template will receive the required JS data and your template will be rendered. You can see the list of delegates that support this type of plugin in the [[Mobile_support_for_plugins#Delegates|Delegates]] section.
===Pure JavaScript plugins===
 
You can always implement the whole plugin yourself using JavaScript instead of using our API. In fact, this is required if you want to implement some features like capturing links in the Mobile app. You can see the list of delegates that only support this type of plugin in the [[#Delegates|Delegates]] section.
===Pure Javascript plugins===
 
You can always implement your whole plugin yourself using Javascript instead of using our API. In fact, this is required if you want to implement some features like capturing links in the Mobile app. You can see the list of delegates that only support this type of plugin in the [[Mobile_support_for_plugins#Delegates|Delegates]] section.
 
==Step by step example==
==Step by step example==
In this example, we are going to update an existing plugin, the [https://github.com/mdjnelson/moodle-mod_certificate Certificate activity module], that previously used a [[Moodle Mobile 2 (Ionic 1) Remote add-ons|Remote add-on]] (a legacy approach to implement mobile plugins).


In this example, we are going to update an existing plugin ([https://github.com/markn86/moodle-mod_certificate Certificate activity module]) that currently uses a Remote add-on.
This is a simple activity module that displays the certificate issued for the current user along with the list of the dates of previously issued certificates. It also stores in the course log that the user viewed a certificate. This module also works offline: when the user downloads the course or activity, the data is pre-fetched and can be viewed offline.
This is a simple activity module that displays the certificate issued for the current user along with the list of the dates of previously issued certificates. It also stores in the course log that the user viewed a certificate. This module also works offline: when the user downloads the course or activity, the data is pre-fetched and can be viewed offline.
 
===Step 1. Update the <code>db/mobile.php</code> file===
The example code can be downloaded from here (https://github.com/markn86/moodle-mod_certificate/commit/003fbac0d80fd96baf428255500980bf95a7a0d6)
In this case, we are updating an existing file. For new plugins, you should create this new file.
 
<syntaxhighlight lang="php">
TIP: Make sure to ([https://docs.moodle.org/35/en/Developer_tools#Purge_all_caches purge all cache]) after making an edit to one of the following files for your changes to be taken into account.
 
===Step 1. Update the db/mobile.php file===
In this case, we are updating an existing file but for new plugins, you should create this new file.
 
<code php>
$addons = [
$addons = [
     'mod_certificate' => [ // Plugin identifier
     'mod_certificate' => [ // Plugin identifier
Line 97: Line 129:
     ],
     ],
];
];
</code>
</syntaxhighlight>
 
;Plugin identifier:
;Plugin identifier:
: A unique name for the plugin, it can be anything (there’s no need to match the module name).
: A unique name for the plugin, it can be anything (there’s no need to match the module name).
 
;Handlers (Different places where the plugin will display content):
;Handlers (Different places where the plugin will display content):
: A plugin can be displayed in different views in the app. Each view should have a unique name inside the plugin scope (alphanumeric).
: A plugin can be displayed in different views in the app. Each view should have a unique name inside the plugin scope (alphanumeric).


; Display data:
; Display data:
: This is only needed for certain types of plugins. Also, depending on the type of delegate it may require additional (or less fields), in this case we are indicating the module icon.
: This is only needed for certain types of plugins. Also, depending on the type of delegate it may require additional (or less fields). In this case, we are indicating the module icon.
 
; Delegate
; Delegate
: Where to display the link to the plugin, see the Delegates chapter in this documentation for all the possible options.
: Where to display the link to the plugin, see the [[#Delegates|Delegates]] section for all the possible options.


; Method:
; Method:
: This is the method in the Moodle \(component)\output\mobile class to be executed the first time the user clicks in the new option displayed in the app.
: This is the method in the Moodle <code>\{component-name}\output\mobile</code> class to be executed the first time the user clicks in the new option displayed in the app.


; Offlinefunctions
; Offlinefunctions
: These are the functions that need to be downloaded for offline usage. This is the list of functions that need to be called and stored when the user downloads a course for offline usage. Please note that you can add functions here that are not even listed in the mobile.php file.  
: This is the list of functions that need to be called and stored when the user downloads a course for offline usage. Please note that you can add functions here that are not even listed in the <code>mobile.php</code> file.  
: In our example, downloading for offline access will mean that we'll execute the functions for getting the certificate and issued certificates passing as parameters the current userid (and courseid when we are using the mod or course delegate). If we have the result of those functions stored in the app, we'll be able to display the certificate information even if the user is offline.
: In our example, downloading for offline access will mean that we'll execute the functions for getting the certificate and issued certificates passing as parameters the current <code>userid</code> (and <code>courseid</code> when we are using the mod or course delegate). If we have the result of those functions stored in the app, we'll be able to display the certificate information even if the user is offline.
: Offline functions will be mostly used to display information for final users, any further interaction with the view won’t be supported offline (for example, trying to send information when the user is offline).
: Offline functions will be mostly used to display information for final users, any further interaction with the view won’t be supported offline (for example, trying to send information when the user is offline).
: You can indicate here other Web Services functions, indicating the parameters that they might need from a defined subset (currently userid and courseid)
: You can indicate here other Web Services functions, indicating the parameters that they might need from a defined subset (currently <code>userid</code> and <code>courseid</code>).
: Prefetching the module will also download all the files returned by the methods in these offline functions (in the ''files'' array).
: Prefetching the module will also download all the files returned by the methods in these offline functions (in the <code>files</code> array).
: Note: If your functions use additional custom parameters (for example, if you implement multiple pages within a module's view function by using a 'page' parameter in addition to the usual cmid, courseid, userid) then the app will not know which additional parameters to supply. In this case, do not list the function in offlinefunctions; instead, you will need to manually implement a [[#Module_prefetch_handler|module prefetch handler]].
: Note that if your functions use additional custom parameters (for example, if you implement multiple pages within a module's view function by using a <code>page</code> parameter in addition to the usual <code>cmid</code>, <code>courseid</code>, and <code>userid</code>) then the app will not know which additional parameters to supply. In this case, do not list the function in <code>offlinefunctions</code>; instead, you will need to manually implement a [[#Module_prefetch_handler|module prefetch handler]].


;Lang:
;Lang:
: <nowiki>The language pack string ids used in the plugin by all the handlers. Normally these will be strings from your own plugin, however, you can list any strings you need here (e.g. ['cancel', 'moodle']). If you do this, be warned that in the app you will then need to refer to that string as {{ 'plugin.myplugin.cancel' | translate }} (not {{ 'plugin.moodle.cancel' | translate }})</nowiki>
: The language pack string ids used in the plugin by all the handlers. Normally these will be strings from your own plugin, however, you can list any strings you need here, like <code>['cancel', 'moodle']</code>. If you do this, be warned that in the app you will then need to refer to that string as <code><nowiki>{{ 'plugin.myplugin.cancel' | translate }}</nowiki></code> (not <code><nowiki>{{ 'plugin.moodle.cancel' | translate }}</nowiki></code>).
: Please only include the strings you actually need. The Web Service that returns the plugin information will include the translation of each string id for every language installed in the platform, and this will then be cached, so listing too many strings is very wasteful.
: Please only include the strings you actually need. The Web Service that returns the plugin information will include the translation of each string id for every language installed in the platform, and this will then be cached, so listing too many strings is very wasteful.
 
There are additional attributes supported by the <code>mobile.php</code> list, you can find about them in the [[#Mobile.php_supported_options|Mobile.php supported options]] section.
There are additional attributes supported by the mobile.php list, see “Mobile.php supported options” section below.
 
===Step 2. Creating the main function===
===Step 2. Creating the main function===
The main function displays the current issued certificate (or several warnings if it’s not possible to issue a certificate). It also displays a link to view the dates of previously issued certificates.
The main function displays the current issued certificate (or several warnings if it’s not possible to issue a certificate). It also displays a link to view the dates of previously issued certificates.


All the functions must be created in the plugin or subsystem classes/output directory, the name of the class must be mobile.
All the functions must be created in the plugin or subsystem <code>classes/output</code> directory, the name of the class must be <code>mobile</code>.


For this example (mod_certificate plugin) the namespace name will be mod_certificate\output.
For this example, the namespace name will be <code>mod_certificate\output</code>.


'''File contents: mod/certificate/classes/output/mobile.php'''
<code>mod/certificate/classes/output/mobile.php</code>
<syntaxhighlight lang="php">
<?php


<code php>
<?php
namespace mod_certificate\output;
namespace mod_certificate\output;


Line 145: Line 173:
use mod_certificate_external;
use mod_certificate_external;


/**
* Mobile output class for certificate
*
* @package    mod_certificate
* @copyright  2018 Juan Leyva
* @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class mobile {
class mobile {


     /**
     /**
     * Returns the certificate course view for the mobile app.
     * Returns the certificate course view for the mobile app.
    * @param  array $args Arguments from tool_mobile_get_content WS
     *
     *
     * @return array      HTML, javascript and otherdata
    * @param  array $args Arguments from tool_mobile_get_content WS.
    *
     * @return array      HTML, JS and other data.
     */
     */
     public static function mobile_course_view($args) {
     public static function mobile_course_view($args) {
Line 167: Line 189:


         // Capabilities check.
         // Capabilities check.
         require_login($args->courseid , false , $cm, true, true);
         require_login($args->courseid, false, $cm, true, true);


         $context = \context_module::instance($cm->id);
         $context = \context_module::instance($cm->id);


         require_capability ('mod/certificate:view', $context);
         require_capability('mod/certificate:view', $context);
         if ($args->userid != $USER->id) {
         if ($args->userid != $USER->id) {
             require_capability('mod/certificate:manage', $context);
             require_capability('mod/certificate:manage', $context);
         }
         }
         $certificate = $DB->get_record('certificate', array('id' => $cm->instance));
         $certificate = $DB->get_record('certificate', ['id' => $cm->instance]);


         // Get certificates from external (taking care of exceptions).
         // Get certificates from external (taking care of exceptions).
Line 183: Line 205:
             $issues = array_values($certificates['issues']); // Make it mustache compatible.
             $issues = array_values($certificates['issues']); // Make it mustache compatible.
         } catch (Exception $e) {
         } catch (Exception $e) {
             $issues = array();
             $issues = [];
         }
         }


Line 189: Line 211:
         foreach ($issues as $issue) {
         foreach ($issues as $issue) {
             if (empty($issue->timemodified)) {
             if (empty($issue->timemodified)) {
                    $issue->timemodified = $issue->timecreated;
                $issue->timemodified = $issue->timecreated;
             }
             }
         }
         }
Line 196: Line 218:
         if ($certificate->requiredtime && !has_capability('mod/certificate:manage', $context)) {
         if ($certificate->requiredtime && !has_capability('mod/certificate:manage', $context)) {
             if (certificate_get_course_time($certificate->course) < ($certificate->requiredtime * 60)) {
             if (certificate_get_course_time($certificate->course) < ($certificate->requiredtime * 60)) {
                    $showget = false;
                $showget = false;
             }
             }
         }
         }


         $certificate->name = format_string($certificate->name);
         $certificate->name = format_string($certificate->name);
         list($certificate->intro, $certificate->introformat) =
         [$certificate->intro, $certificate->introformat] =
                        external_format_text($certificate->intro, $certificate->introformat, $context->id,'mod_certificate', 'intro');
                external_format_text($certificate->intro, $certificate->introformat, $context->id, 'mod_certificate', 'intro');
         $data = array(
         $data = [
             'certificate' => $certificate,
             'certificate' => $certificate,
             'showget' => $showget && count($issues) > 0,
             'showget' => $showget && count($issues) > 0,
Line 210: Line 232:
             'numissues' => count($issues),
             'numissues' => count($issues),
             'cmid' => $cm->id,
             'cmid' => $cm->id,
             'courseid' => $args->courseid
             'courseid' => $args->courseid,
         );
         ];


         return [
         return [
Line 226: Line 248:
     }
     }
}
}
</code>
</syntaxhighlight>
 
Let’s go through the function code to analyse the different parts.
 
;Function declaration:  
;Function declaration:  
: The function name is the same as the one used in the mobile.php file (method field). There is only one argument $args” which is an array containing all the information sent by the mobile app (the courseid, userid, appid, appversionname, appversioncode, applang, appcustomurlscheme…)
: The function name is the same as the one used in the <code>mobile.php</code> file (<code>method</code> field). There is only one argument, <code>$args</code>, which is an array containing all the information sent by the mobile app (the <code>courseid</code>, <code>userid</code>, <code>appid</code>, <code>appversionname</code>, <code>appversioncode</code>, <code>applang</code>, <code>appcustomurlscheme</code>, ...).


; Function implementation:
; Function implementation:
: In the first part of the function, we check permissions and capabilities (like a view.php script would do normally). Then we retrieve the certificate information that’s necessary to display the template.
: In the first part of the function, we check permissions and capabilities (like a <code>view.php</code> script would do normally). Then we retrieve the certificate information that’s necessary to display the template.
 
Finally, we return:
* The rendered template (notice that we could return more than one template but we usually would only need one). By default the app will always render the first template received, the rest of the templates can be used if the plugin defines some Javascript code.
* JavaScript: Empty, because we don’t need any in this case
* Other data: Empty as well, because we don’t need any additional data to be used by directives or components in the template. This field will be published as an object supporting 2-way-data-bind to the template.
* Files: A list of files that the app should be able to download (for offline usage mostly)


; Function return:
* <code>templates</code> — The rendered template (notice that we could return more than one template, but we usually would only need one). By default the app will always render the first template received, the rest of the templates can be used if the plugin defines some JavaScript code.
* <code>javascript</code> — Empty, because we don’t need any in this case.
* <code>otherdata</code> — Empty as well, because we don’t need any additional data to be used by directives or components in the template. This field will be published as an object supporting 2-way data-binding in the template.
* <code>files</code> — A list of files that the app should be able to download (for offline usage mostly).
===Step 3. Creating the template for the main function===
===Step 3. Creating the template for the main function===
This is the most important part of your plugin because it contains the code that will be rendered on the mobile app.
This is the most important part of your plugin because it contains the code that will be rendered on the mobile app.


In this template we’ll be using Ionic and custom directives and components available in the Mobile app.
In this template we’ll be using Ionic, together with directives and components specific to the Moodle App.
 
All the HTML attributes starting with ion- are ionic components. Most of the time the component name is self-explanatory but you may refer to a detailed guide here: https://ionicframework.com/docs/components/


All the HTML attributes starting with ''core-'' are custom components of the Mobile app.
All the HTML elements starting with <code>ion-</code> are ionic components. Most of the time, the component name is self-explanatory but you may refer to a detailed guide here: https://ionicframework.com/docs/components/


'''File contents: mod/certificate/templates/mobile_view_page.mustache'''
All the HTML elements starting with <code>core-</code> are custom components of the Moodle App.


<code xml>
<code>mod/certificate/templates/mobile_view_page.mustache</code>
<syntaxhighlight lang="html+handlebars">
{{=<% %>=}}
{{=<% %>=}}
<div>
<div>
     <core-course-module-description description="<% certificate.intro %>" component="mod_certificate" componentId="<% cmid %>"></core-course-module-description>
     <core-course-module-description description="<% certificate.intro %>" component="mod_certificate" componentId="<% cmid %>">
    </core-course-module-description>


     <ion-list>
     <ion-list>
Line 266: Line 283:
         <%#issues%>
         <%#issues%>
             <ion-item>
             <ion-item>
                 <button ion-button block color="light" core-site-plugins-new-content title="<% certificate.name %>" component="mod_certificate" method="mobile_issues_view" [args]="{cmid: <% cmid %>, courseid: <% courseid %>}">
                 <ion-label><ion-button expand="block" color="light" core-site-plugins-new-content title="<% certificate.name %>"  
                        component="mod_certificate" method="mobile_issues_view"
                        [args]="{cmid: <% cmid %>, courseid: <% courseid %>}">
                     {{ 'plugin.mod_certificate.viewcertificateviews' | translate: {$a: <% numissues %>} }}
                     {{ 'plugin.mod_certificate.viewcertificateviews' | translate: {$a: <% numissues %>} }}
                 </button>
                 </ion-button></ion-label>
             </ion-item>
             </ion-item>
         <%/issues%>
         <%/issues%>


         <%#showget%>
         <%#showget%>
        <ion-item>
            <ion-item>
            <button ion-button block core-course-download-module-main-file moduleId="<% cmid %>" courseId="<% certificate.course %>" component="mod_certificate" [files]="[{fileurl: '<% issue.fileurl %>', filename: '<% issue.filename %>', timemodified: '<% issue.timemodified %>', mimetype: '<% issue.mimetype %>'}]">
                <ion-label>
                <ion-icon name="cloud-download" item-start></ion-icon>
                    <ion-button expand="block" core-course-download-module-main-file moduleId="<% cmid %>"
                {{ 'plugin.mod_certificate.getcertificate' | translate }}
                        courseId="<% certificate.course %>" component="mod_certificate"
            </button>
                        [files]="[{
        </ion-item>
                            fileurl: '<% issue.fileurl %>',
                            filename: '<% issue.filename %>',
                            timemodified: '<% issue.timemodified %>', mimetype: '<% issue.mimetype %>',
                        }]">
               
                        <ion-icon name="cloud-download" slot="start"></ion-icon>
                            {{ 'plugin.mod_certificate.getcertificate' | translate }}
                    </ion-button>
                </ion-label>
            </ion-item>
         <%/showget%>
         <%/showget%>


         <%^showget%>
         <%^showget%>
        <ion-item>
            <ion-item>
            <p>{{ 'plugin.mod_certificate.requiredtimenotmet' | translate }}</p>
                <ion-label><p>{{ 'plugin.mod_certificate.requiredtimenotmet' | translate }}</p></ion-label>
        </ion-item>
            </ion-item>
         <%/showget%>
         <%/showget%>


         <!-- Call log WS when the template is loaded. -->
         <!-- Call log WS when the template is loaded. -->
         <span core-site-plugins-call-ws-on-load name="mod_certificate_view_certificate" [params]="{certificateid: <% certificate.id %>}" [preSets]="{getFromCache: 0, saveToCache: 0}"></span>
         <span core-site-plugins-call-ws-on-load name="mod_certificate_view_certificate"
                [params]="{certificateid: <% certificate.id %>}" [preSets]="{getFromCache: 0, saveToCache: 0}">
        </span>
     </ion-list>
     </ion-list>
</div>
</div>
</code>
</syntaxhighlight>
In the first line of the template we switch delimiters to avoid conflicting with Ionic delimiters (that are curly brackets like mustache).


In the first line of the template we switch delimiters to avoid conflicting with Ionic delimiters (that are curly brackets like mustache).
Then we display the module description using <code>core-course-module-description</code>, which is a component used to include the course module description.
 
Then we display the module description using <code><core-course-module-description</code> that is a component used to include the course module description.


For displaying the certificate information we create a list of elements, adding a header on top.
For displaying the certificate information we create a list of elements, adding a header on top.
The following line <code>{{ 'plugin.mod_certificate.summaryofattempts' | translate }}</code> indicates that the Mobile app will translate the ''summaryofattempts'' string id (here we could’ve used mustache translation but it is usually better to delegate the strings translations to the app). The string id has this format:


“plugin” + plugin identifier (from mobile.php) +  string id (the string must be indicated in the lang field in mobile.php).  
The following line using the <code>translate</code> filter indicates that the app will translate the <code>summaryofattempts</code> string id (here we could’ve used mustache translation but it is usually better to delegate the strings translations to the app). The string id has the following format:
<syntaxhighlight lang="text">
plugin.{plugin-identifier}.{string-id}
</syntaxhighlight>
Where <code>{plugin-identifier}</code> is taken from <code>mobile.php</code> and <code>{string-id}</code> must be indicated in the <code>lang</code> field in <code>mobile.php</code>.  


Then we display a button to transition to another page if there are certificates issued. The attribute (directive) <code>core-site-plugins-new-content</code> indicates that if the user clicks the button, we need to call the function “mobile_issues_view” in the component “mod_certificate” passing as arguments the cmid and courseid. The content returned by this function will be displayed in a new page (see Step 4 for the code of this new page).
Then, we display a button to transition to another page if there are certificates issued. The attribute (directive) <code>core-site-plugins-new-content</code> indicates that if the user clicks the button, we need to call the <code>mobile_issues_view</code> function in the <code>mod_certificate</code> component; passing as arguments the <code>cmid</code> and <code>courseid</code>. The content returned by this function will be displayed in a new page (read the following section to see the code of this new page).


Just after this button we display another one but this time for downloading an issued certificate. The <code>core-course-download-module-main-file</code> directive indicates that clicking this button is for downloading the whole activity and opening the main file. This means that, when the user clicks this button, the whole certificate activity will be available in offline.
Just after this button, we display another one but this time for downloading an issued certificate. The <code>core-course-download-module-main-file</code> directive indicates that clicking this button is for downloading the whole activity and opening the main file. This means that, when the user clicks this button, the whole certificate activity will be available offline.


Finally, just before the ion-list is closed, we use the <code>core-site-plugins-call-ws-on-load</code> directive to indicate that once the page is loaded, we need to call to a Web Service function in the server, in this case we are calling the ''mod_certificate_view_certificate'' that will log that the user viewed this page.
Finally, just before the <code>ion-list</code> is closed, we use the <code>core-site-plugins-call-ws-on-load</code> directive to indicate that once the page is loaded, we need to call a Web Service function in the server, in this case we are calling the <code>mod_certificate_view_certificate</code> that will log that the user viewed this page.


As you can see, no JavaScript was necessary at all. We used plain HTML elements and attributes that did all the complex dynamic logic (like calling a Web Service) behind the scenes.
As you can see, no JavaScript was necessary at all. We used plain HTML elements and attributes that did all the complex dynamic logic (like calling a Web Service) behind the scenes.
===Step 4. Adding an additional page===
===Step 4. Adding an additional page===
Add the following method to <code>mod/certificate/classes/output/mobile.php</code>:
<syntaxhighlight lang="php">
/**
* Returns the certificate issues view for the mobile app.
* @param  array $args Arguments from tool_mobile_get_content WS.
*
* @return array      HTML, JS and other data.
*/
public static function mobile_issues_view($args) {
    global $OUTPUT, $USER, $DB;


'''Partial file contents: mod/certificate/classes/output/mobile.php'''
    $args = (object) $args;
    $cm = get_coursemodule_from_id('certificate', $args->cmid);


<code php>
     // Capabilities check.
     /**
     require_login($args->courseid, false, $cm, true, true);
    * Returns the certificate issues view for the mobile app.
    * @param  array $args Arguments from tool_mobile_get_content WS
    *
    * @return array      HTML, javascript and otherdata
    */
     public static function mobile_issues_view($args) {
        global $OUTPUT, $USER, $DB;


        $args = (object) $args;
    $context = context_module::instance($cm->id);
        $cm = get_coursemodule_from_id('certificate', $args->cmid);


        // Capabilities check.
    require_capability ('mod/certificate:view', $context);
        require_login($args->courseid , false , $cm, true, true);
    if ($args->userid != $USER->id) {
        require_capability('mod/certificate:manage', $context);
    }
    $certificate = $DB->get_record('certificate', ['id' => $cm->instance]);


         $context = context_module::instance($cm->id);
    // Get certificates from external (taking care of exceptions).
    try {
         $issued = mod_certificate_external::issue_certificate($cm->instance);
        $certificates = mod_certificate_external::get_issued_certificates($cm->instance);
        $issues = array_values($certificates['issues']); // Make it mustache compatible.
    } catch (Exception $e) {
        $issues = [];
    }


        require_capability ('mod/certificate:view', $context);
    $data = ['issues' => $issues];
        if ($args->userid != $USER->id) {
            require_capability('mod/certificate:manage', $context);
        }
        $certificate = $DB->get_record('certificate', array('id' => $cm->instance));


        // Get certificates from external (taking care of exceptions).
    return [
        try {
         'templates' => [
            $issued = mod_certificate_external::issue_certificate($cm->instance);
             [
            $certificates = mod_certificate_external::get_issued_certificates($cm->instance);
                 'id' => 'main',
            $issues = array_values($certificates['issues']); // Make it mustache compatible.
                'html' => $OUTPUT->render_from_template('mod_certificate/mobile_view_issues', $data),
        } catch (Exception $e) {
            $issues = array();
         }
 
        $data = [
            'issues' => $issues
        ];
 
        return [
             'templates' => [
                 [
                    'id' => 'main',
                    'html' => $OUTPUT->render_from_template('mod_certificate/mobile_view_issues', $data),
                ],
             ],
             ],
            'javascript' => '',
        ],
            'otherdata' => '',
        'javascript' => '',
        ];
        'otherdata' => '',
    }
    ];
</code>
}
</syntaxhighlight>
This method for the new page was added just after <code>mobile_course_view</code>, the code is quite similar: checks the capabilities, retrieves the information required for the template, and returns the template rendered.


This function for the new page was added just after the mobile_course_view function, the code is quite similar: Capabilities checks, retrieves the information required for the template and returns the template rendered.
The code of the mustache template is also very simple.


The code of the mustache template is also very simple:
<code>mod/certificate/templates/mobile_view_issues.mustache</code>
 
<syntaxhighlight lang="html+handlebars">
'''File contents: mod/certificate/templates/mobile_view_issues.mustache'''
 
<code xml>
{{=<% %>=}}
{{=<% %>=}}
<div>
<div>
Line 376: Line 401:
         <%#issues%>
         <%#issues%>
             <ion-item>
             <ion-item>
                 <p class="item-heading">{{ <%timecreated%> | coreToLocaleString }}</p>
                 <ion-label>
                <p><%grade%></p>
                    <p class="item-heading">{{ <%timecreated%> | coreToLocaleString }}</p>
                    <p><%grade%></p>
                </ion-label>
             </ion-item>
             </ion-item>
         <%/issues%>
         <%/issues%>
     </ion-list>
     </ion-list>
</div>
</div>
</code>
</syntaxhighlight>
 
As we did in the previous template, in the first line of the template we switch delimiters to avoid conflicting with Ionic delimiters (that are curly brackets like mustache).  
As we did in the previous template, in the first line of the template we switch delimiters to avoid conflicting with Ionic delimiters (that are curly brackets like mustache).  


Here we are creating an ionic list that will display a new item in the list per each issued certificated.
Here we are creating an Ionic list that will display a new item in the list per each issued certificated.
 
For the issued certificated we’ll display the time when it was created (using the app filter ''coreToLocaleString''). We are also displaying the grade displayed in the certificate (if any).


For the issued certificated we’ll display the time when it was created (using the app filter <code>coreToLocaleString</code>). We are also displaying the grade displayed in the certificate (if any).
===Step 5. Plugin webservices, if included===
===Step 5. Plugin webservices, if included===
If your plugin uses its own web services, they will also need to be enabled for mobile access in your <code>db/services.php</code> file.


If your plugin uses its own web services, they will also need to be enabled for mobile access in your db/services.php file.
The following line should be included in each webservice definition:
<syntaxhighlight lang="php">
'services' => [MOODLE_OFFICIAL_MOBILE_SERVICE, 'local_mobile'],
</syntaxhighlight>
<code>mod/certificate/db/services.php</code>
<syntaxhighlight lang="php">
<?php


The following line <code>'services'      => [MOODLE_OFFICIAL_MOBILE_SERVICE, 'local_mobile'],</code> should be included in each webservice definition.
'''File contents: mod/certificate/db/services.php'''
<code php>
$functions = [
$functions = [
     'mod_certificate_get_certificates_by_courses' => [
     'mod_certificate_get_certificates_by_courses' => [
         'classname'    => 'mod_certificate_external',
         'classname'    => 'mod_certificate_external',
Line 410: Line 436:
     ],
     ],
];
];
</code>
</syntaxhighlight>
This extra services definition is the reason why you will need to have the local_mobile plugin installed for Moodle versions 3.4 and lower, so that your Moodle site will have all the additional webservices included to deal with all these mobile access calls. This is explained further in the [https://docs.moodle.org/dev/Mobile_support_for_plugins#Moodle_version_requirements Moodle version requirements section] below.
 
==Getting started==
 
The first and most important thing to know is that you don’t need a local mobile environment, you can just use the Chrome or Chromium browser to add mobile support to your plugins!
 
Open this URL (with Chrome or Chromium browser): https://mobileapp.moodledemo.net/ and you will see a web version of the mobile app completely functional (except for some native features). This URL is updated with the latest integration version of the app.
 
Alternatively, you can use the [https://download.moodle.org/desktop/ Moodle Desktop App] that is based in the master (latest stable) version of the app, it is based on Chromium so you can enable the "Developer Tools" and inspect the HTML, inject javascript, debug, etc...
 
To enable "Developer Tools" in Moodle Desktop (and the Chrome or Chromium browser);
* In MacOS: Cmd + Option + I
* In Windows or Linux: Ctrl + Shift + I
 
Please test that your site works correctly in the web version before starting any development.
 
===Moodle version requirements===
 
If your Moodle version is lower than 3.5 you will need to install the [https://docs.moodle.org/en/Moodle_Mobile_additional_features Moodle Mobile additional features plugin].
 
Please use this development version for now: https://github.com/moodlehq/moodle-local_mobile/commits/MOODLE_31_STABLE (if your Moodle version is 3.2, 3.3 or 3.4) you will have to use the specific branch for your version but applying manually the [https://github.com/moodlehq/moodle-local_mobile/commits/MOODLE_31_STABLE last commit from the 3.1 branch] (the one with number MOBILE-2362).
 
Also, when installing the Moodle Mobile Additional features plugin you must follow the installation instructions so the service is set up properly.
 
Remember to update your plugin documentation to reflect that this plugin is mandatory for Mobile support. We don’t recommend to indicate in your plugin version.php a dependency to local_mobile though.
 
===Development workflow===
 
First of all, we recommend creating a simple ''mobile.php'' for displaying a new main menu option (even if your plugin won’t be in the main menu, just to verify that you are able to extend the app plugins). Then open the webapp (https://mobileapp.moodledemo.net/) or refresh the browser if it was already open. Alternatively, you can use the Moodle Desktop app, it is based on Chromium so you can enable the "Developer Tools" and inspect the HTML, inject javascript, debug, etc...
 
Check that you can correctly  see the new menu option you included.
 
Then, develop the main function of the app returning a “Hello world” or basic code (without using templates) to see that everything works together. After adding the classes/output/mobile.php file it is very important to “Purge all caches” to avoid problems with the auto-loading cache.
 
It is important to remember that:
* Any change in the mobile.php file will require you to refresh the web app page in the browser (remember to disable the cache in the Chrome developer options).
* Any change in an existing template or function won’t require to refresh the browser page. In most cases you should just do a PTR (Pull down To Refresh) in the page that displays the view returned by the function. Be aware that PTR will work only when using the “device” emulation in the browser (see following section).
 
===Testing and debugging===
 
To learn how to debug with the web version of the app, please read the following documents:
* [[Moodle Mobile debugging WS requests]] AND
* [[Moodle Mobile development using Chrome or Chromium]] (please, omit the installation section)
 
For plugins using the Javascript API you may develop making use of the console.log function to add trace messages in your code that will be displayed in the browser console.
 
Within the app, make sure to turn on the option: '''App settings''' / '''General''' / '''Display debug messages'''. This means popup errors from the app will show more information.
 
==Mobile.php supported options==
==Mobile.php supported options==
 
In the previous section, we learned about some of the existing options for handlers configuration. This is the full list of supported options.
In the Step by Step section we learned about some of the existing options for handlers configuration. This is the full list of supported options:
 
===Common options===
===Common options===
 
* <code>delegate</code> (mandatory) Name of the delegate to register the handler in.
* '''delegate''' (mandatory): Name of the delegate to register the handler in.
* <code>method</code> (mandatory) The method to call to retrieve the main page content.
* '''method''' (mandatory): The function to call to retrieve the main page content.
* <code>init</code> (optional) A method to call to retrieve the initialisation JS and the restrictions to apply to the whole handler. It can also return templates that can be used from JavaScript. You can learn more about this in the [[#Initialisation|Initialisation]] section.
* '''init''' (optional): A function to call to retrieve the initialization JS and the "restrict" to apply to the whole handler. It can also return templates that can be used from the Javascript of the init method or the Javascript of the handler’s method.
* <code>restricttocurrentuser</code> (optional) Only used if the delegate has a <code>isEnabledForUser</code> function. If true, the handler will only be shown for the current user. For more info about displaying the plugin only for certain users, please see [[#Display_the_plugin_only_if_certain_conditions_are_met|Display the plugin only if certain conditions are met]].
* '''restricttocurrentuser''' (optional) Only used if the delegate has a isEnabledForUser function. If true, the handler will only be shown for current user. For more info about displaying the plugin only for certain users, please see [[Mobile_support_for_plugins#Display_the_plugin_only_if_certain_conditions_are_met|Display the plugin only if certain conditions are met]].
* <code>restricttoenrolledcourses</code> (optional) Only used if the delegate has a <code>isEnabledForCourse</code> function. If true or not defined, the handler will only be shown for courses the user is enrolled in. For more info about displaying the plugin only for certain courses, please see [[#Display_the_plugin_only_if_certain_conditions_are_met|Display the plugin only if certain conditions are met]].
* '''restricttoenrolledcourses''' (optional): Only used if the delegate has a isEnabledForCourse function. If true or not defined, the handler will only be shown for courses the user is enrolled in. For more info about displaying the plugin only for certain courses, please see [[Mobile_support_for_plugins#Display_the_plugin_only_if_certain_conditions_are_met|Display the plugin only if certain conditions are met]].
* <code>styles</code> (optional) An array with two properties: <code>url</code> and <code>version</code>. The URL should point to a CSS file, either using an absolute URL or a relative URL. This file will be downloaded and applied by the app. It's recommended to include styles that will only affect your plugin templates. The version number is used to determine if the file needs to be downloaded again, you should change the version number everytime you change the CSS file.
* '''styles''' (optional): An array with two properties: ''url'' and ''version''. The URL should point to a CSS file, either using an absolute URL or a relative URL. This file will be downloaded and applied by the app. It's recommended to include styles that will only affect your plugin templates. The version number is used to determine if the file needs to be downloaded again, you should change the version number everytime you change the CSS file.
* <code>moodlecomponent</code> (optional) If your plugin supports a component in the app different than the one defined by your plugin, you can use this property to specify it. For example, you can create a local plugin to support a certain course format, activity, etc. The component of your plugin in Moodle would be <code>local_whatever</code>, but in <code>moodlecomponent</code> you can specify that this handler will implement <code>format_whatever</code> or <code>mod_whatever</code>. This property was introduced in the version 3.6.1 of the app.
* '''moodlecomponent''' (optional): If your plugin supports a component in the app different than the one defined by your plugin, you can use this property to specify it. For example, you can create a local plugin to support a certain course format, activity, etc. The component of your plugin in Moodle would be ''local_whatever'', but in "moodlecomponent" you can specify that this handler will implement ''format_whatever'' or ''mod_whatever''. This property was introduced in the version 3.6.1 of the app.
===Options only for CoreMainMenuDelegate ===
 
* <code>displaydata</code> (mandatory):
** <code>title</code> — A language string identifier that was included in the <code>lang</code> section.
** <code>icon</code> — The name of an ionic icon. [[Moodle_App_Plugins_Development_Guide#Using_.27font.27_icons_with_ion-icon|See icons section]].
** <code>class</code> — A CSS class.
* <code>priority</code> (optional) — Priority of the handler. Higher priority is displayed first. Main Menu plugins are always displayed in the More tab, they cannot be displayed as tabs in the bottom bar.
* <code>ptrenabled</code> (optional) — Whether to enable pull-to-refresh gesture to refresh page content.
===Options only for CoreMainMenuHomeDelegate ===
* <code>displaydata</code> (mandatory):
** <code>title</code> — A language string identifier that was included in the <code>lang</code> section.
** <code>class</code> — A CSS class.
* <code>priority</code> (optional) — Priority of the handler. Higher priority is displayed first.
* <code>ptrenabled</code> (optional) — Whether to enable pull-to-refresh gesture to refresh page content.
===Options only for CoreCourseOptionsDelegate===
===Options only for CoreCourseOptionsDelegate===
 
* <code>displaydata</code> (mandatory):
* '''displaydata''' (mandatory): title, class.
** <code>title</code> — A language string identifier that was included in the <code>lang</code> section.
* '''priority''' (optional): Priority of the handler. Higher priority is displayed first.
** <code>class</code> — A CSS class.
* '''ismenuhandler''': (optional) Supported from the 3.7.1 version of the app. Set it to true if you want your plugin to be displayed in the contextual menu of the course instead of in the top tabs. The contextual menu is displayed when you click in the 3-dots button at the top right of the course.
* <code>priority</code> (optional) Priority of the handler. Higher priority is displayed first.
 
* <code>ismenuhandler</code> (optional) Supported from the 3.7.1 version of the app. Set it to <code>true</code> if you want your plugin to be displayed in the contextual menu of the course instead of in the top tabs. The contextual menu is displayed when you click in the 3-dots button at the top right of the course.
===Options only for CoreMainMenuDelegate===
*<code>ptrenabled</code> (optional) — Whether to enable pull-to-refresh gesture to refresh page content.
 
* '''displaydata''' (mandatory):
** title:  a language string identifier that was included in the 'lang' section
** icon: the name of an ionic icon. Valid strings are found here: https://infinitered.github.io/ionicons-version-3-search/ and search for ion-md. Do not include the 'md-' in the string. (eg 'md-information-circle' should be 'information-circle')
** class
* '''priority''' (optional): Priority of the handler. Higher priority is displayed first. Main Menu plugins are always displayed in the "More" tab, they cannot be displayed as tabs in the bottom bar.
 
===Options only for CoreCourseModuleDelegate===
===Options only for CoreCourseModuleDelegate===
 
* <code>displaydata</code> (mandatory):
* '''displaydata''' (mandatory): icon, class.
** <code>icon</code> — Path to the module icon. After Moodle app 4.0, this icon is only used as a fallback, the app will always try to use the theme icon so themes can override icons in the app.
* '''method''' (optional): The function to call to retrieve the main page content. In this delegate the method is optional. If the method is not set, the module won't be clickable.
** <code>class</code> — A CSS class.
* '''offlinefunctions''': (optional) List of functions to call when prefetching the module. It can be a get_content method or a WS. You can filter the params received by the WS. By default, WS will receive these params: courseid, cmid, userid. Other valid values that will be added if they are present in the list of params: courseids (it will receive a list with the courses the user is enrolled in), component + 'id' (e.g. certificateid).
* <code>method</code> (optional) The function to call to retrieve the main page content. In this delegate the method is optional. If the method is not set, the module won't be clickable.
* '''downloadbutton''': (optional) Whether to display download button in the module. If not defined, the button will be shown if there is any offlinefunction.
* <code>offlinefunctions</code> (optional) List of functions to call when prefetching the module. It can be a <code>get_content</code> method or a WS. You can filter the params received by the WS. By default, WS will receive these params: <code>courseid</code>, <code>cmid</code>, <code>userid</code>. Other valid values that will be added if they are present in the list of params: <code>courseids</code> (it will receive a list with the courses the user is enrolled in), <code>{component}id</code> (For example, <code>certificateid</code>).
* '''isresource''': (optional) Whether the module is a resource or an activity. Only used if there is any offlinefunction. If your module relies on the "contents" field, then it should be true.
* <code>downloadbutton</code> (optional) Whether to display download button in the module. If not defined, the button will be shown if there is any offlinefunction.
* '''updatesnames''': (optional) Only used if there is any offlinefunction. A Regular Expression to check if there's any update in the module. It will be compared to the result of ''core_course_check_updates''.
* <code>isresource</code> (optional) Whether the module is a resource or an activity. Only used if there is any offline function. If your module relies on the <code>contents</code> field, then it should be <code>true</code>.
* '''displayopeninbrowser''': (optional) Supported from the 3.6 version of the app. Whether the module should display the "Open in browser" option in the top-right menu. This can be done in JavaScript too: this.displayOpenInBrowser = false;
* <code>updatesnames</code> (optional) Only used if there is any offline function. A regular expression to check if there's any update in the module. It will be compared to the result of <code>core_course_check_updates</code>.
* '''displaydescription''': (optional) Supported from the 3.6 version of the app. Whether the module should display the "Description" option in the top-right menu. This can be done in JavaScript too: this.displayDescription = false;
* <code>displayopeninbrowser</code> (optional) Whether the module should display the "Open in browser" option in the top-right menu. This can be done in JavaScript too: <code>this.displayOpenInBrowser = false;</code>. Supported from the 3.6 version of the app.
* '''displayrefresh''': (optional) Supported from the 3.6 version of the app. Whether the module should display the "Refresh" option in the top-right menu. This can be done in JavaScript too: this.displayRefresh = false;
* <code>displaydescription</code> (optional) — Whether the module should display the "Description" option in the top-right menu. This can be done in JavaScript too: <code>this.displayDescription = false;</code>. Supported from the 3.6 version of the app.
* '''displayprefetch''': (optional) Supported from the 3.6 version of the app. Whether the module should display the download option in the top-right menu. This can be done in JavaScript too: this.displayPrefetch = false;
* <code>displayrefresh</code> (optional) — Whether the module should display the "Refresh" option in the top-right menu. This can be done in JavaScript too: <code>this.displayRefresh = false;</code>. Supported from the 3.6 version of the app.
* '''displaysize''': (optional) Supported from the 3.6 version of the app. Whether the module should display the downloaded size in the top-right menu. This can be done in JavaScript too: this.displaySize = false;
* <code>displayprefetch</code> (optional) — Whether the module should display the download option in the top-right menu. This can be done in JavaScript too: <code>this.displayPrefetch = false;</code>. Supported from the 3.6 version of the app.
* '''coursepagemethod''': (optional) Supported from the 3.8 version of the app. If set, this method will be called when the course is rendered and the HTML returned will be displayed in the course page for the module. Please notice the HTML returned should not contain directives or components, only default HTML.
* <code>displaysize</code> (optional) — Whether the module should display the downloaded size in the top-right menu. This can be done in JavaScript too: <code>this.displaySize = false;</code>. Supported from the 3.6 version of the app.
 
* <code>supportedfeatures</code> (optional) — It can be used to specify the supported features of the plugin. Currently the app only uses <code>FEATURE_MOD_ARCHETYPE</code> and <code>FEATURE_NO_VIEW_LINK</code>. It should be an array with features as keys (For example, <code>[FEATURE_NO_VIEW_LINK => true</code>). If you need to calculate this dynamically please see [[#Module_plugins:_dynamically_determine_if_a_feature_is_supported|Module plugins: dynamically determine if a feature is supported]]. Supported from the 3.6 version of the app.
* <code>coursepagemethod</code> (optional) — If set, this method will be called when the course is rendered and the HTML returned will be displayed in the course page for the module. Please notice the HTML returned should not contain directives or components, only default HTML. Supported from the 3.8 version of the app.
*<code>ptrenabled</code> (optional) — Whether to enable pull-to-refresh gesture to refresh page content.
===Options only for CoreCourseFormatDelegate===
===Options only for CoreCourseFormatDelegate===
 
* <code>canviewallsections</code> (optional) Whether the course format allows seeing all sections in a single page. Defaults to <code>true</code>.
* '''canviewallsections''': (optional) Whether the course format allows seeing all sections in a single page. Defaults to true.
* <code>displayenabledownload</code> (optional) — Deprecated in the 4.0 app, it's no longer used.
* '''displayenabledownload''': (optional) Whether the option to enable section/module download should be displayed. Defaults to true.
* <code>displaysectionselector</code> (optional) — Deprecated in the 4.0 app, use ''displaycourseindex'' instead.
* '''displaysectionselector''': (optional) Whether the default section selector should be displayed. Defaults to true.
*<code>displaycourseindex</code> (optional) Whether the default course index should be displayed. Defaults to <code>true</code>.
 
===Options only for CoreUserDelegate===
===Options only for CoreUserDelegate===
 
* <code>displaydata</code> (mandatory):
* '''displaydata''' (mandatory): title, icon, class.
** <code>title</code> — A language string identifier that was included in the <code>lang</code> section.
* '''type''': The type of the addon. Values accepted: 'newpage' (default) or  'communication'.  
** <code>icon</code> — The name of an ionic icon. [[Moodle_App_Plugins_Development_Guide#Using_.27font.27_icons_with_ion-icon|See icons section]].
* '''priority''' (optional): Priority of the handler. Higher priority is displayed first.  
** <code>class</code> — A CSS class.
 
* <code>type</code> — The type of the addon. The values accepted are <code>'newpage'</code> (default) and <code>'communication'</code>.
* <code>priority</code> (optional) Priority of the handler. Higher priority is displayed first.
*<code>ptrenabled</code> (optional) — Whether to enable pull-to-refresh gesture to refresh page content.
===Options only for CoreSettingsDelegate===
===Options only for CoreSettingsDelegate===
 
* <code>displaydata</code> (mandatory):
* '''displaydata''' (mandatory): title, icon, class.
** <code>title</code> — A language string identifier that was included in the <code>lang</code> section.
* '''priority''' (optional): Priority of the handler. Higher priority is displayed first.  
** <code>icon</code> — The name of an ionic icon. [[Moodle_App_Plugins_Development_Guide#Using_.27font.27_icons_with_ion-icon|See icons section]].
 
** <code>class</code> — A CSS class.
* <code>priority</code> (optional) Priority of the handler. Higher priority is displayed first.  
*<code>ptrenabled</code> (optional) — Whether to enable pull-to-refresh gesture to refresh page content.
===Options only for AddonMessageOutputDelegate===
===Options only for AddonMessageOutputDelegate===
 
* <code>displaydata</code> (mandatory):
* '''displaydata''' (mandatory): title, icon.
**<code>title</code> — A language string identifier that was included in the <code>lang</code> section.
* '''priority''' (optional): Priority of the handler. Higher priority is displayed first.
** <code>icon</code> — The name of an ionic icon. [[Moodle_App_Plugins_Development_Guide#Using_.27font.27_icons_with_ion-icon|See icons section]].
 
* <code>priority</code> (optional) Priority of the handler. Higher priority is displayed first.
*<code>ptrenabled</code> (optional) — Whether to enable pull-to-refresh gesture to refresh page content.
===Options only for CoreBlockDelegate===
===Options only for CoreBlockDelegate===
 
* <code>displaydata</code> (optional):
* '''displaydata''' (optional): title, class, type. If ''title'' is not supplied, it will default to "plugins.block_blockname.pluginname", where ''blockname'' is the name of the block. If ''class'' is not supplied, it will default to "block_blockname", where ''blockname'' is the name of the block. Possible values of ''type'':
** <code>title</code> — A language string identifier that was included in the <code>lang</code> section. If this is not supplied, it will default to <code>'plugins.block_{block-name}.pluginname'</code>, where <code>{block-name}</code> is the name of the block.
** "title": Your block will only display the block title, and when it's clicked it will open a new page to display the block contents (the template returned by the block's method).
** <code>class</code> — A CSS class. If this is not supplied, it will default to <code>block_{block-name}</code>, where <code>{block-name}</code> is the name of the block.
** "prerendered": Your block will display the content and footer returned by the WebService to get the blocks (e.g. core_block_get_course_blocks), so your block's method will never be called.
** <code>type</code> — Possible values are:
** any other value: Your block will immediately call the method specified in mobile.php and it will use the template to render the block.
*** <code>"title"</code> — Your block will only display the block title, and when it's clicked it will open a new page to display the block contents (the template returned by the block's method).
* '''fallback''': (optional) Supported from the 3.9.0 version of the app. This option allows you to specify a block to use in the app instead of your block. E.g. you can make the app display the "My overview" block instead of your block in the app by setting: 'fallback' => 'myoverview'. The fallback will only be used if you don't specify a ''method'' and the ''type'' is different than ''title'' or ''prerendered''..
*** <code>"prerendered"</code> — Your block will display the content and footer returned by the WebService to get the blocks (for example, <code>core_block_get_course_blocks</code>), so your block's method will never be called.
 
*** Any other value Your block will immediately call the method specified in <code>mobile.php</code> and it will use the template to render the block.
* <code>fallback</code> (optional) This option allows you to specify a block to use in the app instead of your block. For example, you can make the app display the "My overview" block instead of your block in the app by setting <code>'fallback' => 'myoverview'</code>. The fallback will only be used if you don't specify a <code>method</code> and the <code>type</code> is different to <code>'title'</code> or <code>'prerendered'</code>. Supported from the 3.9.0 version of the app.
==Delegates==
==Delegates==
 
Delegates can be classified by type of plugin. For more info about type of plugins, please see the [[#Types_of_plugins|Types of plugins]] section.
The delegates can be classified by type of plugin. For more info about type of plugins, please see the See [[Mobile_support_for_plugins#Types_of_plugins|Types of plugins]] section.
 
===Templates generated and downloaded when the user opens the plugins===
===Templates generated and downloaded when the user opens the plugins===
 
====<code>CoreMainMenuDelegate</code>====
====CoreMainMenuDelegate====
 
You must use this delegate when you want to add new items to the main menu (currently displayed at the bottom of the app).  
You must use this delegate when you want to add new items to the main menu (currently displayed at the bottom of the app).  
 
====<code>CoreMainMenuHomeDelegate</code>====
====CoreCourseOptionsDelegate====
You must use this delegate when you want to add new tabs in the home page (by default the app is displaying the "Dashboard" and "Site home" tabs).
 
====<code>CoreCourseOptionsDelegate</code>====
You must use this delegate when you want to add new options in a course (Participants or Grades are examples of this type of delegate).
You must use this delegate when you want to add new options in a course (Participants or Grades are examples of this type of delegate).
 
====<code>CoreCourseModuleDelegate</code>====
====CoreCourseModuleDelegate====
 
You must use this delegate for supporting activity modules or resources.
You must use this delegate for supporting activity modules or resources.
 
====<code>CoreUserDelegate</code>====
====CoreUserDelegate====
 
You must use this delegate when you want to add additional options in the user profile page in the app.
You must use this delegate when you want to add additional options in the user profile page in the app.
====<code>CoreCourseFormatDelegate</code>====
You must use this delegate for supporting course formats. When you open a course from the course list in the mobile app, it will check if there is a <code>CoreCourseFormatDelegate</code> handler for the format that site uses. If so, it will display the course using that handler. Otherwise, it will use the default app course format.


====CoreCourseFormatDelegate====
You can learn more about this at the [[Creating mobile course formats]] page.
 
====<code>CoreSettingsDelegate</code>====
You must use this delegate for supporting course formats.  When you open a course from the course list in the mobile app, it will check if there is a CoreCourseFormatDelegate handler for the format that site uses.  If so, it will display the course using that handler.  Otherwise, it will use the default app course format.  More information is available on [[Creating mobile course formats]].
 
====CoreSettingsDelegate====
 
You must use this delegate to add a new option in the settings page.
You must use this delegate to add a new option in the settings page.
 
====<code>AddonMessageOutputDelegate</code>====
====AddonMessageOutputDelegate====
 
You must use this delegate to support a message output plugin.
You must use this delegate to support a message output plugin.
 
====<code>CoreBlockDelegate</code>====
====CoreBlockDelegate====
You must use this delegate to support a block. For example, blocks can be displayed in Site Home, Dashboard and the Course page.
 
You must use this delegate to support a block. As of Moodle App 3.7.0, blocks are only displayed in Site Home and Dashboard, but they'll be supported in other places of the app soon (e.g. in the course page).
 
===Templates downloaded on login and rendered using JS data===
===Templates downloaded on login and rendered using JS data===
 
====<code>CoreQuestionDelegate</code>====
====CoreQuestionDelegate====
 
You must use this delegate for supporting question types.
You must use this delegate for supporting question types.
https://docs.moodle.org/dev/Creating_mobile_question_types
====CoreQuestionBehaviourDelegate====


You can learn more about this at the [[Creating mobile question types]] page.
====<code>CoreQuestionBehaviourDelegate</code>====
You must use this delegate for supporting question behaviours.
You must use this delegate for supporting question behaviours.
 
====<code>CoreUserProfileFieldDelegate</code>====
====CoreUserProfileFieldDelegate====
 
You must use this delegate for supporting user profile fields.
You must use this delegate for supporting user profile fields.
 
====<code>AddonModQuizAccessRuleDelegate</code>====
====AddonModQuizAccessRuleDelegate====
 
You must use this delegate to support a quiz access rule.
You must use this delegate to support a quiz access rule.
 
====<code>AddonModAssignSubmissionDelegate</code> and <code>AddonModAssignFeedbackDelegate</code>====
====AddonModAssignSubmissionDelegate and AddonModAssignFeedbackDelegate====
 
You must use these delegates to support assign submission or feedback plugins.
You must use these delegates to support assign submission or feedback plugins.
 
====<code>AddonWorkshopAssessmentStrategyDelegate</code>====
====AddonWorkshopAssessmentStrategyDelegate====
 
You must use this delegate to support a workshop assessment strategy plugin.
You must use this delegate to support a workshop assessment strategy plugin.
 
===Pure JavaScript plugins===
===Pure Javascript plugins===
These delegates require JavaScript to be supported. See [[#Initialisation|Initialisation]] for more information.
 
* <code>CoreContentLinksDelegate</code>
These delegates require JavaScript to be supported. See [[Mobile_support_for_plugins#Initialization|Initialization]] for more information.
* <code>CoreCourseModulePrefetchDelegate</code>
 
* <code>CoreFileUploaderDelegate</code>
* CoreContentLinksDelegate
* <code>CorePluginFileDelegate</code>
* CoreCourseModulePrefetchDelegate
* <code>CoreFilterDelegate</code>
* CoreFileUploaderDelegate
* CorePluginFileDelegate
* CoreFilterDelegate
 
==Available components and directives==
==Available components and directives==
===Difference between components and directives===
A directive is usually represented as an HTML attribute, allows you to extend a piece of HTML with additional information or functionality. Example of directives are: <code>core-auto-focus</code>, <code>*ngIf</code>, and <code>ng-repeat</code>.


===Difference between component and directives===
Components are also directives, but they are usually represented as an HTML tag and they are used to add custom elements to the app. Example of components are <code>ion-list</code>, <code>ion-item</code>, and <code>core-search-box</code>.
 
A component (represented as an HTML tag) is used to add custom elements to the app.
Example of components are: ion-list, ion-item, core-search-box
 
A directive (represented as an HTML attribute) allows you to extend a piece of HTML with additional information or functionality.
Example of directives are: core-auto-focus, *ngIf, ng-repeat
 
The Mobile app uses Angular, Ionic and custom components and directives, for a full reference of:
* Angular directives, please check: https://angular.io/api?type=directive
* Ionic components, please check: https://ionicframework.com/docs/


Components and directives are Angular concepts; you can learn more about them and the components come out of the box with Ionic in the following links:
* [https://angular.io/guide/built-in-directives Angular directives documentation]
* [https://ionicframework.com/docs/components Ionic components]
===Custom core components and directives===
===Custom core components and directives===
These are some useful custom components and directives that are only available in the Moodle App. Please note that this isn’t the full list of custom components and directives, it’s just an extract of the most common ones.


These are some useful custom components and directives (only available in the mobile app). Please note that this isn’t the full list of components and directives of the app, it’s just an extract of the most common ones.
You can find a full list of components and directives in the source code of the app, within [https://github.com/moodlehq/moodleapp/tree/master/src/core/components <code>src/core/components</code>] and [https://github.com/moodlehq/moodleapp/tree/master/src/core/directives <code>src/core/directives</code>].
 
====<code>core-format-text</code>====
For a full list of components, go to https://github.com/moodlehq/moodlemobile2/tree/master/src/components
 
For a full list of directives, go to https://github.com/moodlehq/moodlemobile2/tree/master/src/directives
 
====core-format-text====
 
This directive formats the text and adds some directives needed for the app to work as it should. For example, it treats all links and all the embedded media so they work fine in the app. If some content in your template includes links or embedded media, please use this directive.
This directive formats the text and adds some directives needed for the app to work as it should. For example, it treats all links and all the embedded media so they work fine in the app. If some content in your template includes links or embedded media, please use this directive.


This directive automatically applies core-external-content and core-link to all the links and embedded media.
This directive automatically applies <code>core-external-content</code> and <code>core-link</code> to all the links and embedded media.


Data that can be passed to the directive:
Data that can be passed to the directive:
* <code>text</code> (string) — The text to format.
* <code>siteId</code> (string) — Optional. Site ID to use. If not defined, it will use the id of the current site.
* <code>component</code> (string) — Optional. Component to use when downloading embedded files.
* <code>componentId</code> (string|number) — Optional. ID to use in conjunction with the component.
* <code>adaptImg</code> (boolean) — Optional, defaults to <code>true</code>. Whether to adapt images to screen width.
* <code>clean</code> (boolean) — Optional, defaults to <code>false</code>. Whether all HTML tags should be removed.
* <code>singleLine</code> (boolean) — Optional, defaults to <code>false</code>. Whether new lines should be removed to display all the text in single line. Only if <code>clean</code> is <code>true</code>.
* <code>maxHeight</code> (number) — Optional. Max height in pixels to render the content box. The minimum accepted value is 50. Using this parameter will force <code>display: block</code> to calculate the height better. If you want to avoid this, use <code>class="inline"</code> at the same time to use <code>display: inline-block</code>.


* '''text''' (string): The text to format.
* '''siteId''' (string): Optional. Site ID to use. If not defined, current site.
* '''component''' (string): Optional. Component to use when downloading embedded files.
* '''componentId''' (string|number): Optional. ID to use in conjunction with the component.
* '''adaptImg''' (boolean): Optional. Whether to adapt images to screen width. Defaults to true.
* '''clean''' (boolean): Optional. Whether all the HTML tags should be removed. Defaults to false.
* '''singleLine''' (boolean): Optional. Whether new lines should be removed (all text in single line). Only if clean=true. Defaults to false.
* '''maxHeight''' (number): Optional. Max height in pixels to render the content box. It should be 50 at least to make sense. Using this parameter will force display: block to calculate height better. If you want to avoid this use class="inline" at the same time to use display: inline-block.
* '''fullOnClick''' (boolean): Optional. Whether it should open a new page with the full contents on click. Only if maxHeight is set and the content has been collapsed. Defaults to false.
* '''fullTitle''' (string): Optional. Title to use in full view. Defaults to "Description".


Example usage:
Example usage:
 
<syntaxhighlight lang="html+ng2">
<code php>
<core-format-text text="<% cm.description %>" component="mod_certificate" componentId="<% cm.id %>"></core-format-text>
<core-format-text text="<% cm.description %>" component="mod_certificate" componentId="<% cm.id %>"></core-format-text>
</code>
</syntaxhighlight>
 
====core-link====


====<code>core-link</code>====
Directive to handle a link. It performs several checks, like checking if the link needs to be opened in the app, and opens the link as it should (without overriding the app).
Directive to handle a link. It performs several checks, like checking if the link needs to be opened in the app, and opens the link as it should (without overriding the app).


This directive is automatically applied to all the links and media inside core-format-text.
This directive is automatically applied to all the links and media inside <code>core-format-text</code>.


Data that can be passed to the directive:
Data that can be passed to the directive:
* <code>capture</code> (boolean) — Optional, defaults to <code>false</code>. Whether the link needs to be captured by the app (check if the link can be handled by the app instead of opening it in a browser).
* <code>inApp</code> (boolean) — Optional, defaults to <code>false</code>. Whether to open in an embedded browser within the app or in the system browser.
* <code>autoLogin</code> (string) — Optional, defaults to <code>"check"</code>. If the link should be open with auto-login. Accepts the following values:
** <code>"yes"</code> — Always auto-login.
** <code>"no"</code> — Never auto-login.
** <code>"check"</code> — Auto-login only if it points to the current site.


* '''capture''' (boolean): Optional, default false. Whether the link needs to be captured by the app (check if the link can be handled by the app instead of opening it in a browser).
* '''inApp''' (boolean): Optional, default false. True to open in embedded browser, false to open in system browser.
* '''autoLogin''' (string): Optional, default "check". If the link should be open with auto-login. Accepts the following values:
** "yes" -> Always auto-login.
** "no" -> Never auto-login.
** "check" -> Auto-login only if it points to the current site. Default value.


Example usage:
Example usage:
<code php>
<syntaxhighlight lang="html+ng2">
<a href="<% cm.url %>" core-link>
<a href="<% cm.url %>" core-link>
</code>
</syntaxhighlight>
 
====<code>core-external-content</code>====
====core-external-content====
 
Directive to handle links to files and embedded files. This directive should be used in any link to a file or any embedded file that you want to have available when the app is offline.  
Directive to handle links to files and embedded files. This directive should be used in any link to a file or any embedded file that you want to have available when the app is offline.  


If a file is downloaded, its URL will be replaced by the local file URL.
If a file is downloaded, its URL will be replaced by the local file URL.


This directive is automatically applied to all the links and media inside core-format-text.
This directive is automatically applied to all the links and media inside <code>core-format-text</code>.


Data that can be passed to the directive:
Data that can be passed to the directive:
* <code>siteId</code> (string) — Optional. Site ID to use. If not defined, it will use the id of the current site.
* <code>component</code> (string) — Optional. Component to use when downloading embedded files.
* <code>componentId</code> (string|number) — Optional. ID to use in conjunction with the component.


* '''siteId''' (string): Optional. Site ID to use. If not defined, current site.
* '''component''' (string): Optional. Component to use when downloading embedded files.
* '''componentId''' (string|number): Optional. ID to use in conjunction with the component.


Example usage:
Example usage:
<code php>
<syntaxhighlight lang="html+ng2">
<img src="<% event.iconurl %>" core-external-content component="mod_certificate" componentId="<% event.id %>">
<img src="<% event.iconurl %>" core-external-content component="mod_certificate" componentId="<% event.id %>">
</code>
</syntaxhighlight>
 
====<code>core-user-link</code>====
====core-user-link====
 
Directive to go to user profile on click. When the user clicks the element where this directive is attached, the right user profile will be opened.
Directive to go to user profile on click. When the user clicks the element where this directive is attached, the right user profile will be opened.


Data that can be passed to the directive:
Data that can be passed to the directive:
* <code>userId</code> (number) — User id to open the profile.
* <code>courseId</code> (number) — Optional. Course id to show the user info related to that course.


* '''userId''' (number): User id to open the profile.
* '''courseId''' (number): Optional. Course id to show the user info related to that course.


Example usage:
Example usage:
<code php>
<syntaxhighlight lang="html+ng2">
<a ion-item core-user-link userId="<% userid %>">
<a ion-item core-user-link userId="<% userid %>">
</code>
</syntaxhighlight>
 
====<code>core-file</code>====
====core-file====
Component to handle a remote file. It shows the file name, icon (depending on mime type) and a button to download or refresh it. The user can identify if the file is downloaded or not based on the button.
 
Component to handle a remote file. It shows the file name, icon (depending on mimetype) and a button to download/refresh it. The user can identify if the file is downloaded or not based on the button.


Data that can be passed to the directive:
Data that can be passed to the directive:
* <code>file</code> (object) — The file. Must have a <code>filename</code> property and either <code>fileurl</code> or <code>url</code>.
* <code>component</code> (string) — Optional. Component the file belongs to.
* <code>componentId</code> (string|number) — Optional. ID to use in conjunction with the component.
* <code>canDelete</code> (boolean) — Optional. Whether the file can be deleted.
* <code>alwaysDownload</code> (boolean) — Optional. Whether it should always display the refresh button when the file is downloaded. Use it for files that you cannot determine if they're outdated or not.
* <code>canDownload</code> (boolean) — Optional, defaults to <code>true</code>. Whether file can be downloaded.


* file (object): The file. Must have a property 'filename' and a 'fileurl' or 'url'
* component (string): Optional. Component the file belongs to.
* componentId (string|number): Optional. ID to use in conjunction with the component.
* canDelete (boolean): Optional. Whether file can be deleted.
* alwaysDownload (boolean): Optional. Whether it should always display the refresh button when the file is downloaded. Use it for files that you cannot determine if they're outdated or not.
* canDownload (boolean): Optional. Whether file can be downloaded. Defaults to true.


Example usage:
Example usage:
<code php>
<syntaxhighlight lang="html+ng2">
<core-file [file]="{fileurl: '<% issue.url %>', filename: '<% issue.name %>', timemodified: '<% issue.timemodified %>', filesize: '<% issue.size %>'}" component="mod_certificate" componentId="<% cm.id %>"></core-file>
<core-file
</code>
        [file]="{
            fileurl: '<% issue.url %>',
            filename: '<% issue.name %>',
            timemodified: '<% issue.timemodified %>',
            filesize: '<% issue.size %>'
        }"
        component="mod_certificate"
        componentId="<% cm.id %>">
</core-file>
</syntaxhighlight>
====<code>core-download-file</code>====
Directive to allow downloading and opening a file. When the item with this directive is clicked, the file will be downloaded (if needed) and opened.


====core-download-file====
It is usually recommended to use the <code>core-file</code> component since it also displays the state of the file.
 
Directive to allow downloading and open a file. When the item with this directive is clicked, the file will be downloaded (if needed) and opened.
 
It is usually recommended to use the core-file component since it also displays the state of the file.


Data that can be passed to the directive:
Data that can be passed to the directive:
* <code>core-download-file</code> (object) — The file to download.
* <code>component</code> (string) — Optional. Component to link the file to.
* <code>componentId</code> (string|number) — Optional. Component ID to use in conjunction with the component.


* '''core-download-file''' (object): The file to download.
* '''component''' (string): Optional. Component to link the file to.
* '''componentId''' (string|number): Optional. Component ID to use in conjunction with the component.
Example usage: a button to download a file.
<code php>
<button ion-button [core-download-file]="{fileurl: <% issue.url %>, timemodified: <% issue.timemodified %>, filesize: <% issue.size %>}" component="mod_certificate" componentId="<% cm.id %>">
    {{ 'plugin.mod_certificate.download | translate }}
</button>
</code>
====core-course-download-module-main-file====


Example usage (a button to download a file):
<syntaxhighlight lang="html+ng2">
<ion-button
        [core-download-file]="{
            fileurl: <% issue.url %>,
            timemodified: <% issue.timemodified %>,
            filesize: <% issue.size %>
        }"
        component="mod_certificate"
        componentId="<% cm.id %>">
    {{ 'plugin.mod_certificate.download | translate }}
</ion-button>
</syntaxhighlight>
====<code>core-course-download-module-main-file</code>====
Directive to allow downloading and opening the main file of a module.
Directive to allow downloading and opening the main file of a module.


When the item with this directive is clicked, the whole module will be downloaded (if needed) and its main file opened. This is meant for modules like mod_resource.
When the item with this directive is clicked, the whole module will be downloaded (if needed) and its main file opened. This is meant for modules like <code>mod_resource</code>.


This directive must receive either a module or a moduleId. If no files are provided, it will use module.contents.
This directive must receive either a <code>module</code> or a <code>moduleId</code>. If no files are provided, it will use <code>module.contents</code>.


Data that can be passed to the directive:
Data that can be passed to the directive:
* <code>module</code> (object) — Optional, required if module is not supplied. The module object.
* <code>moduleId</code> (number) — Optional, required if module is not supplied. The module ID.
* <code>courseId</code> (number) — The course ID the module belongs to.
* <code>component</code> (string) — Optional. Component to link the file to.
* <code>componentId</code> (string|number) — Optional, defaults to the same value as <code>moduleId</code>. Component ID to use in conjunction with the component.
* <code>files</code> (object[]) — Optional. List of files of the module. If not provided, uses <code>module.contents</code>.


* '''module''' (object): Optional. The module object. Required if module is not supplied.
* '''moduleId''' (number): Optional. The module ID. Required if module is not supplied.
* '''courseId''' (number): The course ID the module belongs to.
* '''component''' (string): Optional. Component to link the file to.
* '''componentId''' (string|number): Optional. Component ID to use in conjunction with the component. If not defined, moduleId.
* '''files''' (object[]): Optional. List of files of the module. If not provided, use module.contents.


Example usage:
Example usage:
<code php>
<syntaxhighlight lang="html+ng2">
<button ion-button block core-course-download-module-main-file moduleId="<% cmid %>" courseId="<% certificate.course %>" component="mod_certificate" [files]="[{fileurl: '<% issue.fileurl %>', filename: '<% issue.filename %>', timemodified: '<% issue.timemodified %>', mimetype: '<% issue.mimetype %>'}]">
<ion-button expand="block" core-course-download-module-main-file moduleId="<% cmid %>"  
        courseId="<% certificate.course %>" component="mod_certificate"
        [files]="[{
            fileurl: '<% issue.fileurl %>',
            filename: '<% issue.filename %>',
            timemodified: '<% issue.timemodified %>',
            mimetype: '<% issue.mimetype %>',
        }]">
     {{ 'plugin.mod_certificate.getcertificate' | translate }}
     {{ 'plugin.mod_certificate.getcertificate' | translate }}
</button>
</ion-button>
</code>
</syntaxhighlight>
 
====<code>core-navbar-buttons</code>====
====core-navbar-buttons====
 
Component to add buttons to the app's header without having to place them inside the header itself. Using this component in a site plugin will allow adding buttons to the header of the current page.
Component to add buttons to the app's header without having to place them inside the header itself. Using this component in a site plugin will allow adding buttons to the header of the current page.


If this component indicates a position (start/end), the buttons will only be added if the header has some buttons in that position. If no start/end is specified, then the buttons will be added to the first <ion-buttons> found in the header.
If this component indicates a position (start/end), the buttons will only be added if the header has some buttons in that position. If no start/end is specified, then the buttons will be added to the first <code><ion-buttons></code> found in the header.


You can use the [hidden] input to hide all the inner buttons if a certain condition is met.
You can use the <code>[hidden]</code> input to hide all the inner buttons if a certain condition is met.


Example usage:
Example usage:
<code php>
<syntaxhighlight lang="html+ng2">
<core-navbar-buttons end>
<core-navbar-buttons end>
     <button ion-button icon-only (click)="action()">
     <ion-button (click)="action()">
         <ion-icon name="funnel"></ion-icon>
         <ion-icon slot="icon-only" name="funnel"></ion-icon>
     </button>
     </ion-button>
</core-navbar-buttons>
</core-navbar-buttons>
</code>
</syntaxhighlight>
 
You can also use this to add options to the context menu, for example:
You can also use this to add options to the context menu. Example usage:
<syntaxhighlight lang="html+ng2">
 
<code php>
<core-navbar-buttons>
<core-navbar-buttons>
     <core-context-menu>
     <core-context-menu>
         <core-context-menu-item [priority]="500" [content]="'Nice boat'" (action)="boatFunction()" [iconAction]="'boat'"></core-context-menu-item>
         <core-context-menu-item
                [priority]="500" content="Nice boat" (action)="boatFunction()"
                iconAction="boat">
        </core-context-menu-item>
     </core-context-menu>
     </core-context-menu>
</core-navbar-buttons>
</core-navbar-buttons>
</code>
</syntaxhighlight>
==== Using 'font' icons with <code>ion-icon</code>====
Font icons are widely used on the app and Moodle LMS website. In order to support [https://fontawesome.com/v5/search?m=free font awesome icons]. We've added a directive that uses prefixes on the <code>name</code> attribute to use different font icons.
* Name prefixed with <code>fas-</code> or <code>fa-</code> will use [https://fontawesome.com/v5/search?m=free&s=solid Font awesome solid] library.
* Name prefixed with <code>far-</code> will use [https://fontawesome.com/v5/search?m=free&s=regular Font awesome regular] library.
* Name prefixed with <code>fab-</code> will use [https://fontawesome.com/v5/search?m=free&s=brands Font awesome brands] library (But only a few are supported and we discourage to use them).
* Name prefixed with <code>moodle-</code> will use some svg icons [https://github.com/moodlehq/moodleapp/tree/master/src/assets/fonts/moodle/moodle imported from Moodle LMS].
* Name prefixed with <code>fam-</code> will use [https://github.com/moodlehq/moodleapp/tree/master/src/assets/fonts/moodle/font-awesome customized font awesome icons].
* If the prefix is not found or not valid, the app will search the icon name on the [https://ionic.io/ionicons ionicons library].


Note that it is not currently possible to remove or modify options from the context menu without using a nasty hack.


Example of usage to show icon "pizza-slice" from font-awesome regular library:
<syntaxhighlight lang="html">
<ion-icon name="fas-pizza-slice"></ion-icon>
</syntaxhighlight>
We encourage the use of font-awesome icons to match the appearance from the LMS website version.
===Specific component and directives for plugins===
===Specific component and directives for plugins===
These are component and directives created specifically for supporting Moodle plugins.
These are component and directives created specifically for supporting Moodle plugins.
 
====<code>core-site-plugins-new-content</code>====
====core-site-plugins-new-content====
 
Directive to display a new content when clicked. This new content can be displayed in a new page or in the current page (only if the current page is already displaying a site plugin content).
Directive to display a new content when clicked. This new content can be displayed in a new page or in the current page (only if the current page is already displaying a site plugin content).


Data that can be passed to the directive:
Data that can be passed to the directive:
* <code>component</code> (string) — The component of the new content.
* <code>method</code> (string) — The method to get the new content.
* <code>args</code> (object) — The params to get the new content.
* <code>preSets</code> (object) — Extra options for the WS call of the new content: whether to use cache or not, etc. This field was added in v3.6.0.
* <code>title</code> (string) — The title to display with the new content. Only if <code>samePage</code> is <code>false</code>.
* <code>samePage</code> (boolean) — Optional, defaults to <code>false</code>. Whether to display the content in same page or open a new one.
* <code>useOtherData</code> (any) — Whether to include <code>otherdata</code> (from the <code>get_content</code> WS call) in the arguments for the new <code>get_content</code> call. If not supplied, no other data will be added. If supplied but empty (<code>null</code>, <code>false</code> or an empty string) all the <code>otherdata</code> will be added. If it’s an array, it will only copy the properties whose names are in the array. Please notice that doing <code>[useOtherData]=""</code> is the same as not supplying it, so nothing will be copied. Also, objects or arrays in <code>otherdata</code> will be converted to a JSON encoded string.
* <code>form</code> (string) — ID or name to identify a form in the template. The form will be obtained from <code>document.forms</code>. If supplied and a form is found, the form data will be retrieved and sent to the new <code>get_content</code> WS call. If your form contains an <code>ion-radio</code>, <code>ion-checkbox</code> or <code>ion-select</code>, please see [[#Values_of_ion-radio.2C_ion-checkbox_or_ion-select_aren.27t_sent_to_my_WS|Values of <code>ion-radio</code>, <code>ion-checkbox</code> or <code>ion-select</code> aren't sent to my WS]].


* '''component''' (string): The component of the new content.
* '''method''' (string): The method to get the new content.
* '''args''' (object): The params to get the new content.
* '''preSets''' (object): Extra options for the WS call of the new content: whether to use cache or not, etc. This field was added in v3.6.0.
* '''title''' (string): The title to display with the new content. Only if samePage=false.
* '''samePage''' (boolean): Whether to display the content in same page or open a new one. Defaults to new page.
* '''useOtherData''' (any): Whether to include ''otherdata'' (from the ''get_content'' WS call) in the args for the new ''get_content'' call. If not supplied, no other data will be added. If supplied but empty ('''null, false or empty string''') all the ''otherdata'' will be added. If it’s an array, it will only copy the properties whose names are in the array. Please notice that [useOtherData]="" is the same as not supplying it, so nothing will be copied. Also, objects or arrays in otherdata will be converted to a JSON encoded string.
* '''form''' (string): ID or name to identify a form in the template. The form will be obtained from ''document.forms''. If supplied and form is found, the form data will be retrieved and sent to the new ''get_content'' WS call. If your form contains an ion-radio, ion-checkbox or ion-select, please see [[Mobile_support_for_plugins#Values_of_ion-radio.2C_ion-checkbox_or_ion-select_aren.27t_sent_to_my_WS|Values of ion-radio, ion-checkbox or ion-select aren't sent to my WS]].


Example usages:
Let's see some examples.


A button to go to a new content page:
A button to go to a new content page:
<code php>
<syntaxhighlight lang="html+ng2">
<button ion-button core-site-plugins-new-content title="<% certificate.name %>" component="mod_certificate" method="mobile_issues_view" [args]="{cmid: <% cmid %>, courseid: <% courseid %>}">
<ion-button core-site-plugins-new-content  
        title="<% certificate.name %>" component="mod_certificate"  
        method="mobile_issues_view" [args]="{cmid: <% cmid %>, courseid: <% courseid %>}">
     {{ 'plugin.mod_certificate.viewissued' | translate }}
     {{ 'plugin.mod_certificate.viewissued' | translate }}
</button>
</ion-button>
</code>
</syntaxhighlight>
 
A button to load new content in current page using <code>userid</code> from <code>otherdata</code>:
A button to load new content in current page using userid from otherdata:
<syntaxhighlight lang="html+ng2">
<code php>
<ion-button core-site-plugins-new-content
<button ion-button core-site-plugins-new-content component="mod_certificate" method="mobile_issues_view" [args]="{cmid: <% cmid %>, courseid: <% courseid %>}" samePage="true" [useOtherData]="['userid']">
        component="mod_certificate" method="mobile_issues_view"
        [args]="{cmid: <% cmid %>, courseid: <% courseid %>}" samePage="true" [useOtherData]="['userid']">
     {{ 'plugin.mod_certificate.viewissued' | translate }}
     {{ 'plugin.mod_certificate.viewissued' | translate }}
</button>
</ion-button>
</code>
</syntaxhighlight>
 
====<code>core-site-plugins-call-ws</code>====
====core-site-plugins-call-ws====
 
Directive to call a WS when the element is clicked. The action to do when the WS call is successful depends on the provided data: display a message, go back or refresh current view.
Directive to call a WS when the element is clicked. The action to do when the WS call is successful depends on the provided data: display a message, go back or refresh current view.


If you want to load a new content when the WS call is done, please see core-site-plugins-call-ws-new-content.
If you want to load a new content when the WS call is done, please see [[#core-site-plugins-call-ws-new-content|<code>core-site-plugins-call-ws-new-content</code>]].


Data that can be passed to the directive:
Data that can be passed to the directive:
* <code>name</code> (string) — The name of the WS to call.
* <code>params</code> (object) — The params for the WS call.
* <code>preSets</code> (object) — Extra options for the WS call: whether to use cache or not, etc.
* <code>useOtherDataForWS</code> (any) — Whether to include <code>otherdata</code> (from the <code>get_content</code> WS call) in the params for the WS call. If not supplied, no other data will be added. If supplied but empty (<code>null</code>, <code>false</code> or an empty string) all the <code>otherdata</code> will be added. If it’s an array, it will only copy the properties whose names are in the array. Please notice that <code>[useOtherDataForWS]=""</code> is the same as not supplying it, so nothing will be copied. Also, objects or arrays in <code>otherdata</code> will be converted to a JSON encoded string.
* <code>form</code> (string) — ID or name to identify a form in the template. The form will be obtained from <code>document.forms</code>. If supplied and a form is found, the form data will be retrieved and sent to the new <code>get_content</code> WS call. If your form contains an <code>ion-radio</code>, <code>ion-checkbox</code> or <code>ion-select</code>, please see [[#Values_of_ion-radio.2C_ion-checkbox_or_ion-select_aren.27t_sent_to_my_WS|Values of <code>ion-radio</code>, <code>ion-checkbox</code> or <code>ion-select</code> aren't sent to my WS]].
* <code>confirmMessage</code> (string) — Message to confirm the action when theuser clicks the element. If not supplied, no confirmation will be requested. If supplied but empty, "Are you sure?" will be used.
* <code>showError</code> (boolean) — Optional, defaults to <code>true</code>. Whether to show an error message if the WS call fails. This field was added in 3.5.2.
* <code>successMessage</code> (string) — Message to show on success. If not supplied, no message. If supplied but empty, defaults to "Success".
* <code>goBackOnSuccess</code> (boolean) — Whether to go back if the WS call is successful.
* <code>refreshOnSuccess</code> (boolean) — Whether to refresh the current view if the WS call is successful.
* <code>onSuccess</code> (Function) — A function to call when the WS call is successful (HTTP call successful and no exception returned). This field was added in 3.5.2.
* <code>onError</code> (Function) — A function to call when the WS call fails (HTTP call fails or an exception is returned). This field was added in 3.5.2.
* <code>onDone</code> (Function) — A function to call when the WS call finishes (either success or fail). This field was added in 3.5.2.


* '''name''' (string): The name of the WS to call.
* '''params''' (object): The params for the WS call.
* '''preSets''' (object): Extra options for the WS call: whether to use cache or not, etc.
* '''useOtherDataForWS''' (any): Whether to include ''otherdata'' (from the ''get_content'' WS call) in the params for the WS call. If not supplied, no other data will be added. If supplied but empty ('''null, false or empty string''') all the ''otherdata'' will be added. If it’s an array, it will only copy the properties whose names are in the array. Please notice that [useOtherDataForWS]="" is the same as not supplying it, so nothing will be copied. Also, objects or arrays in otherdata will be converted to a JSON encoded string.
* '''form''' (string): ID or name to identify a form in the template. The form will be obtained from ''document.forms''. If supplied and form is found, the form data will be retrieved and sent to the WS. If your form contains an ion-radio, ion-checkbox or ion-select, please see [[Mobile_support_for_plugins#Values_of_ion-radio.2C_ion-checkbox_or_ion-select_aren.27t_sent_to_my_WS|Values of ion-radio, ion-checkbox or ion-select aren't sent to my WS]].
* '''confirmMessage''' (string): Message to confirm the action when the user clicks the element. If not supplied, no confirmation. If supplied but empty, default message ("Are you sure?").
* '''showError''' (boolean): Whether to show an error message if the WS call fails. Defaults to true. This field was added in v3.5.2.
* '''successMessage''' (string): Message to show on success. If not supplied, no message. If supplied but empty, default message (“Success”).
* '''goBackOnSuccess''' (boolean): Whether to go back if the WS call is successful.
* '''refreshOnSuccess''' (boolean): Whether to refresh the current view if the WS call is successful.
* '''onSuccess''' (Function): A function to call when the WS call is successful (HTTP call successful and no exception returned). This field was added in v3.5.2.
* '''onError''' (Function): A function to call when the WS call fails (HTTP call fails or an exception is returned). This field was added in v3.5.2.
* '''onDone''' (Function): A function to call when the WS call finishes (either success or fail). This field was added in v3.5.2.


Example usages:
Let's see some examples.


A button to send some data to the server without using cache, displaying default messages and refreshing on success:
A button to send some data to the server without using cache, displaying default messages and refreshing on success:
<code php>
<syntaxhighlight lang="html+ng2">
<button ion-button core-site-plugins-call-ws name="mod_certificate_view_certificate" [params]="{certificateid: <% certificate.id %>}" [preSets]="{getFromCache: 0, saveToCache: 0}" confirmMessage successMessage refreshOnSuccess="true">
<ion-button core-site-plugins-call-ws
        name="mod_certificate_view_certificate" [params]="{certificateid: <% certificate.id %>}"
        [preSets]="{getFromCache: 0, saveToCache: 0}" confirmMessage successMessage
        refreshOnSuccess="true">
     {{ 'plugin.mod_certificate.senddata' | translate }}
     {{ 'plugin.mod_certificate.senddata' | translate }}
</button>
</ion-button>
</code>
</syntaxhighlight>
 
A button to send some data to the server using cache without confirming, going back on success and using <code>userid</code> from <code>otherdata</code>:
A button to send some data to the server using cache without confirming, going back on success and using userid from otherdata:
<syntaxhighlight lang="html+ng2">
<code php>
<ion-button core-site-plugins-call-ws
<button ion-button core-site-plugins-call-ws name="mod_certificate_view_certificate" [params]="{certificateid: <% certificate.id %>}" goBackOnSuccess="true" [useOtherData]="['userid']">
        name="mod_certificate_view_certificate" [params]="{certificateid: <% certificate.id %>}"
        goBackOnSuccess="true" [useOtherData]="['userid']">
     {{ 'plugin.mod_certificate.senddata' | translate }}
     {{ 'plugin.mod_certificate.senddata' | translate }}
</button>
</ion-button>
</code>
</syntaxhighlight>
 
Same as the previous example, but implementing custom JS code to run on success:
Same example as the previous one but implementing a custom JS code to run on success:
<syntaxhighlight lang="html+ng2">
 
<ion-button core-site-plugins-call-ws
<code php>
        name="mod_certificate_view_certificate" [params]="{certificateid: <% certificate.id %>}"
<button ion-button core-site-plugins-call-ws name="mod_certificate_view_certificate" [params]="{certificateid: <% certificate.id %>}" [useOtherData]="['userid']" (onSuccess)="certificateViewed($event)">
        [useOtherData]="['userid']" (onSuccess)="certificateViewed($event)">
     {{ 'plugin.mod_certificate.senddata' | translate }}
     {{ 'plugin.mod_certificate.senddata' | translate }}
</button>
</ion-button>
</code>
</syntaxhighlight>
<code javascript>
In the JavaScript side, you would do:
<syntaxhighlight lang="javascript">
this.certificateViewed = function(result) {
this.certificateViewed = function(result) {
     // Code to run when the WS call is successful.
     // Code to run when the WS call is successful.
};
};
</code>
</syntaxhighlight>
====<code>core-site-plugins-call-ws-new-content</code>====
Directive to call a WS when the element is clicked and load a new content passing the WS result as arguments. This new content can be displayed in a new page or in the same page (only if current page is already displaying a site plugin content).


====core-site-plugins-call-ws-new-content====
If you don't need to load some new content when done, please see [[#core-site-plugins-call-ws|<code>core-site-plugins-call-ws</code>]].
 
Directive to call a WS when the element is clicked and load a new content passing the WS result as args. This new content can be displayed in a new page or in the same page (only if current page is already displaying a site plugin content).
 
If you don't need to load some new content when done, please see core-site-plugins-call-ws.


Data that can be passed to the directive:
Data that can be passed to the directive:
* <code>name</code> (string) — The name of the WS to call.
* <code>params</code> (object) — The parameters for the WS call.
* <code>preSets</code> (object) — Extra options for the WS call: whether to use cache or not, etc.
* <code>useOtherDataForWS</code> (any) — Whether to include <code>otherdata</code> (from the <code>get_content</code> WS call) in the params for the WS call. If not supplied, no other data will be added. If supplied but empty (<code>null</code>, <code>false</code> or an empty string) all the <code>otherdata</code> will be added. If it’s an array, it will only copy the properties whose names are in the array. Please notice that <code>[useOtherDataForWS]=""</code> is the same as not supplying it, so nothing will be copied. Also, objects or arrays in <code>otherdata</code> will be converted to a JSON encoded string.
* <code>form</code> (string) — ID or name to identify a form in the template. The form will be obtained from <code>document.forms</code>. If supplied and a form is found, the form data will be retrieved and sent to the new <code>get_content</code> WS call. If your form contains an <code>ion-radio</code>, <code>ion-checkbox</code> or <code>ion-select</code>, please see [[#Values_of_ion-radio.2C_ion-checkbox_or_ion-select_aren.27t_sent_to_my_WS|Values of <code>ion-radio</code>, <code>ion-checkbox</code> or <code>ion-select</code> aren't sent to my WS]].
* <code>confirmMessage</code> (string) — Message to confirm the action when theuser clicks the element. If not supplied, no confirmation will be requested. If supplied but empty, "Are you sure?" will be used.
* <code>showError</code> (boolean) — Optional, defaults to <code>true</code>. Whether to show an error message if the WS call fails. This field was added in 3.5.2.
* <code>component</code> (string) — The component of the new content.
* <code>method</code> (string) — The method to get the new content.
* <code>args</code> (object) — The parameters to get the new content.
* <code>title</code> (string) — The title to display with the new content. Only if <code>samePage</code> is <code>false</code>.
* <code>samePage</code> (boolean) — Optional, defaults to <code>false</code>. Whether to display the content in the same page or open a new one.
* <code>useOtherData</code> (any) — Whether to include <code>otherdata</code> (from the <code>get_content</code> WS call) in the arguments for the new <code>get_content</code> call. The format is the same as in <code>useOtherDataForWS</code>.
* <code>jsData</code> (any) — JS variables to pass to the new page so they can be used in the template or JS. If <code>true</code> is supplied instead of an object, all initial variables from current page will be copied. This field was added in 3.5.2.
* <code>newContentPreSets</code> (object) — Extra options for the WS call of the new content: whether to use cache or not, etc. This field was added in 3.6.0.
* <code>onSuccess</code> (Function) — A function to call when the WS call is successful (HTTP call successful and no exception returned). This field was added in 3.5.2.
* <code>onError</code> (Function) — A function to call when the WS call fails (HTTP call fails or an exception is returned). This field was added in 3.5.2.
* <code>onDone</code> (Function) — A function to call when the WS call finishes (either success or fail). This field was added in 3.5.2.


* '''name''' (string): The name of the WS to call.
* '''params''' (object): The params for the WS call.
* '''preSets''' (object): Extra options for the WS call: whether to use cache or not, etc.
* '''useOtherDataForWS''' (any): Whether to include ''otherdata'' (from the ''get_content'' WS call) in the params for the WS call. If not supplied, no other data will be added. If supplied but empty ('''null, false or empty string''') all the ''otherdata'' will be added. If it’s an array, it will only copy the properties whose names are in the array. Please notice that [useOtherDataForWS]="" is the same as not supplying it, so nothing will be copied. Also, objects or arrays in otherdata will be converted to a JSON encoded string.
* '''form''' (string): ID or name to identify a form in the template. The form will be obtained from ''document.forms''. If supplied and form is found, the form data will be retrieved and sent to the WS. If your form contains an ion-radio, ion-checkbox or ion-select, please see [[Mobile_support_for_plugins#Values_of_ion-radio.2C_ion-checkbox_or_ion-select_aren.27t_sent_to_my_WS|Values of ion-radio, ion-checkbox or ion-select aren't sent to my WS]].
* '''confirmMessage''' (string): Message to confirm the action when the user clicks the element. If not supplied, no confirmation. If supplied but empty, default message ("Are you sure?").
* '''showError''' (boolean): Whether to show an error message if the WS call fails. Defaults to true. This field was added in v3.5.2.
* '''component''' (string): The component of the new content.
* '''method''' (string): The method to get the new content.
* '''args''' (object): The params to get the new content.
* '''title''' (string): The title to display with the new content. Only if samePage=false.
* '''samePage''' (boolean): Whether to display the content in same page or open a new one. Defaults to new page.
* '''useOtherData''' (any): Whether to include ''otherdata'' (from the ''get_content'' WS call) in the args for the new ''get_content'' call. The format is the same as in ''useOtherDataForWS''.
* '''jsData''' (any): JS variables to pass to the new page so they can be used in the template or JS. If true is supplied instead of an object, all initial variables from current page will be copied. This field was added in v3.5.2.
* '''newContentPreSets''' (object): Extra options for the WS call of the new content: whether to use cache or not, etc. This field was added in v3.6.0.
* '''onSuccess''' (Function): A function to call when the WS call is successful (HTTP call successful and no exception returned). This field was added in v3.5.2.
* '''onError''' (Function): A function to call when the WS call fails (HTTP call fails or an exception is returned). This field was added in v3.5.2.
* '''onDone''' (Function): A function to call when the WS call finishes (either success or fail). This field was added in v3.5.2.


Example usages:
Let's see some examples.


A button to get some data from the server without using cache, showing default confirm and displaying a new page:
A button to get some data from the server without using cache, showing default confirm and displaying a new page:
<code php>
<syntaxhighlight lang="html+ng2">
<button ion-button core-site-plugins-call-ws-new-content name="mod_certificate_get_issued_certificates" [params]="{certificateid: <% certificate.id %>}" [preSets]="{getFromCache: 0, saveToCache: 0}" confirmMessage title="<% certificate.name %>" component="mod_certificate" method="mobile_issues_view" [args]="{cmid: <% cmid %>, courseid: <% courseid %>}">
<ion-button core-site-plugins-call-ws-new-content
        name="mod_certificate_get_issued_certificates" [params]="{certificateid: <% certificate.id %>}"
        [preSets]="{getFromCache: 0, saveToCache: 0}" confirmMessage
        title="<% certificate.name %>" component="mod_certificate"
        method="mobile_issues_view" [args]="{cmid: <% cmid %>, courseid: <% courseid %>}">
     {{ 'plugin.mod_certificate.getissued' | translate }}
     {{ 'plugin.mod_certificate.getissued' | translate }}
</button>
</ion-button>
</code>
</syntaxhighlight>
 
A button to get some data from the server using cache, without confirm, displaying new content in same page and using <code>userid</code> from <code>otherdata</code>:
A button to get some data from the server using cache, without confirm, displaying new content in same page and using ''userid'' from ''otherdata'':
<syntaxhighlight lang="html+ng2">
<code php>
<ion-button core-site-plugins-call-ws-new-content
<button ion-button core-site-plugins-call-ws-new-content name="mod_certificate_get_issued_certificates" [params]="{certificateid: <% certificate.id %>}" component="mod_certificate" method="mobile_issues_view" [args]="{cmid: <% cmid %>, courseid: <% courseid %>}" samePage="true" [useOtherData]="['userid']">
        name="mod_certificate_get_issued_certificates" [params]="{certificateid: <% certificate.id %>}"
        component="mod_certificate" method="mobile_issues_view"
        [args]="{cmid: <% cmid %>, courseid: <% courseid %>}"
        samePage="true" [useOtherData]="['userid']">
     {{ 'plugin.mod_certificate.getissued' | translate }}
     {{ 'plugin.mod_certificate.getissued' | translate }}
</button>
</ion-button>
</code>
</syntaxhighlight>


Same example as the previous one but implementing a custom JS code to run on success:


<code php>
Same as the previous example, but implementing a custom JS code to run on success:
<button ion-button core-site-plugins-call-ws-new-content name="mod_certificate_get_issued_certificates" [params]="{certificateid: <% certificate.id %>}" component="mod_certificate" method="mobile_issues_view" [args]="{cmid: <% cmid %>, courseid: <% courseid %>}" samePage="true" [useOtherData]="['userid']" (onSuccess)="callDone($event)">
<syntaxhighlight lang="html+ng2">
<ion-button core-site-plugins-call-ws-new-content
        name="mod_certificate_get_issued_certificates" [params]="{certificateid: <% certificate.id %>}"
        component="mod_certificate" method="mobile_issues_view"
        [args]="{cmid: <% cmid %>, courseid: <% courseid %>}"
        samePage="true" [useOtherData]="['userid']" (onSuccess)="callDone($event)">
     {{ 'plugin.mod_certificate.getissued' | translate }}
     {{ 'plugin.mod_certificate.getissued' | translate }}
</button>
</ion-button>
</code>
</syntaxhighlight>
<code javascript>
In the JavaScript side, you would do:
<syntaxhighlight lang="javascript">
this.callDone = function(result) {
this.callDone = function(result) {
     // Code to run when the WS call is successful.
     // Code to run when the WS call is successful.
};
};
</code>
</syntaxhighlight>
 
====<code>core-site-plugins-call-ws-on-load</code>====
====core-site-plugins-call-ws-on-load====
 
Directive to call a WS as soon as the template is loaded. This directive is meant for actions to do in the background, like calling logging Web Services.
Directive to call a WS as soon as the template is loaded. This directive is meant for actions to do in the background, like calling logging Web Services.


If you want to call a WS when the user clicks on a certain element, please see core-site-plugins-call-ws.
If you want to call a WS when the user clicks on a certain element, please see [[#core-site-plugins-call-ws|<code>core-site-plugins-call-ws</code>]].
* <code>name</code> (string) — The name of the WS to call.
* <code>params</code> (object) — The parameters for the WS call.
* <code>preSets</code> (object) — Extra options for the WS call: whether to use cache or not, etc.
* <code>useOtherDataForWS</code> (any) — Whether to include <code>otherdata</code> (from the <code>get_content</code> WS call) in the params for the WS call. If not supplied, no other data will be added. If supplied but empty (<code>null</code>, <code>false</code> or an empty string) all the <code>otherdata</code> will be added. If it’s an array, it will only copy the properties whose names are in the array. Please notice that <code>[useOtherDataForWS]=""</code> is the same as not supplying it, so nothing will be copied. Also, objects or arrays in <code>otherdata</code> will be converted to a JSON encoded string.
* <code>form</code> (string) — ID or name to identify a form in the template. The form will be obtained from <code>document.forms</code>. If supplied and a form is found, the form data will be retrieved and sent to the new <code>get_content</code> WS call. If your form contains an <code>ion-radio</code>, <code>ion-checkbox</code> or <code>ion-select</code>, please see [[#Values_of_ion-radio.2C_ion-checkbox_or_ion-select_aren.27t_sent_to_my_WS|Values of <code>ion-radio</code>, <code>ion-checkbox</code> or <code>ion-select</code> aren't sent to my WS]].
* <code>onSuccess</code> (Function) — A function to call when the WS call is successful (HTTP call successful and no exception returned). This field was added in 3.5.2.
* <code>onError</code> (Function) — A function to call when the WS call fails (HTTP call fails or an exception is returned). This field was added in 3.5.2.
* <code>onDone</code> (Function) — A function to call when the WS call finishes (either success or fail). This field was added in 3.5.2.


Note that this will cause an error to appear on each page load if the user is offline in v3.5.1 and older, the bug was fixed in v3.5.2.
* '''name''' (string): The name of the WS to call.
* '''params''' (object): The params for the WS call.
* '''preSets''' (object): Extra options for the WS call: whether to use cache or not, etc.
* '''useOtherDataForWS''' (any): Whether to include ''otherdata'' (from the ''get_content'' WS call) in the params for the WS call. If not supplied, no other data will be added. If supplied but empty ('''null, false or empty string''') all the ''otherdata'' will be added. If it’s an array, it will only copy the properties whose names are in the array. Please notice that [useOtherDataForWS]="" is the same as not supplying it, so nothing will be copied. Also, objects or arrays in otherdata will be converted to a JSON encoded string.
* '''form''' (string): ID or name to identify a form in the template. The form will be obtained from ''document.forms''. If supplied and form is found, the form data will be retrieved and sent to the WS. If your form contains an ion-radio, ion-checkbox or ion-select, please see [[Mobile_support_for_plugins#Values_of_ion-radio.2C_ion-checkbox_or_ion-select_aren.27t_sent_to_my_WS|Values of ion-radio, ion-checkbox or ion-select aren't sent to my WS]].
* '''onSuccess''' (Function): A function to call when the WS call is successful (HTTP call successful and no exception returned). This field was added in v3.5.2.
* '''onError''' (Function): A function to call when the WS call fails (HTTP call fails or an exception is returned). This field was added in v3.5.2.
* '''onDone''' (Function): A function to call when the WS call finishes (either success or fail). This field was added in v3.5.2.


Example usage:
Example usage:
<code php>
<syntaxhighlight lang="html+ng2">
<span core-site-plugins-call-ws-on-load name="mod_certificate_view_certificate" [params]="{certificateid: <% certificate.id %>}" [preSets]="{getFromCache: 0, saveToCache: 0}" (onSuccess)="callDone($event)"></span>
<span core-site-plugins-call-ws-on-load
</code>
        name="mod_certificate_view_certificate" [params]="{certificateid: <% certificate.id %>}"
<code javascript>
        [preSets]="{getFromCache: 0, saveToCache: 0}" (onSuccess)="callDone($event)">
</span>
</syntaxhighlight>
In the JavaScript side, you would do:
<syntaxhighlight lang="javascript">
this.callDone = function(result) {
this.callDone = function(result) {
     // Code to run when the WS call is successful.
     // Code to run when the WS call is successful.
};
};
</code>
</syntaxhighlight>
 
==Advanced features==
==Advanced features==
===Display the plugin only if certain conditions are met===
===Display the plugin only if certain conditions are met===
You might want to display your plugin in the mobile app only if certain dynamic conditions are met, so the plugin would be displayed only for some users. This can be achieved using the initialisation method (for more info, please see the [[#Initialisation|Initialisation]] section ahead).


You might want to display your plugin in the mobile app only if certain dynamic conditions are met, so the plugin would be displayed only for some users. This can be achieved using the "init" method (for more info, please see the [[Mobile_support_for_plugins#Initialization|Initialization]] section ahead).
All initialisation methods are called as soon as your plugin is retrieved. If you don't want your plugin to be displayed for the current user, then you should return the following in the initialisation method (only for Moodle site 3.8 and onwards):
 
<syntaxhighlight lang="php">
All the init methods are called as soon as your plugin is retrieved. If you don't want your plugin to be displayed for the current user, then you should return this in the init method (only for Moodle 3.8 and onwards).
return ['disabled' => true];
 
</syntaxhighlight>
<code php>
If the Moodle site is older than 3.8, then the initialisation method should return this instead:
return [
<syntaxhighlight lang="php">
    'disabled' => true
return ['javascript' => 'this.HANDLER_DISABLED'];
];</code>
</syntaxhighlight>
 
On the other hand, you might want to display a plugin only for certain courses (<code>CoreCourseOptionsDelegate</code>) or only if the user is viewing certain users' profiles (<code>CoreUserDelegate</code>). This can be achieved with the initialisation method too.
If the Moodle version is older than 3.8, then the init method should return this:
 
<code php>
return [
    'javascript' => 'this.HANDLER_DISABLED'
];</code>
 
On the other hand, you might want to display a plugin only for certain courses (''CoreCourseOptionsDelegate'') or only if the user is viewing certain users' profiles (''CoreUserDelegate''). This can be achieved with the init method too.
 
In the init method you can return a "restrict" property with two fields in it: ''courses'' and ''users''. If you return a list of courses IDs in this restrict property, then your plugin will only be displayed when the user views any of those courses. In the same way, if you return a list of user IDs then your plugin will only be displayed when the user views any of those users' profiles.
 
===Using “otherdata”===


The values returned by the functions in otherdata are added to a variable so they can be used both in Javascript and in templates. The otherdata returned by a init call is added to a variable named INIT_OTHERDATA, while the otherdata returned by a ''get_content'' WS call is added to a variable named CONTENT_OTHERDATA.
In the initialisation method you can return a <code>restrict</code> property with two fields in it: <code>courses</code> and <code>users</code>. If you return a list of courses IDs in this property, then your plugin will only be displayed when the user views any of those courses. In the same way, if you return a list of user IDs then your plugin will only be displayed when the user views any of those users' profiles.
===Using <code>otherdata</code>===
The values returned by the functions in <code>otherdata</code> are added to a variable so they can be used both in JavaScript and in templates. The <code>otherdata</code> returned by an initialisation call is added to a variable named <code>INIT_OTHERDATA</code>, while the <code>otherdata</code> returned by a <code>get_content</code> WS call is added to a variable named <code>CONTENT_OTHERDATA</code>.


The otherdata returned by a init call will be passed to the JS and template of all the get_content calls in that handler. The otherdata returned by a get_content call will only be passed to the JS and template returned by that get_content call.
The <code>otherdata</code> returned by an initialisation call will be passed to the JS and template of all the <code>get_content</code> calls in that handler. The <code>otherdata</code> returned by a <code>get_content</code> call will only be passed to the JS and template returned by that <code>get_content</code> call.


This means that, in your Javascript, you can access and use these data like this:
This means that, in your JavaScript, you can access and use the data like this:
<code php>
<syntaxhighlight lang="javascript">
this.CONTENT_OTHERDATA.myVar
this.CONTENT_OTHERDATA.myVar;
</code>
</syntaxhighlight>
And in the template you could use it like this:
And in the template you could use it like this:
<code php>
<syntaxhighlight lang="html+ng2">
{{ CONTENT_OTHERDATA.myVar }}
{{ CONTENT_OTHERDATA.myVar }}
</code>
</syntaxhighlight>
''myVar'' is the name we put to one of our variables, it can be the name you want. In the example above, this is the otherdata returned by the PHP method:
<code>myVar</code> is the name we put to one of our variables, it can be any name that you want. In the example above, this is the <code>otherdata</code> returned by the PHP method:
 
<syntaxhighlight lang="php">
array('myVar' => 'Initial value')
['myVar' => 'Initial value']
 
</syntaxhighlight>
====Example====
====Example====
In our plugin, we want to display an input text with a certain initial value. When the user clicks a button, we want the value in the input to be sent to a certain Web Service. This can be done using <code>otherdata</code>.


In our plugin we want to display an input text with a certain initial value. When the user clicks a button, we want the value in the input to be sent to a certain WebService. This can be done using otherdata.
We will return the initial value of the input in the <code>otherdata</code> of our PHP method:
 
<syntaxhighlight lang="php">
We will return the initial value of the input in the otherdata of our PHP method:
'otherdata' => ['myVar' => 'My initial value'],
<code php>
</syntaxhighlight>
'otherdata' => array('myVar' => 'My initial value'),
</code>
Then in the template we will use it like this:
Then in the template we will use it like this:
<code php>
<syntaxhighlight lang="html+ng2">
<ion-item text-wrap>
<ion-item text-wrap>
     <ion-label stacked>{{ 'plugin.mod_certificate.textlabel | translate }}</ion-label>
     <ion-label position="stacked">{{ 'plugin.mod_certificate.textlabel | translate }}</ion-label>
     <ion-input type="text" [(ngModel)]="CONTENT_OTHERDATA.myVar"></ion-input>
     <ion-input type="text" [(ngModel)]="CONTENT_OTHERDATA.myVar"></ion-input>
</ion-item>
</ion-item>
<ion-item>
<ion-item>
     <button ion-button block color="light" core-site-plugins-call-ws name="mod_certificate_my_webservice" [useOtherDataForWS]="['myVar']">
     <ion-label><ion-button expand="block" color="light" core-site-plugins-call-ws name="mod_certificate_my_webservice" [useOtherDataForWS]="['myVar']">
         {{ 'plugin.mod_certificate.send | translate }}
         {{ 'plugin.mod_certificate.send | translate }}
     </button>
     </ion-button></ion-label>
</ion-item>
</ion-item>
</code>
</syntaxhighlight>
 
In the example above, we are creating an input text and we use <code>[(ngModel)]</code> to use the value in <code>myVar</code> as the initial value and to store the changes in the same <code>myVar</code> variable. This means that the initial value of the input will be "My initial value", and if the user changes the value of the input these changes will be applied to the <code>myVar</code> variable. This is called 2-way data binding in Angular.
In the example above, we are creating an input text and we use ''[(ngModel)]'' to use the value in ''myVar'' as the initial value and to store the changes in the same ''myVar'' variable. This means that the initial value of the input will be “My initial value”, and if the user changes the value of the input these changes will be applied to the ''myVar'' variable. This is called 2-way data binding in Angular.


Then we add a button to send this data to a WS, and for that we use the directive core-site-plugins-call-ws. We use the ''useOtherDataForWS'' attribute to specify which variable from ''otherdata'' we want to send to our WebService. So if the user enters “A new value” in the input and then clicks the button, it will call the WebService ''mod_certificate_my_webservice'' and will send as a param: myVar -> “A new value”.
Then we add a button to send this data to a WS, and for that we use the <code>core-site-plugins-call-ws</code> directive. We use the <code>useOtherDataForWS</code> attribute to specify which variable from <code>otherdata</code> we want to send to our WebService. So if the user enters "A new value" in the input and then clicks the button, it will call the WebService <code>mod_certificate_my_webservice</code> and will send as a parameter <code>['myVar' => 'A new value']</code>.


We can achieve the same result using the ''params'' attribute of the core-site-plugins-call-ws directive instead of using ''useOtherDataForWS'':
We can also achieve the same result using the <code>params</code> attribute of the <code>core-site-plugins-call-ws</code> directive instead of using <code>useOtherDataForWS</code>:
<code php>
<syntaxhighlight lang="html+ng2">
<button ion-button block color="light" core-site-plugins-call-ws name="mod_certificate_my_webservice" [params]="{myVar: CONTENT_OTHERDATA.myVar}">
<ion-button expand="block" color="light" core-site-plugins-call-ws  
        name="mod_certificate_my_webservice" [params]="{myVar: CONTENT_OTHERDATA.myVar}">
     {{ 'plugin.mod_certificate.send | translate }}
     {{ 'plugin.mod_certificate.send | translate }}
</button>
</ion-button>
</code>
</syntaxhighlight>
The WebService call will be exactly the same with both buttons.
The Web Service call will be exactly the same with both versions.
 
Please notice that this example could be done without using otherdata too, using the “''form''” input of the ''core-site-plugins-call-ws directive''.


Notice that this example could be done without using <code>otherdata</code> too, using the <code>form</code> input of the <code>core-site-plugins-call-ws</code> directive.
===Running JS code after a content template has loaded===
===Running JS code after a content template has loaded===
When you return JavaScript code from a handler function using the <code>javascript</code> array key, this code is executed immediately after the web service call returns, which may be before the returned template has been rendered into the DOM.


When you return JavaScript code from a handler function using the 'javascript' array key, this code is executed immediately after the web service call returns, which may be before the returned template has been rendered into the DOM.
If your code needs to run after the DOM has been updated, you can use <code>setTimeout</code> to call it. For example:
 
<syntaxhighlight lang="php">
If your code needs to run after the DOM has been updated, you can use setTimeout to call it. For example:
 
<code php>
return [
return [
     'template' => [ ... ],
     'template' => [
        // ...
    ],
     'javascript' => 'setTimeout(function() { console.log("DOM is available now"); });',
     'javascript' => 'setTimeout(function() { console.log("DOM is available now"); });',
     'otherdata' => '',
     'otherdata' => '',
     'files' => []
     'files' => [],
];
];
</code>
</syntaxhighlight>
 
Notice that if you wanted to write a lot of code here, you might be better off putting it in a function defined in the response from an initialisation template, so that it does not get loaded again with each page of content.
''Note: If you wanted to write a lot of code here, you might be better off putting it in a function defined in the response from an init template, so that it does not get loaded again with each page of content.''
 
===JS functions visible in the templates===
===JS functions visible in the templates===
 
The app provides some JavaScript functions that can be used from the templates to update, refresh or view content. These are the functions:
The app provides some Javascript functions that can be used from the templates to update, refresh or view content. These are the functions:
* <code>openContent(title: string, args: any, component?: string, method?: string)</code> — Open a new page to display some new content. You need to specify the <code>title</code> of the new page and the <code>args</code> to send to the method. If <code>component</code> and <code>method</code> aren't provided, it will use the same as in the current page.
 
* <code>refreshContent(showSpinner = true)</code> — Refresh the current content. By default, it will display a spinner while refreshing. If you don't want it to be displayed, you should pass <code>false</code> as a parameter.
* '''openContent(title: string, args: any, component?: string, method?: string)''': Open a new page to display some new content. You need to specify the ''title'' of the new page and the ''args'' to send to the method. If ''component'' and ''method'' aren't provided, it will use the same as in the current page.
* <code>updateContent(args: any, component?: string, method?: string)</code> — Refresh the current content using different parameters. You need to specify the <code>args</code> to send to the method. If <code>component</code> and <code>method</code> aren't provided, it will use the same as in the current page.
* '''refreshContent(showSpinner = true)''': Refresh the current content. By default it will display a spinner while refreshing, if you don't want it to be displayed you should pass false as a parameter.
* '''updateContent(args: any, component?: string, method?: string)''': Refresh the current content using different params. You need to specify the ''args'' to send to the method. If ''component'' and ''method'' aren't provided, it will use the same as in the current page.
 
====Examples====
====Examples====
=====Group selector=====
=====Group selector=====
Imagine we have an activity that uses groups and we want to let the user select which group they want to see. A possible solution would be to return all the groups in the same template (hidden), and then show the group user selects. However, we can make it more dynamic and return only the group the user is requesting.


Imagine we have an activity that uses groups and we want to let the user select which group he wants to see. A possible solution would be to return all the groups in the same template (hidden), and then show the group user selects. However, we can make it more dynamic and return only the group the user is requesting.
To do so, we'll use a drop down to select the group. When the user selects a group using this drop down, we'll update the page content to display the new group.
 
To do so, we'll use a drop down to select the group. When the user selects a group using this drop down we'll update the page content to display the new group.
 
The main difficulty in this is to tell the view which group needs to be selected when the view is loaded. There are 2 ways to do it: using plain HTML or using Angular's ''ngModel''.


The main difficulty in this is to tell the view which group needs to be selected when the view is loaded. There are 2 ways to do it: using plain HTML or using Angular's <code>ngModel</code>.
======Using plain HTML======
======Using plain HTML======
We need to add a <code>selected</code> attribute to the option that needs to be selected. To do so, we need to pre-caclulate the selected option in the PHP code:
<syntaxhighlight lang="php">
$groupid = empty($args->group) ? 0 : $args->group; // By default, group 0.
$groups = groups_get_activity_allowed_groups($cm, $user->id);


We need to add a "''selected''" attribute to the option that needs to be selected. To do so, we need to pre-caclulate the selected option in the PHP code:
// Detect which group is selected.
foreach ($groups as $gid=>$group) {
    $group->selected = $gid === $groupid;
}


<code php>
$data = [
        $groupid = empty($args->group) ? 0 : $args->group; // By default, group 0.
    'cmid' => $cm->id,
        $groups = groups_get_activity_allowed_groups($cm, $user->id);
    'courseid' => $args->courseid,
        // Detect which group is selected.
    'groups' => $groups,
        foreach ($groups as $gid=>$group) {
];
            $group->selected = $gid === $groupid;
        }


        $data = array(
return [
            'cmid' => $cm->id,
    'templates' => [
            'courseid' => $args->courseid,
         [
            'groups' => $groups
             'id' => 'main',
        );
            'html' => $OUTPUT->render_from_template('mod_certificate/mobile_view_page', $data),
 
        ],
         return array(
    ],
             'templates' => array(
];
                array(
</syntaxhighlight>
                    'id' => 'main',
In the code above, we're retrieving the groups the user can see and then we're adding a <code>selected</code> boolean to each one to determine which one needs to be selected in the drop down. Finally, we pass the list of groups to the template.
                    'html' => $OUTPUT->render_from_template('mod_certificate/mobile_view_page', $data),
                ),
            ),
        );
</code>
 
In the code above, we're retrieving the groups the user can see and then we're adding a "selected" bool to each one to determine which one needs to be selected in the drop down. Finally, we pass the list of groups to the template.


In the template, we display the drop down like this:
In the template, we display the drop down like this:
 
<syntaxhighlight lang="html+handlebars">
<code php>
<ion-select (ionChange)="updateContent({cmid: <% cmid %>, courseid: <% courseid %>, group: $event})" interface="popover">
<ion-select (ionChange)="updateContent({cmid: <% cmid %>, courseid: <% courseid %>, group: $event})" interface="popover">
     <%#groups%>
     <%#groups%>
Line 1,117: Line 1,072:
     <%/groups%>
     <%/groups%>
</ion-select>
</ion-select>
</code>
</syntaxhighlight>
The <code>ionChange</code> function will be called every time the user selects a different group with the drop down. We're using the <code>updateContent</code> function to update the current view using the new group. <code>$event</code> is an Angular variable that will have the selected value (in our case, the group ID that was just selected). This is enough to make the group selector work.
======Using <code>ngModel</code>======
<code>ngModel</code> is an Angular directive that allows storing the value of a certain input or select in a JavaScript variable, and also the opposite way: tell the input or select which value to set. The main problem is that we cannot initialise a JavaScript variable from the template, so we'll use <code>otherdata</code>.


The ''ionChange'' function will be called everytime the user selects a different group with the drop down. We're using the function ''updateContent'' to update the current view using the new group. ''$event'' is an Angular variable that will have the selected value (in our case, the group ID that was just selected). This is enough to make the group selector work.
In the PHP function we'll return the group that needs to be selected in the <code>otherdata</code> array:
<syntaxhighlight lang="php">
$groupid = empty($args->group) ? 0 : $args->group; // By default, group 0.
$groups = groups_get_activity_allowed_groups($cm, $user->id);


======Using ngModel======
// ...


ngModel is an Angular directive that allows storing the value of a certain input/select in a Javascript variable, and also the opposite way: tell the input/select which value to set. The main problem is that we cannot initialize a Javascript variable from the template (Angular doesn't have ''ng-init'' like in AngularJS), so we'll use "otherdata".
return [
 
    'templates' => [
In the PHP function we'll return the group that needs to be selected in the ''otherdata'' array:
         [
 
             'id' => 'main',
<code php>
            'html' => $OUTPUT->render_from_template('mod_certificate/mobile_view_page', $data),
        $groupid = empty($args->group) ? 0 : $args->group; // By default, group 0.
        ],
         $groups = groups_get_activity_allowed_groups($cm, $user->id);
    ],
 
    'otherdata' => [
        ...
        'group' => $groupid,
 
    ],
        return array(
];
             'templates' => array(
</syntaxhighlight>
                array(
In the example above we don't need to iterate over the groups array like in the plain HTML example. However, now we're returning the group id in the <code>otherdata</code> array. As it's explained in the [[#Using_otherdata|Using <code>otherdata</code>]] section, this <code>otherdata</code> is visible in the templates inside a variable named <code>CONTENT_OTHERDATA</code>. So in the template we'll use this variable like this:
                    'id' => 'main',
<syntaxhighlight lang="html+handlebars">
                    'html' => $OUTPUT->render_from_template('mod_certificate/mobile_view_page', $data),
<ion-select [(ngModel)]="CONTENT_OTHERDATA.group"
                ),
        (ionChange)="updateContent({cmid: <% cmid %>, courseid: <% courseid %>, group: CONTENT_OTHERDATA.group})"
            ),
        interface="popover">
            'otherdata' => array(
                'group' => $groupid
            ),
        );
</code>
 
In the example above we don't need to iterate over the groups array like in the plain HTML example. However, now we're returning the groupid in the "otherdata" array. As it's explained in the [[Mobile_support_for_plugins#Using_.E2.80.9Cotherdata.E2.80.9D|Using "otherdata"]] section, this "otherdata" is visible in the templates inside a variable named ''CONTENT_OTHERDATA''. So in the template we'll use this variable like this:
 
<code php>
<ion-select [(ngModel)]="CONTENT_OTHERDATA.group" (ionChange)="updateContent({cmid: <% cmid %>, courseid: <% courseid %>, group: CONTENT_OTHERDATA.group})" interface="popover">
     <%#groups%>
     <%#groups%>
         <ion-option value="<% id %>"><% name %></ion-option>
         <ion-option value="<% id %>"><% name %></ion-option>
     <%/groups%>
     <%/groups%>
</ion-select>
</ion-select>
</code>
</syntaxhighlight>
 
===Use the rich text editor===
===Use the rich text editor===
The rich text editor included in the app requires a <code>FormControl</code> to work. You can use the <code>FormBuilder</code> library to create this control (or to create a whole <code>FormGroup</code> if you prefer).


The rich text editor included in the app requires a FormControl to work. You can use the library FormBuilder to create this control (or to create a whole FormGroup if you prefer).
With the following JavaScript you'll be able to create a <code>FormControl</code>:
 
<syntaxhighlight lang="javascript">
With the following Javascript you'll be able to create a FormControl:
 
<code javascript>
this.control = this.FormBuilder.control(this.CONTENT_OTHERDATA.rte);
this.control = this.FormBuilder.control(this.CONTENT_OTHERDATA.rte);
</code>
</syntaxhighlight>
 
In the example above we're using a value returned in <code>OTHERDATA</code> as the initial value of the rich text editor, but you can use whatever you want.
In the example above we're using a value returned in OTHERDATA as the initial value of the rich text editor, but you can use whatever you want.


Then you need to pass this control to the rich text editor in your template:
Then you need to pass this control to the rich text editor in your template:
 
<syntaxhighlight lang="html+ng2">
<code>
<ion-item>
<ion-item>
     <core-rich-text-editor item-content [control]="control" placeholder="Enter your text here" name="rte_answer"></core-rich-text-editor>
     <core-rich-text-editor item-content [control]="control" placeholder="Enter your text here" name="rte_answer">
    </core-rich-text-editor>
</ion-item>
</ion-item>
</code>
</syntaxhighlight>
Finally, there are several ways to send the value in the rich text editor to a Web Service to save it. This is one of the simplest options:
<syntaxhighlight lang="html+ng2">
<ion-button expand="block" type="submit" core-site-plugins-call-ws name="my_webservice" [params]="{rte: control.value}" ...
</syntaxhighlight>
As you can see, we're passing the value of the rich text editor as a parameter to our Web Service.
===Initialisation===
All handlers can specify an <code>init</code> method in the <code>mobile.php</code> file. This method is meant to return some JavaScript code that needs to be executed as soon as the plugin is retrieved.


Finally, there are several ways to send the value in the rich text editor to a WebService to save it. This is one of the simplest options:
When the app retrieves all the handlers, the first thing it will do is call the <code>tool_mobile_get_content</code> Web Service with the initialisation method. This WS call will only receive the default arguments.


<code>
The app will immediately execute the JavaScript code returned by this WS call. This JavaScript can be used to manually register your handlers in the delegates you want, without having to rely on the default handlers built based on the <code>mobile.php</code> data.
<button ion-button block type="submit" core-site-plugins-call-ws name="my_webservice" [params]="{rte: control.value}" ....
</code>


As you can see, we're passing the value of the rich text editor as a parameter to our WebService.
The templates returned by this method will be added to a <code>INIT_TEMPLATES</code> variable that will be passed to all the JavaScript code of that handler. This means that the JavaScript returned by the initialisation method or the main method can access any of the templates HTML like this:
<syntaxhighlight lang="javascript">
this.INIT_TEMPLATES['main'];
</syntaxhighlight>
In this case, <code>main</code> is the ID of the template we want to use.


===Initialization===
The same happens with the <code>otherdata</code> returned by the initialisation method, it is added to an <code>INIT_OTHERDATA</code> variable.


All handlers can specify a “''init''” method in the mobile.php file. This method is meant to return some JavaScript code that needs to be executed as soon as the plugin is retrieved.
The <code>restrict</code> field returned by this call will be used to determine if your handler is enabled or not. For example, if your handler is for the delegate <code>CoreCourseOptionsDelegate</code> and you return a list of course ids in <code>restrict.courses</code>, then your handler will only be enabled in the courses you returned. This only applies to the default handlers, if you register your own handler using the JavaScript code then you should check yourself if the handler is enabled.


When the app retrieves all the handlers, the first thing it will do is call the ''tool_mobile_get_content'' WebService with the init method. This WS call will only receive the default args.
Finally, if you return an object in this initialisation JavaScript code, all the properties of that object will be passed to all the JavaScript code of that handler so you can use them when the code is run. For example, if your JavaScript code does something like this:
 
<syntaxhighlight lang="javascript">
The app will immediately execute the JavaScript code returned by this WS call. This JavaScript can be used to manually register your handlers in the delegates you want, without having to rely on the default handlers built based on the mobile.php data.
 
The templates returned by this init method will be added to a INIT_TEMPLATES variable that will be passed to all the Javascript code of that handler. This means that the Javascript returned by the init method or the “main” method can access any of the templates HTML like this:
<code javascript>
this.INIT_TEMPLATES[‘main’];
</code>
In this case, “main” is the ID of the template we want to use.
 
The same happens with the ''otherdata'' returned by this init method, it is added to a INIT_OTHERDATA variable.
 
The ''restrict'' field returned by this init call will be used to determine if your handler is enabled or not. For example, if your handler is for the delegate ''CoreCourseOptionsDelegate'' and you return a list of courseids in restrict->courses, then your handler will only be enabled in the courses you returned. This only applies to the “default” handlers, if you register your own handler using the Javascript code then you should check yourself if the handler is enabled.
 
Finally, if you return an object in this init Javascript code, all the properties of that object will be passed to all the Javascript code of that handler so you can use them when the code is run. For example, if your init Javascript code does something like this:
<code javascript>
var result = {
var result = {
     MyAddonClass: new MyAddonClass()
     MyAddonClass: new MyAddonClass()
};
};
result;
result;
</code>
</syntaxhighlight>
Then, for the rest of Javascript code of your handler (e.g. for the “main” method) you can use this variable like this:
Then, for the rest of JavaScript code of your handler (for example, the main method) you can use this variable like this:
<code javascript>
<syntaxhighlight lang="javascript">
this.MyAddonClass
this.MyAddonClass
</code>
</syntaxhighlight>
 
====Examples====
====Examples====
=====Link handlers=====
A link handler allows you to decide what to do when a link with a certain URL is clicked. This is useful, for example, to open your plugin page when a link to your plugin is clicked.


=====Module link handler=====
After the 4.0 version, the Moodle app automatically creates two link handlers for module plugins, you don't need to create them in your plugin's Javascript code anymore:
* A handler to treat links to ''mod/pluginanme/view.php?id=X''. When this link is clicked, it will open your module in the app.
* A handler to treat links to ''mod/pluginname/index.php?id=X''. When this link is clicked, it will open a page in the app listing all the modules of your type inside a certain course.


A link handler allows you to decide what to do when a link with a certain URL is clicked. This is useful, for example, to open your module when a link to the module is clicked. In this example we’ll create a link handler to detect links to a certificate module using a init JavaScript:
<code javascript>
var that = this;


function AddonModCertificateModuleLinkHandler() {
Link handlers have some advanced features that allow you to change how links behave under different conditions.
    that.CoreContentLinksModuleIndexHandler.call(this, that.CoreCourseHelperProvider, 'mmaModCertificate', 'certificate');
======Patterns======
You can define a Regular Expression pattern to match certain links. This will apply the handler only to links that match the pattern.
<syntaxhighlight lang="javascript">
class AddonModFooLinkHandler extends this.CoreContentLinksHandlerBase {


     this.name = "AddonModCertificateLinkHandler";
     constructor() {
}
        super();


AddonModCertificateModuleLinkHandler.prototype = Object.create(this.CoreContentLinksModuleIndexHandler.prototype);
        this.pattern = RegExp('\/mod\/foo\/specialpage.php');
AddonModCertificateModuleLinkHandler.prototype.constructor = AddonModCertificateModuleLinkHandler;
    }


this.CoreContentLinksDelegate.registerHandler(new AddonModCertificateModuleLinkHandler());
</code>
=====Advanced link handler=====
Link handlers have some advanced features that allow you to change how links behave under different conditions.
======Patterns======
You can define a Regular Expression pattern to match certain links.  This will apply the handler only to links that match the pattern.
<code javascript>
function AddonModFooLinkHandler() {
    ....
    this.pattern = RegExp('\/mod\/foo\/specialpage.php');
    ....
}
}
</code>
</syntaxhighlight>
======Priority======
======Priority======
Multiple link handlers may apply to a given link. You can define the order of precedence by setting the priority - the handler with the highest priority will be used.
Multiple link handlers may apply to a given link. You can define the order of precedence by setting the priority; the handler with the highest priority will be used.
 
All default handlers have a priority of 0, so 1 or higher will override the default.
All default handlers have a priority of 0, so 1 or higher will override the default.
<code javascript>
<syntaxhighlight lang="javascript">
function AddonModFooLinkHandler() {
class AddonModFooLinkHandler extends this.CoreContentLinksHandlerBase {
    ....
 
    this.priority = 1;
    constructor() {
     ....
        super();
 
        this.priority = 1;
     }
 
}
}
</code>
</syntaxhighlight>
======Multiple actions======
======Multiple actions======
Once a link has been matched, the handler's getActions() method determines what the link should do. This method has access to the URL and its parameters.
Once a link has been matched, the handler's <code>getActions()</code> method determines what the link should do. This method has access to the URL and its parameters.
 
Different actions can be returned depending on different conditions.
Different actions can be returned depending on different conditions.
<code javascript>
<syntaxhighlight lang="javascript">
AddonModFooLinkHandler.prototype.getActions = function(siteIds, url, params) {
class AddonModFooLinkHandler extends this.CoreContentLinksHandlerBase {
    return [{
 
        action: function(siteId, navCtrl) {
    getActions(siteIds, url, params) {
            // The actual behaviour of the link goes here.
        return [
        },
            {
        sites: [...]
                action: function(siteId, navCtrl) {
    }, {
                    // The actual behaviour of the link goes here.
        ...
                },
    }];
                sites: [
                    // ...
                ],
            },
            {
                // ...
            },
        ];
    }
 
}
}
</code>
</syntaxhighlight>
Once handlers have been matched for a link, the actions will be fetched for all the matching handlers, in priorty order. The first "valid" action will be used to open the link.
Once handlers have been matched for a link, the actions will be fetched for all the matching handlers, in priorty order. The first valid action will be used to open the link.
If your handler is matched with a link, but a condition assessed in the getActions() function means you want to revert to the next highest priorty handler, you can "invalidate"
 
your action by settings its sites propety to an empty array.
If your handler is matched with a link, but a condition assessed in the <code>getActions()</code> method means you want to revert to the next highest priorty handler, you can invalidate your action by settings its sites propety to an empty array.
======Complex example======
======Complex example======
This will match all URLs containing /mod/foo/, and force those with an id parameter that's not in the "supportedModFoos" array to open in the user's browser, rather than the app.
This will match all URLs containing <code>/mod/foo/</code>, and force those with an id parameter that's not in the <code>supportedModFoos</code> array to open in the user's browser, rather than the app.
<syntaxhighlight lang="javascript">
const that = this;
const supportedModFoos = [...];


<code javascript>
class AddonModFooLinkHandler extends this.CoreContentLinksHandlerBase {
var that = this;
 
var supportedModFoos = [...];
    constructor() {
function AddonModFooLinkHandler() {
        super();
    this.pattern = new RegExp('\/mod\/foo\/');
 
    this.name = "AddonModFooLinkHandler";
        this.pattern = new RegExp('\/mod\/foo\/');
    this.priority = 1;
        this.name = 'AddonModFooLinkHandler';
}
        this.priority = 1;
AddonModFooLinkHandler.prototype = Object.create(that.CoreContentLinksHandlerBase.prototype);
    }
AddonModFooLinkHandler.prototype.constructor = AddonModFooLinkHandler;
 
AddonModFooLinkHandler.prototype.getActions = function(siteIds, url, params) {     
    getActions(siteIds, url, params) {     
    var action = {
        const action = {
        action: function() {
            action() {
            that.CoreUtilsProvider.openInBrowser(url);
                that.CoreUtilsProvider.openInBrowser(url);
            },
        };
 
        if (supportedModFoos.indexOf(parseInt(params.id)) !== -1) {
            action.sites = [];
         }
         }
    };
 
    if (supportedModFoos.indexOf(parseInt(params.id)) !== -1) {
         return [action];
         action.sites = [];
     }
     }
    return [action];
};
that.CoreContentLinksDelegate.registerHandler(new AddonModFooLinkHandler());
</code>


}
this.CoreContentLinksDelegate.registerHandler(new AddonModFooLinkHandler());
</syntaxhighlight>
=====Module prefetch handler=====
=====Module prefetch handler=====
The <code>CoreCourseModuleDelegate</code> handler allows you to define a list of offline functions to prefetch a module. However, you might want to create your own prefetch handler to determine what needs to be downloaded. For example, you might need to chain WS calls (pass the result of a WS call to the next one), and this cannot be done using offline functions.


The ''CoreCourseModuleDelegate'' handler allows you to define a list of ''offlinefunctions'' to prefetch a module. However, you might want to create your own prefetch handler to determine what needs to be downloaded. For example, you might need to chain WS calls (pass the result of a WS call to the next one), and this cannot be done using ''offlinefunctions''.
Here’s an example on how to create a prefetch handler using the initialisation JS:
<syntaxhighlight lang="javascript">
// Create a class that extends from CoreCourseActivityPrefetchHandlerBase.
class AddonModCertificateModulePrefetchHandler extends CoreCourseActivityPrefetchHandlerBase {


Here’s an example on how to create a prefetch handler using init JS:
    constructor() {
<code javascript>
        super();
var that = this;


// Create a class that "inherits" from CoreCourseActivityPrefetchHandlerBase.
        this.name = 'AddonModCertificateModulePrefetchHandler';
function AddonModCertificateModulePrefetchHandler() {
        this.modName = 'certificate';
    that.CoreCourseActivityPrefetchHandlerBase.call(this, that.TranslateService, that.CoreAppProvider, that.CoreUtilsProvider,
            that.CoreCourseProvider, that.CoreFilepoolProvider, that.CoreSitesProvider, that.CoreDomUtilsProvider);


    this.name = "AddonModCertificateModulePrefetchHandler";
        // This must match the plugin identifier from db/mobile.php,
    this.modName = "certificate";
        // otherwise the download link in the context menu will not update correctly.
    this.component = "mod_certificate"; // This must match the plugin identifier from db/mobile.php, otherwise the download link in the context menu will not update correctly.
        this.component = 'mod_certificate';
    this.updatesNames = /^configuration$|^.*files$/;
        this.updatesNames = /^configuration$|^.*files$/;
}
    }


AddonModCertificateModulePrefetchHandler.prototype = Object.create(this.CoreCourseActivityPrefetchHandlerBase.prototype);
    // Override the prefetch call.
AddonModCertificateModulePrefetchHandler.prototype.constructor = AddonModCertificateModulePrefetchHandler;
    prefetch(module, courseId, single, dirPath) {
        return this.prefetchPackage(module, courseId, single, prefetchCertificate);
    }


// Override the prefetch call.
}
AddonModCertificateModulePrefetchHandler.prototype.prefetch = function(module, courseId, single, dirPath) {
    return this.prefetchPackage(module, courseId, single, prefetchCertificate);
};


function prefetchCertificate(module, courseId, single, siteId) {
function prefetchCertificate(module, courseId, single, siteId) {
Line 1,333: Line 1,289:


this.CoreCourseModulePrefetchDelegate.registerHandler(new AddonModCertificateModulePrefetchHandler());
this.CoreCourseModulePrefetchDelegate.registerHandler(new AddonModCertificateModulePrefetchHandler());
</code>
</syntaxhighlight>
 
One relatively simple full example is where you have a function that needs to work offline, but it has an additional argument other than the standard ones. You can imagine for this an activity like the book module, where it has multiple pages for the same <code>cmid</code>. The app will not automatically work with this situation it will call the offline function with the standard arguments only so you won't be able to prefetch all the possible parameters.
One relatively simple full example is where you have a function that needs to work offline, but it has an additional argument other than the standard ones. You can imagine for this an activity like the book module, where it has multiple pages for the same cmid. The app will not automatically work with this situation - it will call the offline function with the standard arguments only, so you won't be able to prefetch all the possible parameters.  


To deal with this, you need to implement a web service in your Moodle component that returns the list of possible extra arguments, and then you can call this web service and loop around doing the same thing the app does when it prefetches the offline functions. Here is an example from a third-party module (showing only the actual prefetch function - the rest of the code is as above) where there are multiple values of a custom 'section' parameter for the mobile function 'mobile_document_view':
To deal with this, you need to implement a web service in your Moodle component that returns the list of possible extra arguments, and then you can call this web service and loop around doing the same thing the app does when it prefetches the offline functions. Here is an example from a third-party module (showing only the actual prefetch function, the rest of the code is as above) where there are multiple values of a custom <code>section</code> parameter for the mobile function <code>mobile_document_view</code>:
 
<syntaxhighlight lang="javascript">
<code javascript>
function prefetchOucontent(module, courseId, single, siteId) {
function prefetchOucontent(module, courseId, single, siteId) {
     var component = 'mod_oucontent';
     var component = 'mod_oucontent';
Line 1,377: Line 1,331:
     });
     });
}
}
</code>
</syntaxhighlight>
 
=====Single activity course format=====
=====Single activity course format=====
 
In the following example, the value of <code>INIT_TEMPLATES['main']</code> is:
In the following example, the value of INIT_TEMPLATES["main"] is:
<syntaxhighlight lang="html+ng2">
 
<core-dynamic-component [component]="componentClass" [data]="data"></core-dynamic-component>
<core-dynamic-component [component]="componentClass" [data]="data"></core-dynamic-component>
</syntaxhighlight>
This template is returned by the initialisation method. And this is the JavaScript code returned:
<syntaxhighlight lang="javascript">
var that = this;


This template is returned by the init method. And this is the JavaScript code returned by the init method:
class AddonSingleActivityFormatComponent {
<code javascript>
var that = this;


function getAddonSingleActivityFormatComponent() {
     constructor() {
     function AddonSingleActivityFormatComponent() {
         this.data = {};
         this.data = {};
     };
     }
    AddonSingleActivityFormatComponent.prototype.constructor = AddonSingleActivityFormatComponent;
 
     AddonSingleActivityFormatComponent.prototype.ngOnChanges = function(changes) {
     ngOnChanges(changes) {
         var self = this;
         var self = this;


Line 1,408: Line 1,361:
             this.data.module = module;
             this.data.module = module;
         }
         }
     };
     }
     AddonSingleActivityFormatComponent.prototype.doRefresh = function(refresher, done) {
 
     doRefresh(refresher, done) {
         return Promise.resolve(this.dynamicComponent.callComponentFunction("doRefresh", [refresher, done]));
         return Promise.resolve(this.dynamicComponent.callComponentFunction("doRefresh", [refresher, done]));
     };
     }


return AddonSingleActivityFormatComponent;
}
};


function AddonSingleActivityFormatHandler() {
class AddonSingleActivityFormatHandler {
    this.name = "singleactivity";
   
};
    constructor() {
        this.name = 'singleactivity';
    }


AddonSingleActivityFormatHandler.prototype.constructor = AddonSingleActivityFormatHandler;
    isEnabled() {
        return true;
    }


AddonSingleActivityFormatHandler.prototype.isEnabled = function() {
    canViewAllSections() {
    return true;
        return false;
};
    }


AddonSingleActivityFormatHandler.prototype.canViewAllSections = function(course) {
    getCourseTitle(course, sections) {
    return false;
        if (sections && sections[0] && sections[0].modules && sections[0].modules[0]) {
};
            return sections[0].modules[0].name;
        }


AddonSingleActivityFormatHandler.prototype.getCourseTitle = function(course, sections) {
         return course.fullname || '';
    if (sections && sections[0] && sections[0].modules && sections[0].modules[0]) {
         return sections[0].modules[0].name;
     }
     }


     return course.fullname || "";
     displayEnableDownload() {
};
        return false;
    }


AddonSingleActivityFormatHandler.prototype.displayEnableDownload = function(course) {
    displaySectionSelector() {
    return false;
        return false;
};
    }


AddonSingleActivityFormatHandler.prototype.displaySectionSelector = function(course) {
    getCourseFormatComponent() {
    return false;
        return that.CoreCompileProvider.instantiateDynamicComponent(that.INIT_TEMPLATES['main'], AddonSingleActivityFormatComponent);
};
    }


AddonSingleActivityFormatHandler.prototype.getCourseFormatComponent = function(injector, course) {
}
    that.Injector = injector || that.Injector;
 
    return that.CoreCompileProvider.instantiateDynamicComponent(that.INIT_TEMPLATES["main"], getAddonSingleActivityFormatComponent(), injector);
};


this.CoreCourseFormatDelegate.registerHandler(new AddonSingleActivityFormatHandler());
this.CoreCourseFormatDelegate.registerHandler(new AddonSingleActivityFormatHandler());
</code>
</syntaxhighlight>
 
===Using the JavaScript API===
===Using the JavaScript API===
The JavaScript API is only supported by the delegates specified in the [[#Templates_downloaded_on_login_and_rendered_using_JS_data_2|Templates downloaded on login and rendered using JS data]] section. This API allows you to override any of the functions of the default handler.


The Javascript API is partly supported right now, only the delegates specified in the section [[Mobile_support_for_plugins#Templates_downloaded_on_login_and_rendered_using_JS_data_2|Templates downloaded on login and rendered using JS data]] supports it now. This API allows you to override any of the functions of the default handler.
The <code>method</code> specified in a handler registered in the <code>CoreUserProfileFieldDelegate</code> will be called immediately after the initialisation method, and the JavaScript returned by this method will be run. If this JavaScript code returns an object with certain functions, these functions will override the ones in the default handler.
 
The “method” specified in a handler registered in the ''CoreUserProfileFieldDelegate'' will be called immediately after the init method, and the Javascript returned by this method will be run. If this Javascript code returns an object with certain functions, these function will override the ones in the default handler.
 
For example, if the Javascript returned by the method returns something like this:


<code javascript>
For example, if the JavaScript returned by the method returns something like this:
<syntaxhighlight lang="javascript">
var result = {
var result = {
     getData: function(field, signup, registerAuth, formValues) {
     getData: function(field, signup, registerAuth, formValues) {
        // ...
     }
     }
};
};
result;
result;
</code>
</syntaxhighlight>
 
The the <code>getData</code> function of the default handler will be overridden by the returned <code>getData</code> function.
The the ''getData'' function of the default handler will be overridden by the returned getData function.


The default handler for ''CoreUserProfileFieldDelegate'' only has 2 functions: ''getComponent'' and ''getData''. In addition, the JavaScript code can return an extra function named ''componentInit'' that will be executed when the component returned by ''getComponent'' is initialized.
The default handler for <code>CoreUserProfileFieldDelegate</code> only has 2 functions: <code>getComponent</code> and <code>getData</code>. In addition, the JavaScript code can return an extra function named <code>componentInit</code> that will be executed when the component returned by <code>getComponent</code> is initialised.


Here’s an example on how to support the text user profile field using this API:
Here’s an example on how to support the text user profile field using this API:
 
<syntaxhighlight lang="javascript">
<code javascript>
var that = this;
var that = this;


Line 1,483: Line 1,432:
     componentInit: function() {
     componentInit: function() {
         if (this.field && this.edit && this.form) {
         if (this.field && this.edit && this.form) {
             this.field.modelName = "profile_field_" + this.field.shortname;
             this.field.modelName = 'profile_field_' + this.field.shortname;


             if (this.field.param2) {
             if (this.field.param2) {
                 this.field.maxlength = parseInt(this.field.param2, 10) || "";
                 this.field.maxlength = parseInt(this.field.param2, 10) || '';
             }
             }


             this.field.inputType = that.CoreUtilsProvider.isTrueOrOne(this.field.param3) ? "password" : "text";
             this.field.inputType = that.CoreUtilsProvider.isTrueOrOne(this.field.param3) ? 'password' : 'text';


             var formData = {
             var formData = {
                 value: this.field.defaultdata,
                 value: this.field.defaultdata,
                 disabled: this.disabled
                 disabled: this.disabled,
             };
             };


             this.form.addControl(this.field.modelName, that.FormBuilder.control(formData, this.field.required && !this.field.locked ? that.Validators.required : null));
             this.form.addControl(this.field.modelName,
                that.FormBuilder.control(formData, this.field.required && !this.field.locked ? that.Validators.required : null));
         }
         }
     },
     },
     getData: function(field, signup, registerAuth, formValues) {
     getData: function(field, signup, registerAuth, formValues) {
         var name = "profile_field_" + field.shortname;
         var name = 'profile_field_' + field.shortname;


         return {
         return {
             type: "text",
             type: "text",
             name: name,
             name: name,
             value: that.CoreTextUtilsProvider.cleanTags(formValues[name])
             value: that.CoreTextUtilsProvider.cleanTags(formValues[name]),
         };
         };
     }
     }
Line 1,511: Line 1,461:


result;
result;
</code>
</syntaxhighlight>
 
===Translate dynamic strings===
===Translate dynamic strings===
If you wish to have an element that displays a localised string based on value from your template you can doing something like:
If you wish to have an element that displays a localised string based on value from your template you can doing something like:
 
<syntaxhighlight lang="html+handlebars">
<code>
<ion-card>
<ion-card>
     <ion-card-content translate>
     <ion-card-content>
         plugin.mod_myactivity.<% status %>
         {{ 'plugin.mod_myactivity.<% status %>' | translate }}
     </ion-card-content>
     </ion-card-content>
</ion-card>
</ion-card>
</code>
</syntaxhighlight>
 
This could save you from having to write something like when only one value should be displayed:
This could save you from having to write something like when only one value should be displayed:
 
<syntaxhighlight lang="html+handlebars">
<code>
<ion-card>
<ion-card>
     <ion-card-content>
     <ion-card-content>
Line 1,535: Line 1,480:
     </ion-card-content>
     </ion-card-content>
</ion-card>
</ion-card>
</code>
</syntaxhighlight>
 
===Using strings with dates===
===Using strings with dates===
 
If you have a string that you wish to pass a formatted date, for example in the Moodle language file you have:
If you have a string that you wish to pass a formatted date for example in the Moodle language file you have:
<syntaxhighlight lang="php">
 
<code php>
$string['strwithdate'] = 'This string includes a date of {$a->date} in the middle of it.';
$string['strwithdate'] = 'This string includes a date of {$a->date} in the middle of it.';
</code>
</syntaxhighlight>
 
You can localise the string correctly in your template using something like the following:
You can localise the string correctly in your template using something like the following:
 
<syntaxhighlight lang="html+handlebars">
<code>
{{ 'plugin.mod_myactivity.strwithdate' | translate: {$a: { date: <% timestamp %> * 1000 | coreFormatDate: "dffulldate" } } }}
{{ 'plugin.mod_myactivity.strwithdate' | translate: {$a: { date: <% timestamp %> * 1000 | coreFormatDate: "dffulldate" } } }}
</code>
</syntaxhighlight>
 
A Unix timestamp must be multiplied by 1000 as the Mobile App expects millisecond timestamps, whereas Unix timestamps are in seconds.
A Unix timestamp must be multiplied by 1000 as the Mobile App expects millisecond timestamps, where as Unix timestamps are in seconds.
 
===Support push notification clicks===
===Support push notification clicks===
If your plugin sends push notifications to the app, you might want to open a certain page in the app when the notification is clicked. There are several ways to achieve this.
If your plugin sends push notifications to the app, you might want to open a certain page in the app when the notification is clicked. There are several ways to achieve this.


The easiest way is to include a ''contexturl'' in your notification. When the notification is clicked, the app will try to open the ''contexturl''.
The easiest way is to include a <code>contexturl</code> in your notification. When the notification is clicked, the app will try to open the <code>contexturl</code>.
 
Please notice that the ''contexturl'' will also be displayed in web. If you want to use a specific URL for the app, different than the one displayed in web, you can do so by returning a ''customdata'' array that contains an ''appurl'' property:


<code php>
Please notice that the <code>contexturl</code> will also be displayed in web. If you want to use a specific URL for the app, different than the one displayed in web, you can do so by returning a <code>customdata</code> array that contains an <code>appurl</code> property:
<syntaxhighlight lang="php">
$notification->customdata = [
$notification->customdata = [
     'appurl' => $myurl->out(),
     'appurl' => $myurl->out(),
];
];
</code>
</syntaxhighlight>
 
In both cases you will have to create a link handler to treat the URL. For more info on how to create the link handler, please see [[#Advanced_link_handler|how to create an advanced link handler]].
In both cases you will have to create a link handler to treat the URL. For more info on how to create the link handler, please see [[Mobile_support_for_plugins#Advanced_link_handler|how to create an advanced link handler]].
 
If you want to do something that only happens when the notification is clicked, not when the link is clicked, you'll have to implement a push click handler yourself. The way to create it is similar to [[Mobile_support_for_plugins#Advanced_link_handler|creating an advanced link handler]], but you'll have to use ''CorePushNotificationsDelegate'' and your handler will have to implement the properties and functions defined in the interface [https://github.com/moodlehq/moodlemobile2/blob/master/src/core/pushnotifications/providers/delegate.ts#L24 CorePushNotificationsClickHandler].


If you want to do something that only happens when the notification is clicked, not when the link is clicked, you'll have to implement a push click handler yourself. The way to create it is similar to [[#Advanced_link_handler|creating an advanced link handler]], but you'll have to use <code>CorePushNotificationsDelegate</code> and your handler will have to implement the properties and functions defined in the [https://github.com/moodlehq/moodleapp/blob/master/src/core/features/pushnotifications/services/push-delegate.ts#L27 CorePushNotificationsClickHandler] interface.
===Implement a module similar to mod_label===
===Implement a module similar to mod_label===
 
In Moodle 3.8 or higher, if your plugin doesn't support <code>FEATURE_NO_VIEW_LINK</code> and you don't specify a <code>coursepagemethod</code> then the module will only display the module description in the course page and it won't be clickable in the app, just like <code>mod_label</code>. You can decide if you want the module icon to be displayed or not (if you don't want it to be displayed, then don't define it in <code>displaydata</code>).
In Moodle 3.8 or higher, if your plugin doesn't support ''FEATURE_NO_VIEW_LINK'' and you don't specify a ''coursepagemethod'' then the module will only display the module description in the course page and it won't be clickable in the app, just like mod_label. You can decide if you want the module icon to be displayed or not (if you don't want it to be displayed, then don't define it in ''displaydata'').


However, if your plugin needs to work in previous versions of Moodle or you want to display something different than the description then you need a different approach.
However, if your plugin needs to work in previous versions of Moodle or you want to display something different than the description then you need a different approach.


If your plugin wants to render something in the course page instead of just the module name and description you should specify the property ''coursepagemethod'' in the mobile.php. The template returned by this method will be rendered in the course page. Please notice the HTML returned should not contain directives or components, only default HTML.
If your plugin wants to render something in the course page instead of just the module name and description you should specify the <code>coursepagemethod</code> property in <code>mobile.php</code>. The template returned by this method will be rendered in the course page. Please notice the HTML returned should not contain directives or components, only plain HTML.
 
If you don't want your module to be clickable then you just need to remove the ''method'' from mobile.php. With these 2 changes you can have a module that behaves like mod_label in the app.


If you don't want your module to be clickable then you just need to remove <code>method</code> from <code>mobile.php</code>. With these 2 changes you can have a module that behaves like <code>mod_label</code> in the app.
===Use Ionic navigation lifecycle functions===
===Use Ionic navigation lifecycle functions===
 
Ionic let pages define some functions that will be called when certain navigation lifecycle events happen. For more info about these functions, see [https://ionicframework.com/docs/api/router-outlet Ionic's documentation].
Ionic let pages define some functions that will be called when certain navigation lifecycle events happen. For more info about these functions, see [https://ionicframework.com/blog/navigating-lifecycle-events/ this page].


You can define these functions in your plugin javascript:
You can define these functions in your plugin javascript:
 
<syntaxhighlight lang="javascript">
<code javascript>
this.ionViewWillLeave = function() {
this.ionViewCanLeave = function() {
    // ...
     ...
};</syntaxhighlight>
};</code>
In addition to that, you can also implement <code>canLeave</code> to use Angular route guards:
 
<syntaxhighlight lang="javascript">
this.canLeave = function() {
     // ...
};</syntaxhighlight>
So for example you can make your plugin ask for confirmation if the user tries to leave the page when he has some unsaved data.
So for example you can make your plugin ask for confirmation if the user tries to leave the page when he has some unsaved data.
===Module plugins: dynamically determine if a feature is supported===
===Module plugins: dynamically determine if a feature is supported===
In Moodle you can specify if your plugin supports a certain feature, like <code>FEATURE_NO_VIEW_LINK</code>. If your plugin will always support or not a certain feature, then you can use the <code>supportedfeatures</code> property in <code>mobile.php</code> to specify it ([[#Options_only_for_CoreCourseModuleDelegate|see more documentation about this]]). But if you need to calculate it dynamically then you will have to create a function to calculate it.


In Moodle you can specify if your plugin supports a certain feature, e.g. FEATURE_NO_VIEW_LINK. If your plugin will always support or not a certain feature, then you can use the ''supportedfeatures'' property in mobile.php to specify it (see more documentation about this). But if you need to calculate it dynamically then you will have to create a function to calculate it.
This can be achieved using the initialisation method (for more info, please see the [[#Initialisation|Initialisation]] section above). The JavaScript returned by your initialisation method will need to define a function named <code>supportsFeature</code> that will receive the name of the feature:
 
<syntaxhighlight lang="javascript">
This can be achieved using the "init" method (for more info, please see the [[Mobile_support_for_plugins#Initialization|Initialization]] section above). The Javascript returned by your init method will need to define a function named ''supportsFeature'' that will receive the name of the feature:
 
<code javascript>
var result = {
var result = {
     supportsFeature: function(featureName) {
     supportsFeature: function(featureName) {
         ...
         // ...
     }
     }
};
};
result;</code>
result;
 
</syntaxhighlight>
Currently the app only uses FEATURE_MOD_ARCHETYPE and FEATURE_NO_VIEW_LINK.
Currently the app only uses <code>FEATURE_MOD_ARCHETYPE</code> and <code>FEATURE_NO_VIEW_LINK</code>.
== Testing ==
You can also write automated tests for your plugin using Behat, you can read more about it on the [[Acceptance testing for the Moodle App]] page.
== Upgrading plugins from an older version ==
If you added mobile support to your plugin for the Ionic 3 version of the app (previous to the 3.9.5 release), you will probably need to make some changes to make it compatible with Ionic 5.


Learn more at the [[Moodle App Plugins Upgrade Guide]].
==Troubleshooting==
==Troubleshooting==
=== Invalid response received ===
=== Invalid response received ===
 
You might receive this error when using the <code>core-site-plugins-call-ws</code> directive or similar. By default, the app expects all Web Service calls to return an object, if your Web Service returns another type (string, boolean, etc.) then you need to specify it using the <code>preSets</code> attribute of the directive. For example, if your WS returns a boolean value, then you should specify it like this:
You might receive this error when using the "core-site-plugins-call-ws" directive or similar. By default, the app expects all WebService calls to return an object, if your WebService returns another type (string, bool, ...) then you need to specify it using the preSets attribute of the directive. For example, if your WS returns a boolean value, then you should specify it like this:
<syntaxhighlight lang="html+ng2">
 
[preSets]="{typeExpected: 'boolean'}"
[preSets]="{typeExpected: 'boolean'}"
 
</syntaxhighlight>
In a similar way, if your WebService returns null you need to tell the app not to expect any result using the preSets:
In a similar way, if your Web Service returns <code>null</code> you need to tell the app not to expect any result using <code>preSets</code>:
 
<syntaxhighlight lang="html+ng2">
[preSets]="{responseExpected: false}"
[preSets]="{responseExpected: false}"
</syntaxhighlight>
=== Values of <code>ion-radio</code>, <code>ion-checkbox</code> or <code>ion-select</code> aren't sent to my WS ===
Some directives allow you to specify a form id or name to send the data from the form to a certain WS. These directives look for HTML inputs to retrieve the data to send. However, <code>ion-radio</code>, <code>ion-checkbox</code> and <code>ion-select</code> don't use HTML inputs, they simulate them, so the directive isn't going to find their data and so it won't be sent to the Web Service.


=== Values of ion-radio, ion-checkbox or ion-select aren't sent to my WS ===
There are 2 workarounds to fix this problem.
 
Some directives allow you to specify a form id or name to send the data from the form to a certain WS. These directives look for HTML inputs to retrieve the data to send. However, ion-radio, ion-checkbox and ion-select don't use HTML inputs, they simulate them, so the directive isn't going to find their data and so it won't be sent to the WebService.
 
There are 2 workarounds to fix this problem. It seems that the next major release of Ionic framework does use HTML inputs, so these are temporary solutions.
 
==== Sending the data manually ====
==== Sending the data manually ====
 
The first solution is to send the missing params manually using the <code>params</code> property. We will use <code>ngModel</code> to store the input value in a variable, and this variable will be passed to the parameters. Please notice that <code>ngModel</code> requires the element to have a name, so if you add <code>ngModel</code> to a certain element you need to add a name too.
The first solution is to send the missing params manually using the "''params''" property. We will use ''ngModel'' to store the input value in a variable, and this variable will be passed to the params. Please notice that ''ngModel'' '''requires''' the element to have a name, so if you add ''ngModel'' to a certain element you need to add a name too.


For example, if you have a template like this:
For example, if you have a template like this:
 
<syntaxhighlight lang="html+ng2">
<code javascript>
<ion-list radio-group name="responses">
<ion-list radio-group name="responses">
     <ion-item>
     <ion-item>
Line 1,642: Line 1,572:
</ion-list>
</ion-list>


<button ion-button block type="submit" core-site-plugins-call-ws name="myws" [params]="{id: <% id %>}" form="myform">
<ion-button expand="block" type="submit" core-site-plugins-call-ws name="myws" [params]="{id: <% id %>}" form="myform">
     {{ 'plugin.mycomponent.save' | translate }}
     {{ 'plugin.mycomponent.save' | translate }}
</button>
</ion-button>
</code>
</syntaxhighlight>
 
Then you should modify it like this:
Then you should modify it like this:
 
<syntaxhighlight lang="html+ng2">
<code javascript>
<ion-list radio-group [(ngModel)]="responses">
<ion-list radio-group [(ngModel)]="responses">
     <ion-item>
     <ion-item>
Line 1,657: Line 1,585:
</ion-list>
</ion-list>


<button ion-button block type="submit" core-site-plugins-call-ws name="myws" [params]="{id: <% id %>, responses: responses}" form="myform">
<ion-button expand="block" type="submit" core-site-plugins-call-ws name="myws" [params]="{id: <% id %>, responses: responses}" form="myform">
     {{ 'plugin.mycomponent.save' | translate }}
     {{ 'plugin.mycomponent.save' | translate }}
</button>
</ion-button>
</code>
</syntaxhighlight>
 
Basically, you need to add <code>ngModel</code> to the affected element (in this case, the <code>radio-group</code>). You can put whatever name you want as the value, we used "responses". With this, every time the user selects a radio button the value will be stored in a variable called "responses". Then, in the button we are passing this variable to the parameters of the Web Service.
Basically, you need to add ''ngModel'' to the affected element (in this case, the ''radio-group''). You can put whatever name you want as the value, we used "responses". With this, everytime the user selects a radio button the value will be stored in a variable named "responses". Then, in the button we are passing this variable to the params of the WebService.
 
Please notice that the "form" attribute has priority over "params", so if you have an input with name="responses" it will override what you're manually passing to params.


Please notice that the <code>form</code> attribute has priority over <code>params</code>, so if you have an input with <code>name="responses"</code> it will override what you're manually passing to <code>params</code>.
==== Using a hidden input ====
==== Using a hidden input ====
 
Since the directive is looking for HTML inputs, you need to add one with the value to send to the server. You can use <code>ngModel</code> to synchronise your radio/checkbox/select with the new hidden input. Please notice that <code>ngModel</code> requires the element to have a name, so if you add <code>ngModel</code> to a certain element you need to add a name too.
Since the directive is looking for HTML inputs, you need to add one with the value to send to the server. You can use ''ngModel'' to synchronize your ion-radio/ion-checkbox/ion-select with the new hidden input. Please notice that ''ngModel'' '''requires''' the element to have a name, so if you add ''ngModel'' to a certain element you need to add a name too.


For example, if you have a radio button like this:
For example, if you have a radio button like this:
 
<syntaxhighlight lang="html+ng2">
<code javascript>
<div radio-group name="responses">  
<div radio-group name="responses">  
     <ion-item>
     <ion-item>
Line 1,679: Line 1,603:
     </ion-item>
     </ion-item>
</div>
</div>
</code>
</syntaxhighlight>
 
Then you should modify it like this:
Then you should modify it like this:
 
<syntaxhighlight lang="html+ng2">
<code javascript>
<div radio-group name="responses" [(ngModel)]="responses">  
<div radio-group name="responses" [(ngModel)]="responses">  
     <ion-item>
     <ion-item>
Line 1,692: Line 1,614:
     <ion-input type="hidden" [ngModel]="responses" name="responses"></ion-input>
     <ion-input type="hidden" [ngModel]="responses" name="responses"></ion-input>
</div>
</div>
</code>
</syntaxhighlight>
 
In the example above, we're using a variable called "responses" to synchronise the data between the <code>radio-group</code> and the hidden input. You can use whatever name you want.
In the example above, we're using a variable named "responses" to synchronize the data between the ''radio-group'' and the hidden input. You can use whatever name you want.
=== I can't return an object or array in <code>otherdata</code> ===
 
If you try to return an object or an array in any field inside <code>otherdata</code>, the Web Service call will fail with the following error:
=== I can't return an object or array in otherdata ===
<syntaxhighlight lang="text">
 
Scalar type expected, array or object received
If you try to return an object or an array in any field inside ''otherdata'', the WebService call will fail with the following error:
</syntaxhighlight>
 
Each field in <code>otherdata</code> must be a string, number or boolean; it cannot be an object or array. To make it work, you need to encode your object or array into a JSON string:
''Scalar type expected, array or object received''
<syntaxhighlight lang="php">
 
'otherdata' => ['data' => json_encode($data)],
Each field in ''otherdata'' must be a string, number or boolean, it cannot be an object or array. To make it work, you need to encode your object or array into a JSON string:
</syntaxhighlight>
 
<code php>
'otherdata' => array('data' => json_encode($data))</code>
 
The app will automatically parse this JSON and convert it back into an array or object.
The app will automatically parse this JSON and convert it back into an array or object.
==Examples==
==Examples==
===Accepting dynamic names in a Web Service===
We want to display a form where the names of the fields are dynamic, like it happens in quiz. This data will be sent to a new Web Service that we have created.


===Accepting dynamic names in a WebService===
The first issue we find is that the Web Service needs to define the names of the parameters received, but in this case they're dynamic. The solution is to accept an array of objects with name and value. So in the <code>_parameters()</code> function of our new Web Service, we will add this parameter:
 
<syntaxhighlight lang="php">
We want to display a form where the names of the fields are dynamic, like it happens in quiz. This data will be sent to a new WebService that we have created.
 
The first issue we find is that the WebService needs to define the names of the parameters received, but in this case they're dynamic. The solution is to accept an array of objects with name and value. So in the ''_parameters()'' function of our new WebService, we will add this parameter:
 
<code php>
'data' => new external_multiple_structure(
'data' => new external_multiple_structure(
     new external_single_structure(
     new external_single_structure(
         array(
         [
             'name' => new external_value(PARAM_RAW, 'data name'),
             'name' => new external_value(PARAM_RAW, 'data name'),
             'value' => new external_value(PARAM_RAW, 'data value'),
             'value' => new external_value(PARAM_RAW, 'data value'),
         )
         ]
     ),
     ),
     'The data to be saved', VALUE_DEFAULT, array()
     'The data to be saved', VALUE_DEFAULT, []
)</code>
)
</syntaxhighlight>
Now we need to adapt our form to send the data as the Web Service requires it. In our template, we have a button with the <code>core-site-plugins-call-ws</code> directive that will send the form data to our Web Service. To make this work we will have to pass the parameters manually, without using the <code>form</code> attribute, because we need to format the data before it is sent.


Now we need to adapt our form to send the data as the WebService requires it. In our template, we have a button with the directive ''core-site-plugins-call-ws'' that will send the form data to our WebService. To make this work we will have to pass the parameters manually, without using the "''form''" attribute, because we need to format the data before it is sent.
Since we will send the parameters manually and we want it all to be sent in the same array, we will use <code>ngModel</code> to store the input data into a variable that we'll call <code>data</code>, but you can use the name you want. This variable will be an object that will hold the input data with the format "name->value". For example, if I have an input with name "a1" and value "My answer", the data object will be:
 
<syntaxhighlight lang="javascript">
Since we will send the params manually and we want it all to be sent in the same array, we will use ''ngModel'' to store the input data into a variable that we'll call "data", but you can use the name you want. This "data" will be an object that will hold the input data with the format "name->value". For example, if I have an input with name "a1" and value "My answer", the data object will be:
{a1: 'My answer'}
 
</syntaxhighlight>
{a1: "My answer"}
So we need to add <code>ngModel</code> to all the inputs whose values need to be sent to the <code>data</code> WS param. Please notice that <code>ngModel</code> requires the element to have a name, so if you add <code>ngModel</code> to a certain element you need to add a name too. For example:
 
<syntaxhighlight lang="html+ng2">
So we need to add ''ngModel'' to all the inputs whose values need to be sent to the "data" WS param. Please notice that ''ngModel'' '''requires''' the element to have a name, so if you add ''ngModel'' to a certain element you need to add a name too. For example:
<ion-input name="<% name %>" [(ngModel)]="CONTENT_OTHERDATA.data['<% name %>']">
 
</syntaxhighlight>
<code javascript><ion-input name="<% name %>" [(ngModel)]="CONTENT_OTHERDATA.data['<% name %>']"></code>
As you can see, we're using <code>CONTENT_OTHERDATA</code> to store the data. We do it like this because we'll use <code>otherdata</code> to initialise the form, setting the values the user has already stored. If you don't need to initialise the form, then you can use the <code>dataObject</code> variable, an empty object that the mobile app creates for you:
 
<syntaxhighlight lang="html+ng2">
As you can see, we're using ''CONTENT_OTHERDATA'' to store the data. We do it like this because we'll use ''otherdata'' to initialize the form, setting the values the user has already stored. If you don't need to initialize the form, then you can use the variable "dataObject", an empty object that the Mobile app creates for you: [(ngModel)]="dataObject['<% name %>']"
[(ngModel)]="dataObject['<% name %>']"
 
</syntaxhighlight>
The Mobile app has a function that allows you to convert this data object into an array like the one the WS expects: ''objectToArrayOfObjects''. So in our button we'll use this function to format the data before it's sent:
The app has a function that allows you to convert this data object into an array like the one the WS expects: <code>objectToArrayOfObjects</code>. So in our button we'll use this function to format the data before it's sent:
 
<syntaxhighlight lang="html+ng2">
<code javascript>
<ion-button expand="block" type="submit" core-site-plugins-call-ws name="my_ws_name"
<button ion-button block type="submit" core-site-plugins-call-ws name="my_ws_name"
     [params]="{id: <% id %>, data: CoreUtilsProvider.objectToArrayOfObjects(CONTENT_OTHERDATA.data, 'name', 'value')}"
     [params]="{id: <% id %>, data: CoreUtilsProvider.objectToArrayOfObjects(CONTENT_OTHERDATA.data, 'name', 'value')}"
     successMessage
     successMessage
     refreshOnSuccess="true">
     refreshOnSuccess="true">
</code>
</syntaxhighlight>
As you can see in the example above, we're specifying that the keys of the <code>data</code> object need to be stored in a property called "name", and the values need to be stored in a property called "value". If your Web Service expects different names you need to change the parameters of the <code>objectToArrayOfObjects</code> function.


As you can see in the example above, we're specifying that the keys of the "data" object need to be stored in a property named "name", and the values need to be stored in a property named "value". If your WebService expects different names you need to change the parameters of the function ''objectToArrayOfObjects''.
If you open your plugin now in the app it will display an error in the JavaScript console. The reason is that the <code>data</code> variable doesn't exist inside <code>CONTENT_OTHERDATA</code>. As it is explained in previous sections, <code>CONTENT_OTHERDATA</code> holds the data that you return in <code>otherdata</code> for your method. We'll use <code>otherdata</code> to initialise the values to be displayed in the form.
 
If you open your plugin now in the Mobile app it will display an error in the Javascript console. The reason is that the variable "data" doesn't exist inside ''CONTENT_OTHERDATA''. As it is explained in previous sections, ''CONTENT_OTHERDATA'' holds the data that you return in ''otherdata'' for your method. We'll use ''otherdata'' to initialize the values to be displayed in the form.
 
If the user hasn't answered the form yet, we can initialize the "data" object as an empty object. Please remember that we cannot return arrays or objects in ''otherdata'', so we'll return a JSON string.
 
<code php>
'otherdata' => array('data' => '{}')</code>


If the user hasn't answered the form yet, we can initialise the <code>data</code> object as an empty object. Please remember that we cannot return arrays or objects in <code>otherdata</code>, so we'll return a JSON string.
<syntaxhighlight lang="php">
'otherdata' => ['data' => '{}'],
</syntaxhighlight>
With the code above, the form will always be empty when the user opens it. But now we want to check if the user has already answered the form and fill the form with the previous values. We will do it like this:
With the code above, the form will always be empty when the user opens it. But now we want to check if the user has already answered the form and fill the form with the previous values. We will do it like this:
<syntaxhighlight lang="php">
$userdata = get_user_responses(); // It will held the data in a format name->value. Example: ['a1' => 'My value'].


<code php>
// ...
$userdata = get_user_responses(); // It will held the data in a format name->value. Example: array('a1' => 'My value').
...
'otherdata' => array('data' => json_encode($userdata))
</code>
 
Now the user will be able to see previous values when the form is opened, and clicking the button will send the data to our WebService in array format.


'otherdata' => ['data' => json_encode($userdata)],
</syntaxhighlight>
Now the user will be able to see previous values when the form is opened, and clicking the button will send the data to our Web Service in array format.
==Moodle plugins with mobile support==
==Moodle plugins with mobile support==
* Group choice: [https://moodle.org/plugins/mod_choicegroup Moodle plugins directory entry] and [https://github.com/ndunand/moodle-mod_choicegroup code in github].
* Group choice: [https://moodle.org/plugins/mod_choicegroup Moodle plugins directory entry] and [https://github.com/ndunand/moodle-mod_choicegroup code in github].
* Custom certificate: [https://moodle.org/plugins/mod_customcert Moodle plugins directory entry] and [https://github.com/markn86/moodle-mod_customcert code in github].
* Custom certificate: [https://moodle.org/plugins/mod_customcert Moodle plugins directory entry] and [https://github.com/markn86/moodle-mod_customcert code in github].
Line 1,779: Line 1,690:
* ForumNG (unfinished support) [https://moodle.org/plugins/mod_forumng Moodle plugins directory entry] and [https://github.com/moodleou/moodle-mod_forumng in github].
* ForumNG (unfinished support) [https://moodle.org/plugins/mod_forumng Moodle plugins directory entry] and [https://github.com/moodleou/moodle-mod_forumng in github].
* News block [https://github.com/moodleou/moodle-block_news in github].
* News block [https://github.com/moodleou/moodle-block_news in github].
* H5P activity module [https://moodle.org/plugins/mod_hvp Moodle plugins directory entry] and [https://github.com/h5p/h5p-moodle-plugin in github].
* H5P activity module [https://moodle.org/plugins/mod_hvp Moodle plugins directory entry] and [https://github.com/h5p/h5p-moodle-plugin in github].
 
See the complete list in [https://moodle.org/plugins/browse.php?list=award&id=6 the plugins database] (it may contain some outdated plugins).
See the complete list in the plugins database [https://moodle.org/plugins/browse.php?list=award&id=6 here] (it may contain some outdated plugins)
 
=== Mobile app support award ===
=== Mobile app support award ===
 
If you want your plugin to be awarded in the plugins directory and marked as supporting the mobile app, please feel encouraged to contact us via email at [mailto:mobile@moodle.com mobile@moodle.com].
If you want your plugin to be awarded in the plugins directory and marked as supporting the mobile app, please feel encouraged to contact us via email [mailto:mobile@moodle.com mobile@moodle.com].


Don't forget to include a link to your plugin page and the location of its code repository.
Don't forget to include a link to your plugin page and the location of its code repository.


See [https://moodle.org/plugins/?q=award:mobile-app the list of awarded plugins] in the plugins directory
See [https://moodle.org/plugins/?q=award:mobile-app the list of awarded plugins] in the plugins directory.
 
[[Category:Mobile]]
[[Category:Mobile]]
[[Category:Moodle App Ionic 5]]

Latest revision as of 13:03, 14 July 2022

Important:

This content of this page has been updated and migrated to the new Moodle Developer Resources. The information contained on the page should no longer be seen up-to-date.

Why not view this page on the new site and help us to migrate more content to the new site!


If you want to add mobile support to your Moodle plugin, you can achieve it by extending different areas of the app using just PHP server side code and providing templates written with Ionic and custom components.

You will have to:

  1. Create a db/mobile.php file in your plugin. In this file, you will be able to indicate which areas of the app you want to extend. For example, adding a new option in the main menu, implementing support for a new activity module, including a new option in the course menu, including a new option in the user profile, etc. All the areas supported are described further in this document.
  2. Create new functions in a reserved namespace that will return the content of the new options. The content should be returned rendered as html. This html should use Ionic components so that it looks native, but it can be generated using mustache templates.


Let’s clarify some points:

  • You don’t need to create new Web Service functions (although you will be able to use them for advanced features). You just need plain php functions that will be placed in a reserved namespace.
  • Those functions will be exported via the Web Service function tool_mobile_get_content.
  • As arguments of your functions you will always receive the userid, some relevant details of the app (like the app version or the current language in the app), and some specific data depending on the type of plugin (courseid, cmid, ...).
  • The mobile app also implements a list of custom Ionic components and directives that provide dynamic behaviour; like indicating that you are linking a file that can be downloaded, allowing a transition to new pages into the app calling a specific function in the server, submitting form data to the server, etc.


Getting started

If you only want to write a plugin, it is not necessary that you set up your environment to work with the Moodle App. In fact, you don't even need to compile it. You can just use a Chromium-based browser to add mobile support to your plugins!

You can use the app from one of the hosted versions on master.apps.moodledemo.net (the latest stable version) and integration.apps.moodledemo.net (the latest development version). If you need any specific environment (hosted versions are deployed with a production environment), you can also use Docker images. And if you need to test your plugin in a native device, you can always use Moodle HQ's application.

This should suffice for developing plugins. However, if you are working on advanced functionality and you need to run the application from the source code, you can find more information in the Moodle App Development Guide.

Development workflow

Before getting into the specifics of your plugin, we recommend that you start adding a simple "Hello World" button in the app to see that everything works properly.

Let's say your plugin is called local_hello, you can start by adding the following files:

db/mobile.php

<?php

$addons = [
    'local_hello' => [
        'handlers' => [
            'hello' => [
                'delegate' => 'CoreMainMenuDelegate',
                'method' => 'view_hello',
                'displaydata' => [
                    'title' => 'hello',
                    'icon' => 'earth',
                ],
            ],
        ],
        'lang' => [
            ['hello', 'local_hello'],
        ],
    ],
];

classes/output/mobile.php

<?php

namespace local_hello\output;

defined('MOODLE_INTERNAL') || die();

class mobile {

    public static function view_hello() {
        return [
            'templates' => [
                [
                    'id' => 'main',
                    'html' => '<h1 class="text-center">{{ "plugin.local_hello.hello" | translate }}</h1>',
                ],
            ],
        ];
    }

}

lang/en/local_hello.php

<?php

$string['hello'] = 'Hello World';

Once you've done that, try logging into your site in the app and you should see a new button in the main menu or more menu (depending on the device) saying "Hello World". If you press this button, you should see a page saying "Hello World!".

Congratualtions, you have written your first Moodle plugin with moodle support!

You can read the rest of this page to learn more about mobile plugins and start working on your plugin. Here's some things to keep in mind:

  • If you change the mobile.php file, you will have to refresh the browser. And remember to disable the network cache.
  • If you change an existing template or function, you won’t have to refresh the browser. In most cases, doing a PTR (Pull To Refresh) in the page that displays the template will suffice.
  • If any of these doesn't show your changes, you may need to purge all caches to avoid problems with the auto-loading cache.
  • Ultimately, if that doesn't work either, you may have to log out from the site and log in again. If any changes affect plugin installation, you may also need to increase the version in your plugin's version.php file and upgrade it in the site.

Types of plugins

There are 3 types of plugins:

Templates generated and downloaded when the user opens the plugins

Templates downloaded when requested.png

With this type of plugin, the template of your plugin will be generated and downloaded when the user opens the plugin in the app. This means that your function will receive some context parameters. For example, if you're developing a course module plugin you will receive the courseid and the cmid (course module ID). You can see the list of delegates that support this type of plugin in the Delegates section.

Templates downloaded on login and rendered using JS data

Templates downloaded on login.png

With this type of plugin, the template for your plugin will be downloaded when the user logs in into the app and will be stored in the device. This means that your function will not receive any context parameters, and you need to return a generic template that will be built with JS data like the ones in the Moodle App. When the user opens a page that includes your plugin, your template will receive the required JS data and your template will be rendered. You can see the list of delegates that support this type of plugin in the Delegates section.

Pure JavaScript plugins

You can always implement the whole plugin yourself using JavaScript instead of using our API. In fact, this is required if you want to implement some features like capturing links in the Mobile app. You can see the list of delegates that only support this type of plugin in the Delegates section.

Step by step example

In this example, we are going to update an existing plugin, the Certificate activity module, that previously used a Remote add-on (a legacy approach to implement mobile plugins).

This is a simple activity module that displays the certificate issued for the current user along with the list of the dates of previously issued certificates. It also stores in the course log that the user viewed a certificate. This module also works offline: when the user downloads the course or activity, the data is pre-fetched and can be viewed offline.

Step 1. Update the db/mobile.php file

In this case, we are updating an existing file. For new plugins, you should create this new file.

$addons = [
    'mod_certificate' => [ // Plugin identifier
        'handlers' => [ // Different places where the plugin will display content.
            'coursecertificate' => [ // Handler unique name (alphanumeric).
                'displaydata' => [
                    'icon' => $CFG->wwwroot . '/mod/certificate/pix/icon.gif',
                    'class' => '',
                ],
       
                'delegate' => 'CoreCourseModuleDelegate', // Delegate (where to display the link to the plugin)
                'method' => 'mobile_course_view', // Main function in \mod_certificate\output\mobile
                'offlinefunctions' => [
                    'mobile_course_view' => [],
                    'mobile_issues_view' => [],
                ], // Function that needs to be downloaded for offline.
            ],
        ],
        'lang' => [ // Language strings that are used in all the handlers.
            ['pluginname', 'certificate'],
            ['summaryofattempts', 'certificate'],
            ['getcertificate', 'certificate'],
            ['requiredtimenotmet', 'certificate'],
            ['viewcertificateviews', 'certificate'],
        ],
    ],
];
Plugin identifier
A unique name for the plugin, it can be anything (there’s no need to match the module name).
Handlers (Different places where the plugin will display content)
A plugin can be displayed in different views in the app. Each view should have a unique name inside the plugin scope (alphanumeric).
Display data
This is only needed for certain types of plugins. Also, depending on the type of delegate it may require additional (or less fields). In this case, we are indicating the module icon.
Delegate
Where to display the link to the plugin, see the Delegates section for all the possible options.
Method
This is the method in the Moodle \{component-name}\output\mobile class to be executed the first time the user clicks in the new option displayed in the app.
Offlinefunctions
This is the list of functions that need to be called and stored when the user downloads a course for offline usage. Please note that you can add functions here that are not even listed in the mobile.php file.
In our example, downloading for offline access will mean that we'll execute the functions for getting the certificate and issued certificates passing as parameters the current userid (and courseid when we are using the mod or course delegate). If we have the result of those functions stored in the app, we'll be able to display the certificate information even if the user is offline.
Offline functions will be mostly used to display information for final users, any further interaction with the view won’t be supported offline (for example, trying to send information when the user is offline).
You can indicate here other Web Services functions, indicating the parameters that they might need from a defined subset (currently userid and courseid).
Prefetching the module will also download all the files returned by the methods in these offline functions (in the files array).
Note that if your functions use additional custom parameters (for example, if you implement multiple pages within a module's view function by using a page parameter in addition to the usual cmid, courseid, and userid) then the app will not know which additional parameters to supply. In this case, do not list the function in offlinefunctions; instead, you will need to manually implement a module prefetch handler.
Lang
The language pack string ids used in the plugin by all the handlers. Normally these will be strings from your own plugin, however, you can list any strings you need here, like ['cancel', 'moodle']. If you do this, be warned that in the app you will then need to refer to that string as {{ 'plugin.myplugin.cancel' | translate }} (not {{ 'plugin.moodle.cancel' | translate }}).
Please only include the strings you actually need. The Web Service that returns the plugin information will include the translation of each string id for every language installed in the platform, and this will then be cached, so listing too many strings is very wasteful.

There are additional attributes supported by the mobile.php list, you can find about them in the Mobile.php supported options section.

Step 2. Creating the main function

The main function displays the current issued certificate (or several warnings if it’s not possible to issue a certificate). It also displays a link to view the dates of previously issued certificates.

All the functions must be created in the plugin or subsystem classes/output directory, the name of the class must be mobile.

For this example, the namespace name will be mod_certificate\output.

mod/certificate/classes/output/mobile.php

<?php

namespace mod_certificate\output;

use context_module;
use mod_certificate_external;

class mobile {

    /**
     * Returns the certificate course view for the mobile app.
     *
     * @param  array $args Arguments from tool_mobile_get_content WS.
     *
     * @return array       HTML, JS and other data.
     */
    public static function mobile_course_view($args) {
        global $OUTPUT, $USER, $DB;

        $args = (object) $args;
        $cm = get_coursemodule_from_id('certificate', $args->cmid);

        // Capabilities check.
        require_login($args->courseid, false, $cm, true, true);

        $context = \context_module::instance($cm->id);

        require_capability('mod/certificate:view', $context);
        if ($args->userid != $USER->id) {
            require_capability('mod/certificate:manage', $context);
        }
        $certificate = $DB->get_record('certificate', ['id' => $cm->instance]);

        // Get certificates from external (taking care of exceptions).
        try {
            $issued = \mod_certificate_external::issue_certificate($cm->instance);
            $certificates = \mod_certificate_external::get_issued_certificates($cm->instance);
            $issues = array_values($certificates['issues']); // Make it mustache compatible.
        } catch (Exception $e) {
            $issues = [];
        }

        // Set timemodified for each certificate.
        foreach ($issues as $issue) {
            if (empty($issue->timemodified)) {
                $issue->timemodified = $issue->timecreated;
            }
        }

        $showget = true;
        if ($certificate->requiredtime && !has_capability('mod/certificate:manage', $context)) {
            if (certificate_get_course_time($certificate->course) < ($certificate->requiredtime * 60)) {
                $showget = false;
            }
        }

        $certificate->name = format_string($certificate->name);
        [$certificate->intro, $certificate->introformat] =
                external_format_text($certificate->intro, $certificate->introformat, $context->id, 'mod_certificate', 'intro');
        $data = [
            'certificate' => $certificate,
            'showget' => $showget && count($issues) > 0,
            'issues' => $issues,
            'issue' => $issues[0],
            'numissues' => count($issues),
            'cmid' => $cm->id,
            'courseid' => $args->courseid,
        ];

        return [
            'templates' => [
                [
                    'id' => 'main',
                    'html' => $OUTPUT->render_from_template('mod_certificate/mobile_view_page', $data),
                ],
            ],
            'javascript' => '',
            'otherdata' => '',
            'files' => $issues,
        ];
    }
}
Function declaration
The function name is the same as the one used in the mobile.php file (method field). There is only one argument, $args, which is an array containing all the information sent by the mobile app (the courseid, userid, appid, appversionname, appversioncode, applang, appcustomurlscheme, ...).
Function implementation
In the first part of the function, we check permissions and capabilities (like a view.php script would do normally). Then we retrieve the certificate information that’s necessary to display the template.
Function return
  • templates — The rendered template (notice that we could return more than one template, but we usually would only need one). By default the app will always render the first template received, the rest of the templates can be used if the plugin defines some JavaScript code.
  • javascript — Empty, because we don’t need any in this case.
  • otherdata — Empty as well, because we don’t need any additional data to be used by directives or components in the template. This field will be published as an object supporting 2-way data-binding in the template.
  • files — A list of files that the app should be able to download (for offline usage mostly).

Step 3. Creating the template for the main function

This is the most important part of your plugin because it contains the code that will be rendered on the mobile app.

In this template we’ll be using Ionic, together with directives and components specific to the Moodle App.

All the HTML elements starting with ion- are ionic components. Most of the time, the component name is self-explanatory but you may refer to a detailed guide here: https://ionicframework.com/docs/components/

All the HTML elements starting with core- are custom components of the Moodle App.

mod/certificate/templates/mobile_view_page.mustache

{{=<% %>=}}
<div>
    <core-course-module-description description="<% certificate.intro %>" component="mod_certificate" componentId="<% cmid %>">
    </core-course-module-description>

    <ion-list>
        <ion-list-header>
            <p class="item-heading">{{ 'plugin.mod_certificate.summaryofattempts' | translate }}</p>
        </ion-list-header>

        <%#issues%>
            <ion-item>
                <ion-label><ion-button expand="block" color="light" core-site-plugins-new-content title="<% certificate.name %>" 
                        component="mod_certificate" method="mobile_issues_view"
                        [args]="{cmid: <% cmid %>, courseid: <% courseid %>}">
                    {{ 'plugin.mod_certificate.viewcertificateviews' | translate: {$a: <% numissues %>} }}
                </ion-button></ion-label>
            </ion-item>
        <%/issues%>

        <%#showget%>
            <ion-item>
                <ion-label>
                    <ion-button expand="block" core-course-download-module-main-file moduleId="<% cmid %>"
                        courseId="<% certificate.course %>" component="mod_certificate"
                        [files]="[{
                            fileurl: '<% issue.fileurl %>',
                            filename: '<% issue.filename %>',
                            timemodified: '<% issue.timemodified %>', mimetype: '<% issue.mimetype %>',
                        }]">
                 
                        <ion-icon name="cloud-download" slot="start"></ion-icon>
                            {{ 'plugin.mod_certificate.getcertificate' | translate }}
                    </ion-button>
                </ion-label>
            </ion-item>
        <%/showget%>

        <%^showget%>
            <ion-item>
                <ion-label><p>{{ 'plugin.mod_certificate.requiredtimenotmet' | translate }}</p></ion-label>
            </ion-item>
        <%/showget%>

        <!-- Call log WS when the template is loaded. -->
        <span core-site-plugins-call-ws-on-load name="mod_certificate_view_certificate"
                [params]="{certificateid: <% certificate.id %>}" [preSets]="{getFromCache: 0, saveToCache: 0}">
        </span>
    </ion-list>
</div>

In the first line of the template we switch delimiters to avoid conflicting with Ionic delimiters (that are curly brackets like mustache).

Then we display the module description using core-course-module-description, which is a component used to include the course module description.

For displaying the certificate information we create a list of elements, adding a header on top.

The following line using the translate filter indicates that the app will translate the summaryofattempts string id (here we could’ve used mustache translation but it is usually better to delegate the strings translations to the app). The string id has the following format:

plugin.{plugin-identifier}.{string-id}

Where {plugin-identifier} is taken from mobile.php and {string-id} must be indicated in the lang field in mobile.php.

Then, we display a button to transition to another page if there are certificates issued. The attribute (directive) core-site-plugins-new-content indicates that if the user clicks the button, we need to call the mobile_issues_view function in the mod_certificate component; passing as arguments the cmid and courseid. The content returned by this function will be displayed in a new page (read the following section to see the code of this new page).

Just after this button, we display another one but this time for downloading an issued certificate. The core-course-download-module-main-file directive indicates that clicking this button is for downloading the whole activity and opening the main file. This means that, when the user clicks this button, the whole certificate activity will be available offline.

Finally, just before the ion-list is closed, we use the core-site-plugins-call-ws-on-load directive to indicate that once the page is loaded, we need to call a Web Service function in the server, in this case we are calling the mod_certificate_view_certificate that will log that the user viewed this page.

As you can see, no JavaScript was necessary at all. We used plain HTML elements and attributes that did all the complex dynamic logic (like calling a Web Service) behind the scenes.

Step 4. Adding an additional page

Add the following method to mod/certificate/classes/output/mobile.php:

/**
 * Returns the certificate issues view for the mobile app.
 * @param  array $args Arguments from tool_mobile_get_content WS.
 *
 * @return array       HTML, JS and other data.
 */
public static function mobile_issues_view($args) {
    global $OUTPUT, $USER, $DB;

    $args = (object) $args;
    $cm = get_coursemodule_from_id('certificate', $args->cmid);

    // Capabilities check.
    require_login($args->courseid, false, $cm, true, true);

    $context = context_module::instance($cm->id);

    require_capability ('mod/certificate:view', $context);
    if ($args->userid != $USER->id) {
        require_capability('mod/certificate:manage', $context);
    }
    $certificate = $DB->get_record('certificate', ['id' => $cm->instance]);

    // Get certificates from external (taking care of exceptions).
    try {
        $issued = mod_certificate_external::issue_certificate($cm->instance);
        $certificates = mod_certificate_external::get_issued_certificates($cm->instance);
        $issues = array_values($certificates['issues']); // Make it mustache compatible.
    } catch (Exception $e) {
        $issues = [];
    }

    $data = ['issues' => $issues];

    return [
        'templates' => [
            [
                'id' => 'main',
                'html' => $OUTPUT->render_from_template('mod_certificate/mobile_view_issues', $data),
            ],
        ],
        'javascript' => '',
        'otherdata' => '',
    ];
}

This method for the new page was added just after mobile_course_view, the code is quite similar: checks the capabilities, retrieves the information required for the template, and returns the template rendered.

The code of the mustache template is also very simple.

mod/certificate/templates/mobile_view_issues.mustache

{{=<% %>=}}
<div>
    <ion-list>
        <%#issues%>
            <ion-item>
                <ion-label>
                    <p class="item-heading">{{ <%timecreated%> | coreToLocaleString }}</p>
                    <p><%grade%></p>
                </ion-label>
            </ion-item>
        <%/issues%>
    </ion-list>
</div>

As we did in the previous template, in the first line of the template we switch delimiters to avoid conflicting with Ionic delimiters (that are curly brackets like mustache).

Here we are creating an Ionic list that will display a new item in the list per each issued certificated.

For the issued certificated we’ll display the time when it was created (using the app filter coreToLocaleString). We are also displaying the grade displayed in the certificate (if any).

Step 5. Plugin webservices, if included

If your plugin uses its own web services, they will also need to be enabled for mobile access in your db/services.php file.

The following line should be included in each webservice definition:

'services' => [MOODLE_OFFICIAL_MOBILE_SERVICE, 'local_mobile'],

mod/certificate/db/services.php

<?php

$functions = [
    'mod_certificate_get_certificates_by_courses' => [
        'classname'     => 'mod_certificate_external',
        'methodname'    => 'get_certificates_by_courses',
        'description'   => 'Returns a list of certificate instances...',
        'type'          => 'read',
        'capabilities'  => 'mod/certificate:view',
        'services'      => [MOODLE_OFFICIAL_MOBILE_SERVICE, 'local_mobile'],
    ],
];

Mobile.php supported options

In the previous section, we learned about some of the existing options for handlers configuration. This is the full list of supported options.

Common options

  • delegate (mandatory) — Name of the delegate to register the handler in.
  • method (mandatory) — The method to call to retrieve the main page content.
  • init (optional) — A method to call to retrieve the initialisation JS and the restrictions to apply to the whole handler. It can also return templates that can be used from JavaScript. You can learn more about this in the Initialisation section.
  • restricttocurrentuser (optional) — Only used if the delegate has a isEnabledForUser function. If true, the handler will only be shown for the current user. For more info about displaying the plugin only for certain users, please see Display the plugin only if certain conditions are met.
  • restricttoenrolledcourses (optional) — Only used if the delegate has a isEnabledForCourse function. If true or not defined, the handler will only be shown for courses the user is enrolled in. For more info about displaying the plugin only for certain courses, please see Display the plugin only if certain conditions are met.
  • styles (optional) — An array with two properties: url and version. The URL should point to a CSS file, either using an absolute URL or a relative URL. This file will be downloaded and applied by the app. It's recommended to include styles that will only affect your plugin templates. The version number is used to determine if the file needs to be downloaded again, you should change the version number everytime you change the CSS file.
  • moodlecomponent (optional) — If your plugin supports a component in the app different than the one defined by your plugin, you can use this property to specify it. For example, you can create a local plugin to support a certain course format, activity, etc. The component of your plugin in Moodle would be local_whatever, but in moodlecomponent you can specify that this handler will implement format_whatever or mod_whatever. This property was introduced in the version 3.6.1 of the app.

Options only for CoreMainMenuDelegate

  • displaydata (mandatory):
    • title — A language string identifier that was included in the lang section.
    • icon — The name of an ionic icon. See icons section.
    • class — A CSS class.
  • priority (optional) — Priority of the handler. Higher priority is displayed first. Main Menu plugins are always displayed in the More tab, they cannot be displayed as tabs in the bottom bar.
  • ptrenabled (optional) — Whether to enable pull-to-refresh gesture to refresh page content.

Options only for CoreMainMenuHomeDelegate

  • displaydata (mandatory):
    • title — A language string identifier that was included in the lang section.
    • class — A CSS class.
  • priority (optional) — Priority of the handler. Higher priority is displayed first.
  • ptrenabled (optional) — Whether to enable pull-to-refresh gesture to refresh page content.

Options only for CoreCourseOptionsDelegate

  • displaydata (mandatory):
    • title — A language string identifier that was included in the lang section.
    • class — A CSS class.
  • priority (optional) — Priority of the handler. Higher priority is displayed first.
  • ismenuhandler (optional) — Supported from the 3.7.1 version of the app. Set it to true if you want your plugin to be displayed in the contextual menu of the course instead of in the top tabs. The contextual menu is displayed when you click in the 3-dots button at the top right of the course.
  • ptrenabled (optional) — Whether to enable pull-to-refresh gesture to refresh page content.

Options only for CoreCourseModuleDelegate

  • displaydata (mandatory):
    • icon — Path to the module icon. After Moodle app 4.0, this icon is only used as a fallback, the app will always try to use the theme icon so themes can override icons in the app.
    • class — A CSS class.
  • method (optional) — The function to call to retrieve the main page content. In this delegate the method is optional. If the method is not set, the module won't be clickable.
  • offlinefunctions (optional) — List of functions to call when prefetching the module. It can be a get_content method or a WS. You can filter the params received by the WS. By default, WS will receive these params: courseid, cmid, userid. Other valid values that will be added if they are present in the list of params: courseids (it will receive a list with the courses the user is enrolled in), {component}id (For example, certificateid).
  • downloadbutton (optional) — Whether to display download button in the module. If not defined, the button will be shown if there is any offlinefunction.
  • isresource (optional) — Whether the module is a resource or an activity. Only used if there is any offline function. If your module relies on the contents field, then it should be true.
  • updatesnames (optional) — Only used if there is any offline function. A regular expression to check if there's any update in the module. It will be compared to the result of core_course_check_updates.
  • displayopeninbrowser (optional) — Whether the module should display the "Open in browser" option in the top-right menu. This can be done in JavaScript too: this.displayOpenInBrowser = false;. Supported from the 3.6 version of the app.
  • displaydescription (optional) — Whether the module should display the "Description" option in the top-right menu. This can be done in JavaScript too: this.displayDescription = false;. Supported from the 3.6 version of the app.
  • displayrefresh (optional) — Whether the module should display the "Refresh" option in the top-right menu. This can be done in JavaScript too: this.displayRefresh = false;. Supported from the 3.6 version of the app.
  • displayprefetch (optional) — Whether the module should display the download option in the top-right menu. This can be done in JavaScript too: this.displayPrefetch = false;. Supported from the 3.6 version of the app.
  • displaysize (optional) — Whether the module should display the downloaded size in the top-right menu. This can be done in JavaScript too: this.displaySize = false;. Supported from the 3.6 version of the app.
  • supportedfeatures (optional) — It can be used to specify the supported features of the plugin. Currently the app only uses FEATURE_MOD_ARCHETYPE and FEATURE_NO_VIEW_LINK. It should be an array with features as keys (For example, [FEATURE_NO_VIEW_LINK => true). If you need to calculate this dynamically please see Module plugins: dynamically determine if a feature is supported. Supported from the 3.6 version of the app.
  • coursepagemethod (optional) — If set, this method will be called when the course is rendered and the HTML returned will be displayed in the course page for the module. Please notice the HTML returned should not contain directives or components, only default HTML. Supported from the 3.8 version of the app.
  • ptrenabled (optional) — Whether to enable pull-to-refresh gesture to refresh page content.

Options only for CoreCourseFormatDelegate

  • canviewallsections (optional) — Whether the course format allows seeing all sections in a single page. Defaults to true.
  • displayenabledownload (optional) — Deprecated in the 4.0 app, it's no longer used.
  • displaysectionselector (optional) — Deprecated in the 4.0 app, use displaycourseindex instead.
  • displaycourseindex (optional) — Whether the default course index should be displayed. Defaults to true.

Options only for CoreUserDelegate

  • displaydata (mandatory):
    • title — A language string identifier that was included in the lang section.
    • icon — The name of an ionic icon. See icons section.
    • class — A CSS class.
  • type — The type of the addon. The values accepted are 'newpage' (default) and 'communication'.
  • priority (optional) — Priority of the handler. Higher priority is displayed first.
  • ptrenabled (optional) — Whether to enable pull-to-refresh gesture to refresh page content.

Options only for CoreSettingsDelegate

  • displaydata (mandatory):
    • title — A language string identifier that was included in the lang section.
    • icon — The name of an ionic icon. See icons section.
    • class — A CSS class.
  • priority (optional) — Priority of the handler. Higher priority is displayed first.
  • ptrenabled (optional) — Whether to enable pull-to-refresh gesture to refresh page content.

Options only for AddonMessageOutputDelegate

  • displaydata (mandatory):
    • title — A language string identifier that was included in the lang section.
    • icon — The name of an ionic icon. See icons section.
  • priority (optional) — Priority of the handler. Higher priority is displayed first.
  • ptrenabled (optional) — Whether to enable pull-to-refresh gesture to refresh page content.

Options only for CoreBlockDelegate

  • displaydata (optional):
    • title — A language string identifier that was included in the lang section. If this is not supplied, it will default to 'plugins.block_{block-name}.pluginname', where {block-name} is the name of the block.
    • class — A CSS class. If this is not supplied, it will default to block_{block-name}, where {block-name} is the name of the block.
    • type — Possible values are:
      • "title" — Your block will only display the block title, and when it's clicked it will open a new page to display the block contents (the template returned by the block's method).
      • "prerendered" — Your block will display the content and footer returned by the WebService to get the blocks (for example, core_block_get_course_blocks), so your block's method will never be called.
      • Any other value — Your block will immediately call the method specified in mobile.php and it will use the template to render the block.
  • fallback (optional) — This option allows you to specify a block to use in the app instead of your block. For example, you can make the app display the "My overview" block instead of your block in the app by setting 'fallback' => 'myoverview'. The fallback will only be used if you don't specify a method and the type is different to 'title' or 'prerendered'. Supported from the 3.9.0 version of the app.

Delegates

Delegates can be classified by type of plugin. For more info about type of plugins, please see the Types of plugins section.

Templates generated and downloaded when the user opens the plugins

CoreMainMenuDelegate

You must use this delegate when you want to add new items to the main menu (currently displayed at the bottom of the app).

CoreMainMenuHomeDelegate

You must use this delegate when you want to add new tabs in the home page (by default the app is displaying the "Dashboard" and "Site home" tabs).

CoreCourseOptionsDelegate

You must use this delegate when you want to add new options in a course (Participants or Grades are examples of this type of delegate).

CoreCourseModuleDelegate

You must use this delegate for supporting activity modules or resources.

CoreUserDelegate

You must use this delegate when you want to add additional options in the user profile page in the app.

CoreCourseFormatDelegate

You must use this delegate for supporting course formats. When you open a course from the course list in the mobile app, it will check if there is a CoreCourseFormatDelegate handler for the format that site uses. If so, it will display the course using that handler. Otherwise, it will use the default app course format.

You can learn more about this at the Creating mobile course formats page.

CoreSettingsDelegate

You must use this delegate to add a new option in the settings page.

AddonMessageOutputDelegate

You must use this delegate to support a message output plugin.

CoreBlockDelegate

You must use this delegate to support a block. For example, blocks can be displayed in Site Home, Dashboard and the Course page.

Templates downloaded on login and rendered using JS data

CoreQuestionDelegate

You must use this delegate for supporting question types.

You can learn more about this at the Creating mobile question types page.

CoreQuestionBehaviourDelegate

You must use this delegate for supporting question behaviours.

CoreUserProfileFieldDelegate

You must use this delegate for supporting user profile fields.

AddonModQuizAccessRuleDelegate

You must use this delegate to support a quiz access rule.

AddonModAssignSubmissionDelegate and AddonModAssignFeedbackDelegate

You must use these delegates to support assign submission or feedback plugins.

AddonWorkshopAssessmentStrategyDelegate

You must use this delegate to support a workshop assessment strategy plugin.

Pure JavaScript plugins

These delegates require JavaScript to be supported. See Initialisation for more information.

  • CoreContentLinksDelegate
  • CoreCourseModulePrefetchDelegate
  • CoreFileUploaderDelegate
  • CorePluginFileDelegate
  • CoreFilterDelegate

Available components and directives

Difference between components and directives

A directive is usually represented as an HTML attribute, allows you to extend a piece of HTML with additional information or functionality. Example of directives are: core-auto-focus, *ngIf, and ng-repeat.

Components are also directives, but they are usually represented as an HTML tag and they are used to add custom elements to the app. Example of components are ion-list, ion-item, and core-search-box.

Components and directives are Angular concepts; you can learn more about them and the components come out of the box with Ionic in the following links:

Custom core components and directives

These are some useful custom components and directives that are only available in the Moodle App. Please note that this isn’t the full list of custom components and directives, it’s just an extract of the most common ones.

You can find a full list of components and directives in the source code of the app, within src/core/components and src/core/directives.

core-format-text

This directive formats the text and adds some directives needed for the app to work as it should. For example, it treats all links and all the embedded media so they work fine in the app. If some content in your template includes links or embedded media, please use this directive.

This directive automatically applies core-external-content and core-link to all the links and embedded media.

Data that can be passed to the directive:

  • text (string) — The text to format.
  • siteId (string) — Optional. Site ID to use. If not defined, it will use the id of the current site.
  • component (string) — Optional. Component to use when downloading embedded files.
  • componentId (string|number) — Optional. ID to use in conjunction with the component.
  • adaptImg (boolean) — Optional, defaults to true. Whether to adapt images to screen width.
  • clean (boolean) — Optional, defaults to false. Whether all HTML tags should be removed.
  • singleLine (boolean) — Optional, defaults to false. Whether new lines should be removed to display all the text in single line. Only if clean is true.
  • maxHeight (number) — Optional. Max height in pixels to render the content box. The minimum accepted value is 50. Using this parameter will force display: block to calculate the height better. If you want to avoid this, use class="inline" at the same time to use display: inline-block.


Example usage:

<core-format-text text="<% cm.description %>" component="mod_certificate" componentId="<% cm.id %>"></core-format-text>

core-link

Directive to handle a link. It performs several checks, like checking if the link needs to be opened in the app, and opens the link as it should (without overriding the app).

This directive is automatically applied to all the links and media inside core-format-text.

Data that can be passed to the directive:

  • capture (boolean) — Optional, defaults to false. Whether the link needs to be captured by the app (check if the link can be handled by the app instead of opening it in a browser).
  • inApp (boolean) — Optional, defaults to false. Whether to open in an embedded browser within the app or in the system browser.
  • autoLogin (string) — Optional, defaults to "check". If the link should be open with auto-login. Accepts the following values:
    • "yes" — Always auto-login.
    • "no" — Never auto-login.
    • "check" — Auto-login only if it points to the current site.


Example usage:

<a href="<% cm.url %>" core-link>

core-external-content

Directive to handle links to files and embedded files. This directive should be used in any link to a file or any embedded file that you want to have available when the app is offline.

If a file is downloaded, its URL will be replaced by the local file URL.

This directive is automatically applied to all the links and media inside core-format-text.

Data that can be passed to the directive:

  • siteId (string) — Optional. Site ID to use. If not defined, it will use the id of the current site.
  • component (string) — Optional. Component to use when downloading embedded files.
  • componentId (string|number) — Optional. ID to use in conjunction with the component.


Example usage:

<img src="<% event.iconurl %>" core-external-content component="mod_certificate" componentId="<% event.id %>">

core-user-link

Directive to go to user profile on click. When the user clicks the element where this directive is attached, the right user profile will be opened.

Data that can be passed to the directive:

  • userId (number) — User id to open the profile.
  • courseId (number) — Optional. Course id to show the user info related to that course.


Example usage:

<a ion-item core-user-link userId="<% userid %>">

core-file

Component to handle a remote file. It shows the file name, icon (depending on mime type) and a button to download or refresh it. The user can identify if the file is downloaded or not based on the button.

Data that can be passed to the directive:

  • file (object) — The file. Must have a filename property and either fileurl or url.
  • component (string) — Optional. Component the file belongs to.
  • componentId (string|number) — Optional. ID to use in conjunction with the component.
  • canDelete (boolean) — Optional. Whether the file can be deleted.
  • alwaysDownload (boolean) — Optional. Whether it should always display the refresh button when the file is downloaded. Use it for files that you cannot determine if they're outdated or not.
  • canDownload (boolean) — Optional, defaults to true. Whether file can be downloaded.


Example usage:

<core-file
        [file]="{
            fileurl: '<% issue.url %>',
            filename: '<% issue.name %>',
            timemodified: '<% issue.timemodified %>',
            filesize: '<% issue.size %>'
        }"
        component="mod_certificate"
        componentId="<% cm.id %>">
</core-file>

core-download-file

Directive to allow downloading and opening a file. When the item with this directive is clicked, the file will be downloaded (if needed) and opened.

It is usually recommended to use the core-file component since it also displays the state of the file.

Data that can be passed to the directive:

  • core-download-file (object) — The file to download.
  • component (string) — Optional. Component to link the file to.
  • componentId (string|number) — Optional. Component ID to use in conjunction with the component.


Example usage (a button to download a file):

<ion-button
        [core-download-file]="{
            fileurl: <% issue.url %>,
            timemodified: <% issue.timemodified %>,
            filesize: <% issue.size %>
        }"
        component="mod_certificate"
        componentId="<% cm.id %>">
    {{ 'plugin.mod_certificate.download | translate }}
</ion-button>

core-course-download-module-main-file

Directive to allow downloading and opening the main file of a module.

When the item with this directive is clicked, the whole module will be downloaded (if needed) and its main file opened. This is meant for modules like mod_resource.

This directive must receive either a module or a moduleId. If no files are provided, it will use module.contents.

Data that can be passed to the directive:

  • module (object) — Optional, required if module is not supplied. The module object.
  • moduleId (number) — Optional, required if module is not supplied. The module ID.
  • courseId (number) — The course ID the module belongs to.
  • component (string) — Optional. Component to link the file to.
  • componentId (string|number) — Optional, defaults to the same value as moduleId. Component ID to use in conjunction with the component.
  • files (object[]) — Optional. List of files of the module. If not provided, uses module.contents.


Example usage:

<ion-button expand="block" core-course-download-module-main-file moduleId="<% cmid %>" 
        courseId="<% certificate.course %>" component="mod_certificate"
        [files]="[{
            fileurl: '<% issue.fileurl %>',
            filename: '<% issue.filename %>',
            timemodified: '<% issue.timemodified %>',
            mimetype: '<% issue.mimetype %>',
        }]">
    {{ 'plugin.mod_certificate.getcertificate' | translate }}
</ion-button>

core-navbar-buttons

Component to add buttons to the app's header without having to place them inside the header itself. Using this component in a site plugin will allow adding buttons to the header of the current page.

If this component indicates a position (start/end), the buttons will only be added if the header has some buttons in that position. If no start/end is specified, then the buttons will be added to the first <ion-buttons> found in the header.

You can use the [hidden] input to hide all the inner buttons if a certain condition is met.

Example usage:

<core-navbar-buttons end>
    <ion-button (click)="action()">
        <ion-icon slot="icon-only" name="funnel"></ion-icon>
    </ion-button>
</core-navbar-buttons>

You can also use this to add options to the context menu, for example:

<core-navbar-buttons>
    <core-context-menu>
        <core-context-menu-item
                [priority]="500" content="Nice boat" (action)="boatFunction()"
                iconAction="boat">
        </core-context-menu-item>
    </core-context-menu>
</core-navbar-buttons>

Using 'font' icons with ion-icon

Font icons are widely used on the app and Moodle LMS website. In order to support font awesome icons. We've added a directive that uses prefixes on the name attribute to use different font icons.


Example of usage to show icon "pizza-slice" from font-awesome regular library:

<ion-icon name="fas-pizza-slice"></ion-icon>

We encourage the use of font-awesome icons to match the appearance from the LMS website version.

Specific component and directives for plugins

These are component and directives created specifically for supporting Moodle plugins.

core-site-plugins-new-content

Directive to display a new content when clicked. This new content can be displayed in a new page or in the current page (only if the current page is already displaying a site plugin content).

Data that can be passed to the directive:

  • component (string) — The component of the new content.
  • method (string) — The method to get the new content.
  • args (object) — The params to get the new content.
  • preSets (object) — Extra options for the WS call of the new content: whether to use cache or not, etc. This field was added in v3.6.0.
  • title (string) — The title to display with the new content. Only if samePage is false.
  • samePage (boolean) — Optional, defaults to false. Whether to display the content in same page or open a new one.
  • useOtherData (any) — Whether to include otherdata (from the get_content WS call) in the arguments for the new get_content call. If not supplied, no other data will be added. If supplied but empty (null, false or an empty string) all the otherdata will be added. If it’s an array, it will only copy the properties whose names are in the array. Please notice that doing [useOtherData]="" is the same as not supplying it, so nothing will be copied. Also, objects or arrays in otherdata will be converted to a JSON encoded string.
  • form (string) — ID or name to identify a form in the template. The form will be obtained from document.forms. If supplied and a form is found, the form data will be retrieved and sent to the new get_content WS call. If your form contains an ion-radio, ion-checkbox or ion-select, please see Values of ion-radio, ion-checkbox or ion-select aren't sent to my WS.


Let's see some examples.

A button to go to a new content page:

<ion-button core-site-plugins-new-content 
        title="<% certificate.name %>" component="mod_certificate" 
        method="mobile_issues_view" [args]="{cmid: <% cmid %>, courseid: <% courseid %>}">
     {{ 'plugin.mod_certificate.viewissued' | translate }}
</ion-button>

A button to load new content in current page using userid from otherdata:

<ion-button core-site-plugins-new-content
        component="mod_certificate" method="mobile_issues_view"
        [args]="{cmid: <% cmid %>, courseid: <% courseid %>}" samePage="true" [useOtherData]="['userid']">
    {{ 'plugin.mod_certificate.viewissued' | translate }}
</ion-button>

core-site-plugins-call-ws

Directive to call a WS when the element is clicked. The action to do when the WS call is successful depends on the provided data: display a message, go back or refresh current view.

If you want to load a new content when the WS call is done, please see core-site-plugins-call-ws-new-content.

Data that can be passed to the directive:

  • name (string) — The name of the WS to call.
  • params (object) — The params for the WS call.
  • preSets (object) — Extra options for the WS call: whether to use cache or not, etc.
  • useOtherDataForWS (any) — Whether to include otherdata (from the get_content WS call) in the params for the WS call. If not supplied, no other data will be added. If supplied but empty (null, false or an empty string) all the otherdata will be added. If it’s an array, it will only copy the properties whose names are in the array. Please notice that [useOtherDataForWS]="" is the same as not supplying it, so nothing will be copied. Also, objects or arrays in otherdata will be converted to a JSON encoded string.
  • form (string) — ID or name to identify a form in the template. The form will be obtained from document.forms. If supplied and a form is found, the form data will be retrieved and sent to the new get_content WS call. If your form contains an ion-radio, ion-checkbox or ion-select, please see Values of ion-radio, ion-checkbox or ion-select aren't sent to my WS.
  • confirmMessage (string) — Message to confirm the action when theuser clicks the element. If not supplied, no confirmation will be requested. If supplied but empty, "Are you sure?" will be used.
  • showError (boolean) — Optional, defaults to true. Whether to show an error message if the WS call fails. This field was added in 3.5.2.
  • successMessage (string) — Message to show on success. If not supplied, no message. If supplied but empty, defaults to "Success".
  • goBackOnSuccess (boolean) — Whether to go back if the WS call is successful.
  • refreshOnSuccess (boolean) — Whether to refresh the current view if the WS call is successful.
  • onSuccess (Function) — A function to call when the WS call is successful (HTTP call successful and no exception returned). This field was added in 3.5.2.
  • onError (Function) — A function to call when the WS call fails (HTTP call fails or an exception is returned). This field was added in 3.5.2.
  • onDone (Function) — A function to call when the WS call finishes (either success or fail). This field was added in 3.5.2.


Let's see some examples.

A button to send some data to the server without using cache, displaying default messages and refreshing on success:

<ion-button core-site-plugins-call-ws
        name="mod_certificate_view_certificate" [params]="{certificateid: <% certificate.id %>}"
        [preSets]="{getFromCache: 0, saveToCache: 0}" confirmMessage successMessage
        refreshOnSuccess="true">
    {{ 'plugin.mod_certificate.senddata' | translate }}
</ion-button>

A button to send some data to the server using cache without confirming, going back on success and using userid from otherdata:

<ion-button core-site-plugins-call-ws
        name="mod_certificate_view_certificate" [params]="{certificateid: <% certificate.id %>}"
        goBackOnSuccess="true" [useOtherData]="['userid']">
     {{ 'plugin.mod_certificate.senddata' | translate }}
</ion-button>

Same as the previous example, but implementing custom JS code to run on success:

<ion-button core-site-plugins-call-ws
        name="mod_certificate_view_certificate" [params]="{certificateid: <% certificate.id %>}"
        [useOtherData]="['userid']" (onSuccess)="certificateViewed($event)">
     {{ 'plugin.mod_certificate.senddata' | translate }}
</ion-button>

In the JavaScript side, you would do:

this.certificateViewed = function(result) {
    // Code to run when the WS call is successful.
};

core-site-plugins-call-ws-new-content

Directive to call a WS when the element is clicked and load a new content passing the WS result as arguments. This new content can be displayed in a new page or in the same page (only if current page is already displaying a site plugin content).

If you don't need to load some new content when done, please see core-site-plugins-call-ws.

Data that can be passed to the directive:

  • name (string) — The name of the WS to call.
  • params (object) — The parameters for the WS call.
  • preSets (object) — Extra options for the WS call: whether to use cache or not, etc.
  • useOtherDataForWS (any) — Whether to include otherdata (from the get_content WS call) in the params for the WS call. If not supplied, no other data will be added. If supplied but empty (null, false or an empty string) all the otherdata will be added. If it’s an array, it will only copy the properties whose names are in the array. Please notice that [useOtherDataForWS]="" is the same as not supplying it, so nothing will be copied. Also, objects or arrays in otherdata will be converted to a JSON encoded string.
  • form (string) — ID or name to identify a form in the template. The form will be obtained from document.forms. If supplied and a form is found, the form data will be retrieved and sent to the new get_content WS call. If your form contains an ion-radio, ion-checkbox or ion-select, please see Values of ion-radio, ion-checkbox or ion-select aren't sent to my WS.
  • confirmMessage (string) — Message to confirm the action when theuser clicks the element. If not supplied, no confirmation will be requested. If supplied but empty, "Are you sure?" will be used.
  • showError (boolean) — Optional, defaults to true. Whether to show an error message if the WS call fails. This field was added in 3.5.2.
  • component (string) — The component of the new content.
  • method (string) — The method to get the new content.
  • args (object) — The parameters to get the new content.
  • title (string) — The title to display with the new content. Only if samePage is false.
  • samePage (boolean) — Optional, defaults to false. Whether to display the content in the same page or open a new one.
  • useOtherData (any) — Whether to include otherdata (from the get_content WS call) in the arguments for the new get_content call. The format is the same as in useOtherDataForWS.
  • jsData (any) — JS variables to pass to the new page so they can be used in the template or JS. If true is supplied instead of an object, all initial variables from current page will be copied. This field was added in 3.5.2.
  • newContentPreSets (object) — Extra options for the WS call of the new content: whether to use cache or not, etc. This field was added in 3.6.0.
  • onSuccess (Function) — A function to call when the WS call is successful (HTTP call successful and no exception returned). This field was added in 3.5.2.
  • onError (Function) — A function to call when the WS call fails (HTTP call fails or an exception is returned). This field was added in 3.5.2.
  • onDone (Function) — A function to call when the WS call finishes (either success or fail). This field was added in 3.5.2.


Let's see some examples.

A button to get some data from the server without using cache, showing default confirm and displaying a new page:

<ion-button core-site-plugins-call-ws-new-content
        name="mod_certificate_get_issued_certificates" [params]="{certificateid: <% certificate.id %>}"
        [preSets]="{getFromCache: 0, saveToCache: 0}" confirmMessage
        title="<% certificate.name %>" component="mod_certificate"
        method="mobile_issues_view" [args]="{cmid: <% cmid %>, courseid: <% courseid %>}">
    {{ 'plugin.mod_certificate.getissued' | translate }}
</ion-button>

A button to get some data from the server using cache, without confirm, displaying new content in same page and using userid from otherdata:

<ion-button core-site-plugins-call-ws-new-content
        name="mod_certificate_get_issued_certificates" [params]="{certificateid: <% certificate.id %>}"
        component="mod_certificate" method="mobile_issues_view"
        [args]="{cmid: <% cmid %>, courseid: <% courseid %>}"
        samePage="true" [useOtherData]="['userid']">
    {{ 'plugin.mod_certificate.getissued' | translate }}
</ion-button>


Same as the previous example, but implementing a custom JS code to run on success:

<ion-button core-site-plugins-call-ws-new-content
        name="mod_certificate_get_issued_certificates" [params]="{certificateid: <% certificate.id %>}"
        component="mod_certificate" method="mobile_issues_view"
        [args]="{cmid: <% cmid %>, courseid: <% courseid %>}"
        samePage="true" [useOtherData]="['userid']" (onSuccess)="callDone($event)">
    {{ 'plugin.mod_certificate.getissued' | translate }}
</ion-button>

In the JavaScript side, you would do:

this.callDone = function(result) {
    // Code to run when the WS call is successful.
};

core-site-plugins-call-ws-on-load

Directive to call a WS as soon as the template is loaded. This directive is meant for actions to do in the background, like calling logging Web Services.

If you want to call a WS when the user clicks on a certain element, please see core-site-plugins-call-ws.

  • name (string) — The name of the WS to call.
  • params (object) — The parameters for the WS call.
  • preSets (object) — Extra options for the WS call: whether to use cache or not, etc.
  • useOtherDataForWS (any) — Whether to include otherdata (from the get_content WS call) in the params for the WS call. If not supplied, no other data will be added. If supplied but empty (null, false or an empty string) all the otherdata will be added. If it’s an array, it will only copy the properties whose names are in the array. Please notice that [useOtherDataForWS]="" is the same as not supplying it, so nothing will be copied. Also, objects or arrays in otherdata will be converted to a JSON encoded string.
  • form (string) — ID or name to identify a form in the template. The form will be obtained from document.forms. If supplied and a form is found, the form data will be retrieved and sent to the new get_content WS call. If your form contains an ion-radio, ion-checkbox or ion-select, please see Values of ion-radio, ion-checkbox or ion-select aren't sent to my WS.
  • onSuccess (Function) — A function to call when the WS call is successful (HTTP call successful and no exception returned). This field was added in 3.5.2.
  • onError (Function) — A function to call when the WS call fails (HTTP call fails or an exception is returned). This field was added in 3.5.2.
  • onDone (Function) — A function to call when the WS call finishes (either success or fail). This field was added in 3.5.2.


Example usage:

<span core-site-plugins-call-ws-on-load
        name="mod_certificate_view_certificate" [params]="{certificateid: <% certificate.id %>}"
        [preSets]="{getFromCache: 0, saveToCache: 0}" (onSuccess)="callDone($event)">
</span>

In the JavaScript side, you would do:

this.callDone = function(result) {
    // Code to run when the WS call is successful.
};

Advanced features

Display the plugin only if certain conditions are met

You might want to display your plugin in the mobile app only if certain dynamic conditions are met, so the plugin would be displayed only for some users. This can be achieved using the initialisation method (for more info, please see the Initialisation section ahead).

All initialisation methods are called as soon as your plugin is retrieved. If you don't want your plugin to be displayed for the current user, then you should return the following in the initialisation method (only for Moodle site 3.8 and onwards):

return ['disabled' => true];

If the Moodle site is older than 3.8, then the initialisation method should return this instead:

return ['javascript' => 'this.HANDLER_DISABLED'];

On the other hand, you might want to display a plugin only for certain courses (CoreCourseOptionsDelegate) or only if the user is viewing certain users' profiles (CoreUserDelegate). This can be achieved with the initialisation method too.

In the initialisation method you can return a restrict property with two fields in it: courses and users. If you return a list of courses IDs in this property, then your plugin will only be displayed when the user views any of those courses. In the same way, if you return a list of user IDs then your plugin will only be displayed when the user views any of those users' profiles.

Using otherdata

The values returned by the functions in otherdata are added to a variable so they can be used both in JavaScript and in templates. The otherdata returned by an initialisation call is added to a variable named INIT_OTHERDATA, while the otherdata returned by a get_content WS call is added to a variable named CONTENT_OTHERDATA.

The otherdata returned by an initialisation call will be passed to the JS and template of all the get_content calls in that handler. The otherdata returned by a get_content call will only be passed to the JS and template returned by that get_content call.

This means that, in your JavaScript, you can access and use the data like this:

this.CONTENT_OTHERDATA.myVar;

And in the template you could use it like this:

{{ CONTENT_OTHERDATA.myVar }}

myVar is the name we put to one of our variables, it can be any name that you want. In the example above, this is the otherdata returned by the PHP method:

['myVar' => 'Initial value']

Example

In our plugin, we want to display an input text with a certain initial value. When the user clicks a button, we want the value in the input to be sent to a certain Web Service. This can be done using otherdata.

We will return the initial value of the input in the otherdata of our PHP method:

'otherdata' => ['myVar' => 'My initial value'],

Then in the template we will use it like this:

<ion-item text-wrap>
    <ion-label position="stacked">{{ 'plugin.mod_certificate.textlabel | translate }}</ion-label>
    <ion-input type="text" [(ngModel)]="CONTENT_OTHERDATA.myVar"></ion-input>
</ion-item>
<ion-item>
    <ion-label><ion-button expand="block" color="light" core-site-plugins-call-ws name="mod_certificate_my_webservice" [useOtherDataForWS]="['myVar']">
        {{ 'plugin.mod_certificate.send | translate }}
    </ion-button></ion-label>
</ion-item>

In the example above, we are creating an input text and we use [(ngModel)] to use the value in myVar as the initial value and to store the changes in the same myVar variable. This means that the initial value of the input will be "My initial value", and if the user changes the value of the input these changes will be applied to the myVar variable. This is called 2-way data binding in Angular.

Then we add a button to send this data to a WS, and for that we use the core-site-plugins-call-ws directive. We use the useOtherDataForWS attribute to specify which variable from otherdata we want to send to our WebService. So if the user enters "A new value" in the input and then clicks the button, it will call the WebService mod_certificate_my_webservice and will send as a parameter ['myVar' => 'A new value'].

We can also achieve the same result using the params attribute of the core-site-plugins-call-ws directive instead of using useOtherDataForWS:

<ion-button expand="block" color="light" core-site-plugins-call-ws 
        name="mod_certificate_my_webservice" [params]="{myVar: CONTENT_OTHERDATA.myVar}">
    {{ 'plugin.mod_certificate.send | translate }}
</ion-button>

The Web Service call will be exactly the same with both versions.

Notice that this example could be done without using otherdata too, using the form input of the core-site-plugins-call-ws directive.

Running JS code after a content template has loaded

When you return JavaScript code from a handler function using the javascript array key, this code is executed immediately after the web service call returns, which may be before the returned template has been rendered into the DOM.

If your code needs to run after the DOM has been updated, you can use setTimeout to call it. For example:

return [
    'template' => [
        // ...
    ],
    'javascript' => 'setTimeout(function() { console.log("DOM is available now"); });',
    'otherdata' => '',
    'files' => [],
];

Notice that if you wanted to write a lot of code here, you might be better off putting it in a function defined in the response from an initialisation template, so that it does not get loaded again with each page of content.

JS functions visible in the templates

The app provides some JavaScript functions that can be used from the templates to update, refresh or view content. These are the functions:

  • openContent(title: string, args: any, component?: string, method?: string) — Open a new page to display some new content. You need to specify the title of the new page and the args to send to the method. If component and method aren't provided, it will use the same as in the current page.
  • refreshContent(showSpinner = true) — Refresh the current content. By default, it will display a spinner while refreshing. If you don't want it to be displayed, you should pass false as a parameter.
  • updateContent(args: any, component?: string, method?: string) — Refresh the current content using different parameters. You need to specify the args to send to the method. If component and method aren't provided, it will use the same as in the current page.

Examples

Group selector

Imagine we have an activity that uses groups and we want to let the user select which group they want to see. A possible solution would be to return all the groups in the same template (hidden), and then show the group user selects. However, we can make it more dynamic and return only the group the user is requesting.

To do so, we'll use a drop down to select the group. When the user selects a group using this drop down, we'll update the page content to display the new group.

The main difficulty in this is to tell the view which group needs to be selected when the view is loaded. There are 2 ways to do it: using plain HTML or using Angular's ngModel.

Using plain HTML

We need to add a selected attribute to the option that needs to be selected. To do so, we need to pre-caclulate the selected option in the PHP code:

$groupid = empty($args->group) ? 0 : $args->group; // By default, group 0.
$groups = groups_get_activity_allowed_groups($cm, $user->id);

// Detect which group is selected.
foreach ($groups as $gid=>$group) {
    $group->selected = $gid === $groupid;
}

$data = [
    'cmid' => $cm->id,
    'courseid' => $args->courseid,
    'groups' => $groups,
];

return [
    'templates' => [
        [
            'id' => 'main',
            'html' => $OUTPUT->render_from_template('mod_certificate/mobile_view_page', $data),
        ],
    ],
];

In the code above, we're retrieving the groups the user can see and then we're adding a selected boolean to each one to determine which one needs to be selected in the drop down. Finally, we pass the list of groups to the template.

In the template, we display the drop down like this:

<ion-select (ionChange)="updateContent({cmid: <% cmid %>, courseid: <% courseid %>, group: $event})" interface="popover">
    <%#groups%>
        <ion-option value="<% id %>" <%#selected%>selected<%/selected%> ><% name %></ion-option>
    <%/groups%>
</ion-select>

The ionChange function will be called every time the user selects a different group with the drop down. We're using the updateContent function to update the current view using the new group. $event is an Angular variable that will have the selected value (in our case, the group ID that was just selected). This is enough to make the group selector work.

Using ngModel

ngModel is an Angular directive that allows storing the value of a certain input or select in a JavaScript variable, and also the opposite way: tell the input or select which value to set. The main problem is that we cannot initialise a JavaScript variable from the template, so we'll use otherdata.

In the PHP function we'll return the group that needs to be selected in the otherdata array:

$groupid = empty($args->group) ? 0 : $args->group; // By default, group 0.
$groups = groups_get_activity_allowed_groups($cm, $user->id);

// ...

return [
    'templates' => [
        [
            'id' => 'main',
            'html' => $OUTPUT->render_from_template('mod_certificate/mobile_view_page', $data),
        ],
    ],
    'otherdata' => [
        'group' => $groupid,
    ],
];

In the example above we don't need to iterate over the groups array like in the plain HTML example. However, now we're returning the group id in the otherdata array. As it's explained in the Using otherdata section, this otherdata is visible in the templates inside a variable named CONTENT_OTHERDATA. So in the template we'll use this variable like this:

<ion-select [(ngModel)]="CONTENT_OTHERDATA.group"
        (ionChange)="updateContent({cmid: <% cmid %>, courseid: <% courseid %>, group: CONTENT_OTHERDATA.group})"
        interface="popover">
    <%#groups%>
        <ion-option value="<% id %>"><% name %></ion-option>
    <%/groups%>
</ion-select>

Use the rich text editor

The rich text editor included in the app requires a FormControl to work. You can use the FormBuilder library to create this control (or to create a whole FormGroup if you prefer).

With the following JavaScript you'll be able to create a FormControl:

this.control = this.FormBuilder.control(this.CONTENT_OTHERDATA.rte);

In the example above we're using a value returned in OTHERDATA as the initial value of the rich text editor, but you can use whatever you want.

Then you need to pass this control to the rich text editor in your template:

<ion-item>
    <core-rich-text-editor item-content [control]="control" placeholder="Enter your text here" name="rte_answer">
    </core-rich-text-editor>
</ion-item>

Finally, there are several ways to send the value in the rich text editor to a Web Service to save it. This is one of the simplest options:

<ion-button expand="block" type="submit" core-site-plugins-call-ws name="my_webservice" [params]="{rte: control.value}" ...

As you can see, we're passing the value of the rich text editor as a parameter to our Web Service.

Initialisation

All handlers can specify an init method in the mobile.php file. This method is meant to return some JavaScript code that needs to be executed as soon as the plugin is retrieved.

When the app retrieves all the handlers, the first thing it will do is call the tool_mobile_get_content Web Service with the initialisation method. This WS call will only receive the default arguments.

The app will immediately execute the JavaScript code returned by this WS call. This JavaScript can be used to manually register your handlers in the delegates you want, without having to rely on the default handlers built based on the mobile.php data.

The templates returned by this method will be added to a INIT_TEMPLATES variable that will be passed to all the JavaScript code of that handler. This means that the JavaScript returned by the initialisation method or the main method can access any of the templates HTML like this:

this.INIT_TEMPLATES['main'];

In this case, main is the ID of the template we want to use.

The same happens with the otherdata returned by the initialisation method, it is added to an INIT_OTHERDATA variable.

The restrict field returned by this call will be used to determine if your handler is enabled or not. For example, if your handler is for the delegate CoreCourseOptionsDelegate and you return a list of course ids in restrict.courses, then your handler will only be enabled in the courses you returned. This only applies to the default handlers, if you register your own handler using the JavaScript code then you should check yourself if the handler is enabled.

Finally, if you return an object in this initialisation JavaScript code, all the properties of that object will be passed to all the JavaScript code of that handler so you can use them when the code is run. For example, if your JavaScript code does something like this:

var result = {
    MyAddonClass: new MyAddonClass()
};

result;

Then, for the rest of JavaScript code of your handler (for example, the main method) you can use this variable like this:

this.MyAddonClass

Examples

Link handlers

A link handler allows you to decide what to do when a link with a certain URL is clicked. This is useful, for example, to open your plugin page when a link to your plugin is clicked.

After the 4.0 version, the Moodle app automatically creates two link handlers for module plugins, you don't need to create them in your plugin's Javascript code anymore:

  • A handler to treat links to mod/pluginanme/view.php?id=X. When this link is clicked, it will open your module in the app.
  • A handler to treat links to mod/pluginname/index.php?id=X. When this link is clicked, it will open a page in the app listing all the modules of your type inside a certain course.


Link handlers have some advanced features that allow you to change how links behave under different conditions.

Patterns

You can define a Regular Expression pattern to match certain links. This will apply the handler only to links that match the pattern.

class AddonModFooLinkHandler extends this.CoreContentLinksHandlerBase {

    constructor() {
        super();

        this.pattern = RegExp('\/mod\/foo\/specialpage.php');
    }

}
Priority

Multiple link handlers may apply to a given link. You can define the order of precedence by setting the priority; the handler with the highest priority will be used.

All default handlers have a priority of 0, so 1 or higher will override the default.

class AddonModFooLinkHandler extends this.CoreContentLinksHandlerBase {

    constructor() {
        super();

        this.priority = 1;
    }

}
Multiple actions

Once a link has been matched, the handler's getActions() method determines what the link should do. This method has access to the URL and its parameters.

Different actions can be returned depending on different conditions.

class AddonModFooLinkHandler extends this.CoreContentLinksHandlerBase {

    getActions(siteIds, url, params) {
        return [
            {
                action: function(siteId, navCtrl) {
                    // The actual behaviour of the link goes here.
                },
                sites: [
                    // ...
                ],
            },
            {
                // ...
            },
        ];
    }

}

Once handlers have been matched for a link, the actions will be fetched for all the matching handlers, in priorty order. The first valid action will be used to open the link.

If your handler is matched with a link, but a condition assessed in the getActions() method means you want to revert to the next highest priorty handler, you can invalidate your action by settings its sites propety to an empty array.

Complex example

This will match all URLs containing /mod/foo/, and force those with an id parameter that's not in the supportedModFoos array to open in the user's browser, rather than the app.

const that = this;
const supportedModFoos = [...];

class AddonModFooLinkHandler extends this.CoreContentLinksHandlerBase {

    constructor() {
        super();

        this.pattern = new RegExp('\/mod\/foo\/');
        this.name = 'AddonModFooLinkHandler';
        this.priority = 1;
    }

    getActions(siteIds, url, params) {     
        const action = {
            action() {
                that.CoreUtilsProvider.openInBrowser(url);
            },
        };

        if (supportedModFoos.indexOf(parseInt(params.id)) !== -1) {
            action.sites = [];
        }

        return [action];
    }

}

this.CoreContentLinksDelegate.registerHandler(new AddonModFooLinkHandler());
Module prefetch handler

The CoreCourseModuleDelegate handler allows you to define a list of offline functions to prefetch a module. However, you might want to create your own prefetch handler to determine what needs to be downloaded. For example, you might need to chain WS calls (pass the result of a WS call to the next one), and this cannot be done using offline functions.

Here’s an example on how to create a prefetch handler using the initialisation JS:

// Create a class that extends from CoreCourseActivityPrefetchHandlerBase.
class AddonModCertificateModulePrefetchHandler extends CoreCourseActivityPrefetchHandlerBase {

    constructor() {
        super();

        this.name = 'AddonModCertificateModulePrefetchHandler';
        this.modName = 'certificate';

        // This must match the plugin identifier from db/mobile.php,
        // otherwise the download link in the context menu will not update correctly.
        this.component = 'mod_certificate';
        this.updatesNames = /^configuration$|^.*files$/;
    }

    // Override the prefetch call.
    prefetch(module, courseId, single, dirPath) {
        return this.prefetchPackage(module, courseId, single, prefetchCertificate);
    }

}

function prefetchCertificate(module, courseId, single, siteId) {
    // Perform all the WS calls.
    // You can access most of the app providers using that.ClassName. E.g. that.CoreWSProvider.call().
}

this.CoreCourseModulePrefetchDelegate.registerHandler(new AddonModCertificateModulePrefetchHandler());

One relatively simple full example is where you have a function that needs to work offline, but it has an additional argument other than the standard ones. You can imagine for this an activity like the book module, where it has multiple pages for the same cmid. The app will not automatically work with this situation — it will call the offline function with the standard arguments only — so you won't be able to prefetch all the possible parameters.

To deal with this, you need to implement a web service in your Moodle component that returns the list of possible extra arguments, and then you can call this web service and loop around doing the same thing the app does when it prefetches the offline functions. Here is an example from a third-party module (showing only the actual prefetch function, the rest of the code is as above) where there are multiple values of a custom section parameter for the mobile function mobile_document_view:

function prefetchOucontent(module, courseId, single, siteId) {
    var component = 'mod_oucontent';

    // Get the site, first.
    return that.CoreSitesProvider.getSite(siteId).then(function(site) {
        // Read the list of pages in this document using a web service.
        return site.read('mod_oucontent_get_page_list', {'cmid': module.id}).then(function(response) {
            var promises = [];

            // For each page, read and process the page - this is a copy of logic in the app at
            // siteplugins.ts (prefetchFunctions), but modified to add the custom argument.
            for(var i = 0; i < response.length; i++) {
                var args = {
                    courseid: courseId,
                    cmid: module.id,
                    userid: site.getUserId()
                };
                if (response[i] !== '') {
                    args.section = response[i];
                }

                promises.push(that.CoreSitePluginsProvider.getContent(
                        component, 'mobile_document_view', args).then(
                        function(result) {
                            var subPromises = [];
                            if (result.files && result.files.length) {
                                subPromises.push(that.CoreFilepoolProvider.downloadOrPrefetchFiles(
                                        site.id, result.files, true, false, component, module.id));
                            }
                            return Promise.all(subPromises);
                        }));
            }

            return Promise.all(promises);
        });
    });
}
Single activity course format

In the following example, the value of INIT_TEMPLATES['main'] is:

<core-dynamic-component [component]="componentClass" [data]="data"></core-dynamic-component>

This template is returned by the initialisation method. And this is the JavaScript code returned:

var that = this;

class AddonSingleActivityFormatComponent {

    constructor() {
        this.data = {};
    }

    ngOnChanges(changes) {
        var self = this;

        if (this.course && this.sections && this.sections.length) {
            var module = this.sections[0] && this.sections[0].modules && this.sections[0].modules[0];
            if (module && !this.componentClass) {
                that.CoreCourseModuleDelegate.getMainComponent(that.Injector, this.course, module).then((component) => {
                    self.componentClass = component || that.CoreCourseUnsupportedModuleComponent;
                });
            }

            this.data.courseId = this.course.id;
            this.data.module = module;
        }
    }

    doRefresh(refresher, done) {
        return Promise.resolve(this.dynamicComponent.callComponentFunction("doRefresh", [refresher, done]));
    }

}

class AddonSingleActivityFormatHandler {
    
    constructor() {
        this.name = 'singleactivity';
    }

    isEnabled() {
        return true;
    }

    canViewAllSections() {
        return false;
    }

    getCourseTitle(course, sections) {
        if (sections && sections[0] && sections[0].modules && sections[0].modules[0]) {
            return sections[0].modules[0].name;
        }

        return course.fullname || '';
    }

    displayEnableDownload() {
        return false;
    }

    displaySectionSelector() {
        return false;
    }

    getCourseFormatComponent() {
        return that.CoreCompileProvider.instantiateDynamicComponent(that.INIT_TEMPLATES['main'], AddonSingleActivityFormatComponent);
    }

}

this.CoreCourseFormatDelegate.registerHandler(new AddonSingleActivityFormatHandler());

Using the JavaScript API

The JavaScript API is only supported by the delegates specified in the Templates downloaded on login and rendered using JS data section. This API allows you to override any of the functions of the default handler.

The method specified in a handler registered in the CoreUserProfileFieldDelegate will be called immediately after the initialisation method, and the JavaScript returned by this method will be run. If this JavaScript code returns an object with certain functions, these functions will override the ones in the default handler.

For example, if the JavaScript returned by the method returns something like this:

var result = {
    getData: function(field, signup, registerAuth, formValues) {
        // ...
    }
};
result;

The the getData function of the default handler will be overridden by the returned getData function.

The default handler for CoreUserProfileFieldDelegate only has 2 functions: getComponent and getData. In addition, the JavaScript code can return an extra function named componentInit that will be executed when the component returned by getComponent is initialised.

Here’s an example on how to support the text user profile field using this API:

var that = this;

var result = {
    componentInit: function() {
        if (this.field && this.edit && this.form) {
            this.field.modelName = 'profile_field_' + this.field.shortname;

            if (this.field.param2) {
                this.field.maxlength = parseInt(this.field.param2, 10) || '';
            }

            this.field.inputType = that.CoreUtilsProvider.isTrueOrOne(this.field.param3) ? 'password' : 'text';

            var formData = {
                value: this.field.defaultdata,
                disabled: this.disabled,
            };

            this.form.addControl(this.field.modelName,
                that.FormBuilder.control(formData, this.field.required && !this.field.locked ? that.Validators.required : null));
        }
    },
    getData: function(field, signup, registerAuth, formValues) {
        var name = 'profile_field_' + field.shortname;

        return {
            type: "text",
            name: name,
            value: that.CoreTextUtilsProvider.cleanTags(formValues[name]),
        };
    }
};

result;

Translate dynamic strings

If you wish to have an element that displays a localised string based on value from your template you can doing something like:

<ion-card>
    <ion-card-content>
        {{ 'plugin.mod_myactivity.<% status %>' | translate }}
    </ion-card-content>
</ion-card>

This could save you from having to write something like when only one value should be displayed:

<ion-card>
    <ion-card-content>
        <%#isedting%>{{ 'plugin.mod_myactivity.editing' | translate }}<%/isediting%>
        <%#isopen%>{{ 'plugin.mod_myactivity.open' | translate }}<%/isopen%>
        <%#isclosed%>{{ 'plugin.mod_myactivity.closed' | translate }}<%/isclosed%>
    </ion-card-content>
</ion-card>

Using strings with dates

If you have a string that you wish to pass a formatted date, for example in the Moodle language file you have:

$string['strwithdate'] = 'This string includes a date of {$a->date} in the middle of it.';

You can localise the string correctly in your template using something like the following:

{{ 'plugin.mod_myactivity.strwithdate' | translate: {$a: { date: <% timestamp %> * 1000 | coreFormatDate: "dffulldate" } } }}

A Unix timestamp must be multiplied by 1000 as the Mobile App expects millisecond timestamps, whereas Unix timestamps are in seconds.

Support push notification clicks

If your plugin sends push notifications to the app, you might want to open a certain page in the app when the notification is clicked. There are several ways to achieve this.

The easiest way is to include a contexturl in your notification. When the notification is clicked, the app will try to open the contexturl.

Please notice that the contexturl will also be displayed in web. If you want to use a specific URL for the app, different than the one displayed in web, you can do so by returning a customdata array that contains an appurl property:

$notification->customdata = [
    'appurl' => $myurl->out(),
];

In both cases you will have to create a link handler to treat the URL. For more info on how to create the link handler, please see how to create an advanced link handler.

If you want to do something that only happens when the notification is clicked, not when the link is clicked, you'll have to implement a push click handler yourself. The way to create it is similar to creating an advanced link handler, but you'll have to use CorePushNotificationsDelegate and your handler will have to implement the properties and functions defined in the CorePushNotificationsClickHandler interface.

Implement a module similar to mod_label

In Moodle 3.8 or higher, if your plugin doesn't support FEATURE_NO_VIEW_LINK and you don't specify a coursepagemethod then the module will only display the module description in the course page and it won't be clickable in the app, just like mod_label. You can decide if you want the module icon to be displayed or not (if you don't want it to be displayed, then don't define it in displaydata).

However, if your plugin needs to work in previous versions of Moodle or you want to display something different than the description then you need a different approach.

If your plugin wants to render something in the course page instead of just the module name and description you should specify the coursepagemethod property in mobile.php. The template returned by this method will be rendered in the course page. Please notice the HTML returned should not contain directives or components, only plain HTML.

If you don't want your module to be clickable then you just need to remove method from mobile.php. With these 2 changes you can have a module that behaves like mod_label in the app.

Use Ionic navigation lifecycle functions

Ionic let pages define some functions that will be called when certain navigation lifecycle events happen. For more info about these functions, see Ionic's documentation.

You can define these functions in your plugin javascript:

this.ionViewWillLeave = function() {
    // ...
};

In addition to that, you can also implement canLeave to use Angular route guards:

this.canLeave = function() {
    // ...
};

So for example you can make your plugin ask for confirmation if the user tries to leave the page when he has some unsaved data.

Module plugins: dynamically determine if a feature is supported

In Moodle you can specify if your plugin supports a certain feature, like FEATURE_NO_VIEW_LINK. If your plugin will always support or not a certain feature, then you can use the supportedfeatures property in mobile.php to specify it (see more documentation about this). But if you need to calculate it dynamically then you will have to create a function to calculate it.

This can be achieved using the initialisation method (for more info, please see the Initialisation section above). The JavaScript returned by your initialisation method will need to define a function named supportsFeature that will receive the name of the feature:

var result = {
    supportsFeature: function(featureName) {
        // ...
    }
};
result;

Currently the app only uses FEATURE_MOD_ARCHETYPE and FEATURE_NO_VIEW_LINK.

Testing

You can also write automated tests for your plugin using Behat, you can read more about it on the Acceptance testing for the Moodle App page.

Upgrading plugins from an older version

If you added mobile support to your plugin for the Ionic 3 version of the app (previous to the 3.9.5 release), you will probably need to make some changes to make it compatible with Ionic 5.

Learn more at the Moodle App Plugins Upgrade Guide.

Troubleshooting

Invalid response received

You might receive this error when using the core-site-plugins-call-ws directive or similar. By default, the app expects all Web Service calls to return an object, if your Web Service returns another type (string, boolean, etc.) then you need to specify it using the preSets attribute of the directive. For example, if your WS returns a boolean value, then you should specify it like this:

[preSets]="{typeExpected: 'boolean'}"

In a similar way, if your Web Service returns null you need to tell the app not to expect any result using preSets:

[preSets]="{responseExpected: false}"

Values of ion-radio, ion-checkbox or ion-select aren't sent to my WS

Some directives allow you to specify a form id or name to send the data from the form to a certain WS. These directives look for HTML inputs to retrieve the data to send. However, ion-radio, ion-checkbox and ion-select don't use HTML inputs, they simulate them, so the directive isn't going to find their data and so it won't be sent to the Web Service.

There are 2 workarounds to fix this problem.

Sending the data manually

The first solution is to send the missing params manually using the params property. We will use ngModel to store the input value in a variable, and this variable will be passed to the parameters. Please notice that ngModel requires the element to have a name, so if you add ngModel to a certain element you need to add a name too.

For example, if you have a template like this:

<ion-list radio-group name="responses">
    <ion-item>
        <ion-label>First value</ion-label>
        <ion-radio value="1"></ion-radio>
    </ion-item>
</ion-list>

<ion-button expand="block" type="submit" core-site-plugins-call-ws name="myws" [params]="{id: <% id %>}" form="myform">
    {{ 'plugin.mycomponent.save' | translate }}
</ion-button>

Then you should modify it like this:

<ion-list radio-group [(ngModel)]="responses">
    <ion-item>
        <ion-label>First value</ion-label>
        <ion-radio value="1"></ion-radio>
    </ion-item>
</ion-list>

<ion-button expand="block" type="submit" core-site-plugins-call-ws name="myws" [params]="{id: <% id %>, responses: responses}" form="myform">
    {{ 'plugin.mycomponent.save' | translate }}
</ion-button>

Basically, you need to add ngModel to the affected element (in this case, the radio-group). You can put whatever name you want as the value, we used "responses". With this, every time the user selects a radio button the value will be stored in a variable called "responses". Then, in the button we are passing this variable to the parameters of the Web Service.

Please notice that the form attribute has priority over params, so if you have an input with name="responses" it will override what you're manually passing to params.

Using a hidden input

Since the directive is looking for HTML inputs, you need to add one with the value to send to the server. You can use ngModel to synchronise your radio/checkbox/select with the new hidden input. Please notice that ngModel requires the element to have a name, so if you add ngModel to a certain element you need to add a name too.

For example, if you have a radio button like this:

<div radio-group name="responses"> 
    <ion-item>
        <ion-label>First value</ion-label>
        <ion-radio value="1"></ion-radio>
    </ion-item>
</div>

Then you should modify it like this:

<div radio-group name="responses" [(ngModel)]="responses"> 
    <ion-item>
        <ion-label>First value</ion-label>
        <ion-radio value="1"></ion-radio>
    </ion-item>

    <ion-input type="hidden" [ngModel]="responses" name="responses"></ion-input>
</div>

In the example above, we're using a variable called "responses" to synchronise the data between the radio-group and the hidden input. You can use whatever name you want.

I can't return an object or array in otherdata

If you try to return an object or an array in any field inside otherdata, the Web Service call will fail with the following error:

Scalar type expected, array or object received

Each field in otherdata must be a string, number or boolean; it cannot be an object or array. To make it work, you need to encode your object or array into a JSON string:

'otherdata' => ['data' => json_encode($data)],

The app will automatically parse this JSON and convert it back into an array or object.

Examples

Accepting dynamic names in a Web Service

We want to display a form where the names of the fields are dynamic, like it happens in quiz. This data will be sent to a new Web Service that we have created.

The first issue we find is that the Web Service needs to define the names of the parameters received, but in this case they're dynamic. The solution is to accept an array of objects with name and value. So in the _parameters() function of our new Web Service, we will add this parameter:

'data' => new external_multiple_structure(
     new external_single_structure(
        [
            'name' => new external_value(PARAM_RAW, 'data name'),
            'value' => new external_value(PARAM_RAW, 'data value'),
        ]
    ),
    'The data to be saved', VALUE_DEFAULT, []
)

Now we need to adapt our form to send the data as the Web Service requires it. In our template, we have a button with the core-site-plugins-call-ws directive that will send the form data to our Web Service. To make this work we will have to pass the parameters manually, without using the form attribute, because we need to format the data before it is sent.

Since we will send the parameters manually and we want it all to be sent in the same array, we will use ngModel to store the input data into a variable that we'll call data, but you can use the name you want. This variable will be an object that will hold the input data with the format "name->value". For example, if I have an input with name "a1" and value "My answer", the data object will be:

{a1: 'My answer'}

So we need to add ngModel to all the inputs whose values need to be sent to the data WS param. Please notice that ngModel requires the element to have a name, so if you add ngModel to a certain element you need to add a name too. For example:

<ion-input name="<% name %>" [(ngModel)]="CONTENT_OTHERDATA.data['<% name %>']">

As you can see, we're using CONTENT_OTHERDATA to store the data. We do it like this because we'll use otherdata to initialise the form, setting the values the user has already stored. If you don't need to initialise the form, then you can use the dataObject variable, an empty object that the mobile app creates for you:

[(ngModel)]="dataObject['<% name %>']"

The app has a function that allows you to convert this data object into an array like the one the WS expects: objectToArrayOfObjects. So in our button we'll use this function to format the data before it's sent:

<ion-button expand="block" type="submit" core-site-plugins-call-ws name="my_ws_name"
    [params]="{id: <% id %>, data: CoreUtilsProvider.objectToArrayOfObjects(CONTENT_OTHERDATA.data, 'name', 'value')}"
    successMessage
    refreshOnSuccess="true">

As you can see in the example above, we're specifying that the keys of the data object need to be stored in a property called "name", and the values need to be stored in a property called "value". If your Web Service expects different names you need to change the parameters of the objectToArrayOfObjects function.

If you open your plugin now in the app it will display an error in the JavaScript console. The reason is that the data variable doesn't exist inside CONTENT_OTHERDATA. As it is explained in previous sections, CONTENT_OTHERDATA holds the data that you return in otherdata for your method. We'll use otherdata to initialise the values to be displayed in the form.

If the user hasn't answered the form yet, we can initialise the data object as an empty object. Please remember that we cannot return arrays or objects in otherdata, so we'll return a JSON string.

'otherdata' => ['data' => '{}'],

With the code above, the form will always be empty when the user opens it. But now we want to check if the user has already answered the form and fill the form with the previous values. We will do it like this:

$userdata = get_user_responses(); // It will held the data in a format name->value. Example: ['a1' => 'My value'].

// ...

'otherdata' => ['data' => json_encode($userdata)],

Now the user will be able to see previous values when the form is opened, and clicking the button will send the data to our Web Service in array format.

Moodle plugins with mobile support

See the complete list in the plugins database (it may contain some outdated plugins).

Mobile app support award

If you want your plugin to be awarded in the plugins directory and marked as supporting the mobile app, please feel encouraged to contact us via email at mobile@moodle.com.

Don't forget to include a link to your plugin page and the location of its code repository.

See the list of awarded plugins in the plugins directory.