Embedding legacy UI in Tui

Embedding legacy UI in Tui

Totara 20+

Sometimes you find a need to embed a piece of legacy UI in Tui. Provided that licensing restrictions are followed (i.e. no direct communication between Tui and legacy), this is usually possible.

The challenge is not in getting the HTML, but rather in getting the JS requirements and initialisation code. If we were to just pass in the HTML and have Vue render it, we would see the HTML, but it would be inserted after all of the scripts have loaded, so scripts expecting the HTML to exist when the page loads would not work.

In order to solve this, we need to insert the script requirements at the same time as the HTML.

There are two parts to this:

Output capturing

First, we need to capture the JS requirements along with the HTML.

There is a specific API provided for this: you can call $PAGE->requires->capture_end_code_requirements() with a callback, and every requirement that would normally be emitted at the end of the body of the page (script tags, initialisation calls, etc) will be captured and returned as a trusted_html instance, instead of becoming part of the page.

Usage looks like the following:

<?php $embedded_html = null; $embedded_js = $PAGE->requires->capture_end_code_requirements( function() use(&$embedded_html) { $jsmodule = [ 'name' => 'mod_example', 'fullpath' => '/mod/example/module.js', ]; $PAGE->requires->js_init_call( 'M.mod_example.init', ['args' => ''], false, $jsmodule ); $embedded_html = trusted_html::trust( $OUTPUT->render_from_template('mod_example/atemplate', $data) ); } ); // These are the props passed to our Tui component $tui_component_props = [ 'exampleUI' => trusted_html::join([$embedded_html, $embedded_js]), ];

Rendering

On the frontend, we can’t simply render the HTML with v-html or v-content, as both of those use the innerHTML API to insert the provided HTML. Script tags inserted that way are not executed.

Instead, there is a dedicated component for this use case: EmbeddedFragment. This component scans for and executes the embedded script tags.

It can be used in a Vue component like so:

<script> import EmbeddedFragment from 'tui/components/content/EmbeddedFragment'; defineProps({ /** @type {import('vue').PropType<import('tui/content').TrustedHtml>} */ exampleUI: Object, }); </script> <template> <EmbeddedFragment :html="exampleUI" /> </template>

Make sure this component is not conditionally rendered, as each time it is mounted it will execute the scripts.

Multiple legacy UI elements

In the examples above, we combine the embedded HTML and JS before passing to the frontend. If you had multiple pieces of legacy UI to render, you would capture them all in the same capture_end_code_requirements call, and render the JS separately.

Note: Sometimes when combining multiple js_init_call() and using the same $embedded_js for multiple html/components can lead to errors being thrown due to the js expecting an element in the HTML but it doesn’t exist. Errors in some cases can lead to modals not being launched even if the error isn’t directly related. I.e. Any errors in the console will lead to the modal from Notification.confirm to not be launched.

For example:

<?php $abc_html = null; $xyz_html = null; $embedded_js = $PAGE->requires->capture_end_code_requirements( function() use(&$abc_html, &$xyz_html) { // UI element 1 $PAGE->requires->js_init_call(...); $abc_html = trusted_html::trust($OUTPUT->render_from_template('mod_example/abc')); // UI element 2 $PAGE->requires->js_init_call(...); $xyz_html = trusted_html::trust($OUTPUT->render_from_template('mod_example/xyz')); } ); $tui_component_props = [ 'abcUI' => $abc_html, 'xyzUI' => $xyz_html, 'embeddedJS' => $embedded_js, ];
<script> import EmbeddedFragment from 'tui/components/content/EmbeddedFragment'; defineProps({ abcUI: Object, xyzUI: Object, embeddedJS: Object, }); </script> <template> <div> <div> <h2>ABC</h2> <EmbeddedFragment :html="abcUI" /> </div> <div> <h2>XYZ</h2> <EmbeddedFragment :html="xyzUI" /> </div> <EmbeddedFragment :html="embeddedJS" /> </div> </template>

Related

The embedded JavaScript from the legacy UI element may still be expecting certain things about the page – for example, that certain elements exist. This API won’t automatically solve this, you’ll need to either make sure the element exists, or edit the JavaScript that is being embedded.

It’s also possible to capture an entire page and wrap it in a Vue component using the $PAGE->requires->output_content_js and $PAGE->requires->get_content_end_code() APIs. See server/course/format/pathway/classes/watcher/pathway_watcher.php for an example of this in use, but you should prefer the approach using capture_end_code_requirements() unless you need to override the rendering of existing third party pages and wrap them.