View Templates

Version compatibility

View templates were introduced in version 4.90.0 of the JourneyApps Runtime.

In TypeScript apps, the minimum runtime-build version required is 2.4.7. See the docs here on how to configure this.

Overview

View templates allow developers to define view XML as a standalone template, and reference it across views. What this means for developers:

  • Less code duplication across views.

  • Easier to maintain complex views, by breaking up the view XML into smaller snippets in templates.

  • Can introduce a better separation of concerns - a set of view components can be grouped into distinct templates.

View templates are supported in both JavaScript and TypeScript apps.

Limitations

  • Limited validation and auto-complete in OXIDE.

Usage & Syntax

Using view templates involves two things:

  1. Defining the template -> This is done using template definitions (template.xml files) for your app.

  2. Referencing it in a view -> This is done using the template UI component on a view.

Let's dive into these:

1) Define a view template

When working with view templates we recommend opening (and docking) the View Templates panel in OXIDE:

Here you can create new templates, view and select existing templates.

You can create multiple template files (e.g. demo.template.xml), and each can contain multiple template definitions (<template-def />). A template file is simply a way to group similar template definitions.

A template file:

<?xml version="1.0" encoding="UTF-8"?>
<templates>
    <template-def name="dialog">

    </template-def>
</templates>

Inside template-def, you can define UI components in the similar to how you’d define them in views. You can pass parameters from your views into the template definition, including objects and functions.

<!-- buttons.template.xml -->
<?xml version="1.0" encoding="UTF-8"?>
<templates>
    <template-def name="save-back-buttons">
        <param name="user" type="user" />
        <param name="onBack" type="function" />
        <param name="onSave" type="function" />
        
        <info>{user.name}</info>
        <button-group>
            <button label="Back" icon="fa-arrow-circle-left" on-press="$:onBack" style="outline" validate="false" />
            <button label="Save" icon="fa-check-circle" on-press="$:onSave" validate="true" />
        </button-group>
    </template-def>

    <template-def name="">
        ...
    </template-def>
</templates>

2) Reference the template in a view

In your view XML, reference the view template using the template UI component and pass the parameters you defined:

<!-- main.view.xml -->
<?xml version="1.0" encoding="UTF-8"?>
<view title="buttons">
    <var name="user" type="user" />

    <!-- Provide the name of the template definition in the name="" attribute. -->
    <!-- Pass parameters from your view to the template using the parameters you defined in the template definition. -->
    <template name="save-back-buttons" user="user" onBack="$:goBack()" onSave="$:save()" />
</view>
// main.ts
async function init() {
    view.user = await DB.user.first();
}
function goBack() {
    notification.success("Back was pressed");
}
function save() {
    notification.success("Save was pressed");
}

Use Cases

Functions

The below example illustrates how a function can be called in a view template and pass parameters to the view.

Template definition:

<!-- dialog.template.xml -->
<!-- Dialog to select an item from an object table -->

<template-def name="select-item-template">
    <param name="items" type="array:line_item" />
    <param name="selectItem" type="function">
        <!-- Define function arguments and type -->
        <arg name="selectedItem" type="line_item"/>
    </param>
        
    <dialog id="select-item-dialog" title="Select an item" auto-hide="true">
        <body>
            <object-table query="items" label="Items" empty-message="Your items will appear here">
                <column heading="Name">{name}</column>
                <column heading="Code">{product_code}</column>
                <action on-press="$:selectItem($object)" />
            </object-table>
        </body>
    </dialog>        
</template-def>

View XML:

<!-- main.view.xml -->
<!-- Reference the template -->
<template name="select-item-template" 
    items="items" 
    selectItem="$:selectItem(selectedItem)" />

View TS:

// main.ts
function selectItem(selectedItem) {
    view.selected_item = selectedItem;
    component.dialog({ id: 'select-item-dialog' }).hide(); 
}

App Modules (TypeScript)

App Modules can be referenced from template definitions directly.

<!-- buttons.template.xml -->
<?xml version="1.0" encoding="UTF-8"?>
<templates>
    <template-def name="save-back-buttons">
        <import module="*" as="shared" from="~/lib/mylib"/> 
        
        <param name="user" type="user" />
        
        <info>{user.name}</info>
        <button-group>
            <button label="Back" icon="fa-arrow-circle-left" on-press="$:shared.onBack()" style="outline" validate="false" />
            <button label="Save" icon="fa-check-circle" on-press="$:shared.onSave()" validate="true" />
        </button-group>
    </template-def>
</templates>

Nested templates

You can nest templates by referencing one in a template definition as follows:

<!-- nested-demo.template.xml -->
<?xml version="1.0" encoding="UTF-8"?>
<templates>
  <template-def name="job-dialog">
        <param name="job" type="job" />
        <param name="editMode" type="boolean" />
        <param name="mainButtonLabel" type="text" />
        <param name="onSave" type="function" />
        <param name="onCancel" type="function" />
        <dialog id="job-details-dialog" auto-hide="true">
            <body>
                <toggle bind="editMode" label="Edit" required="false" />
                <columns>
                    <column hide-if="editMode">
                        <heading>Edit job</heading>
                        <template name="edit-job" />
                    </column>
                    <column show-if="editMode">
                        <heading>Job details</heading>
                        <template name="job-details" job="job" />
                    </column>
                </columns>
            </body>
            <button-group>
                <button label="Cancel" show-if="editMode" on-press="$:onCancel" style="outline" />
                <button label="{mainButtonLabel}" on-press="$:onSave" />
            </button-group>
        </dialog>
    </template-def>
    <template-def name="job-details">
        <param name="job" type="job" />
        <info-table>
            <row label="Job number" value="#{job.number}" />
            <row label="Description" value="{job.description}" />
            <row label="Completed" value="{job.completed}" />
        </info-table>
    </template-def>
    <template-def name="edit-job">
        <param name="job" type="job" />
        
        <info>Job number: #{job.number}</info>
        <text-input bind="job.description" label="Description" required="false" />
        <toggle label="Completed" bind="job.description" mode="checkbox" required="false" />
  </template-def>
</templates>

Complete Example

The below shows a composable dialog as a view template and demonstrates:

  • Passing objects or functions as parameters to the template.

  • Using a function expression (inline function)

  • Using an app module in a view template.


<!-- dialog.template.xml -->
<?xml version="1.0" encoding="UTF-8"?>
<templates>
    <template-def name="create-edit-item-template">
        <import module="*" as="shared" from="~/lib/index"/> 

        <param name="lineItem" type="line_item" />
        <param name="onCancel" type="function" />
        <param name="onSave" type="function" />
        
        <!-- Dialog that will be used in multiple views -->
        <dialog id="create-edit-item" title="{$:lineItem ? 'Edit' : 'Create'} Item" auto-hide="false"> 
            <body>
                <text-input label="Name" bind="lineItem.name" required="true" />
                <text-input label="Product Code" bind="lineItem.product_code" required="true" />
                <heading />
                <button show-if="lineItem" label="Delete" icon="{$:shared.ICONS.delete}" on-press="$:shared.dialogHelper.delete('add-edit-item')" validate="false" color="negative" style="outline"  /> 
            </body>
            <button-group>
                <button label="Cancel" icon="{$:shared.ICONS.close}" on-press="$:onCancel" validate="false" style="outline" />
                <button label="Save" icon="{$:shared.ICONS.done}" on-press="$:onSave" validate="true" />
            </button-group>
        </dialog>      
    </template-def>
</templates>

The corresponding view XML and TS:

<!-- main.view.xml -->
<?xml version="1.0" encoding="UTF-8"?>
<view title="Dialog Example">
    <var name="line_item" type="line_item" />

    <button label="Show dialog" on-press="$:shared.dialogHelper.open('create-edit-item')" validate="false" />
    <template name="create-edit-item-template" lineItem="line_item" onCancel="$:cancel('create-edit-item')" onSave="$:save('create-edit-item')" /> 
    
    <!-- For reference, here's how the dialog 
    was defined prior to the template -->
    <dialog id="reference" title="{$:view.line_item ? 'Edit' : 'Add'} Item" auto-hide="false">
        <body>
            <text-input label="Name" bind="line_item.name" required="true" />
            <text-input label="Product Code" bind="line_item.product_code" required="true" />
            <heading />
            <button show-if="line_item" label="Delete" icon="{$:shared.ICONS.delete}" on-press="$:shared.dialogHelper.delete()" validate="false" color="negative" style="outline"  />
        </body>
        <button-group>
            <button label="Cancel" icon="{$:shared.ICONS.close}" on-press="$:cancel()" validate="false" style="outline" />
            <button label="Save" icon="{$:shared.ICONS.done}" on-press="$:save()" validate="true" />
        </button-group>
    </dialog>
        
</view>
// main.ts

async function init() {    
    view.line_item = await DB.line_item.first();
}

function cancel(dialogId:string) {
    notification.success("Cancel was pressed");
    shared.dialogHelper.close(dialogId);
}
async function save(dialogId:string) {
    await view.line_item.save();
    notification.success("Save was pressed");
    shared.dialogHelper.close(dialogId);
}

Architecture

It is important to note that a view template, unlike other UI components, does not evaluate expressions, but instead replaces templated components and attributes with the provided values.

Let us use the following example of a template with function param:

<!-- example.view.xml: -->
<view title="Example">  
  <template name="test" sayHello="$:alert(msg, theUser)" />
</view>

Attribute sayHello is $:alert(msg, theUser) - which is a function call.

// example.ts
function alert(msg: string, theUser: DB.user, source?: string) {
    notification.info(msg + ' ' + theUser.name);
}
<!-- example.template.xml -->
<?xml version="1.0" encoding="UTF-8"?>
<templates>
   <template-def name="example">
     <param name="sayHello" type="function">       
        <arg name="msg" type="text" />
        <arg name="theUser" type="user" />
     </param>
   
     <button label="Greet" on-press="$:sayHello('Hello', user)" />
   </template-def>
</templates>

The template-def declares a function parameter with two arguments, msg and theUser.

When this template-def is compiled for the particular view (this happens during a deploy, not at runtime), the expression is broken up into parts and the view's template component is replaced with the components inside the template-def. Function definitions are replaced as follows:

This results in the button’s attribute on-press="$:alert('Hello', user)"

Also note that one could still pass view specific arguments, for example <template name="test" sayHello="$:alert(msg, theUser, 'main')"/> and only the two matching arg values will be replaced, resulting in on-press="$:alert('Hello', user, 'main')"

Last updated