Skip to content
This repository has been archived by the owner on Feb 20, 2019. It is now read-only.

Templating Syntax

BobNisco edited this page Nov 13, 2014 · 19 revisions

Summary

Templates allow you to define your view markup, data bindings, and event handlers declaratively. A declarative approach means that by default, they are written in an easily parseable, easily translatable format. As technology improves, the templates can be translated into new formats in the future if necessary.

Defining a view

Views should each be defined in their own directory under your project's src directory. So to make a new view, make a new directory under src that is TitleCased, indicating the class name of your view. For example, ToggleButton.

A view file is simply an html file that gets parsed and translated into a TypeScript class. The root element must have a js-type attribute, which defines the classname.

src/ToggleButton/ToggleButton.html:

<div js-type="ToggleButton"></div>

Binding to states

State data is sent to the view as a json object containing name/value pairs that will be mixed into the view's viewModel. For example:

var toggleButton = new ToggleButton();

toggleButton.setData({ isToggled: true });
console.alert(toggleButton.viewModel.isToggled); // alerts 'true'

In your template, you can use the js-bind attribute to indicate a binding of the viewModel state to the effect it has on the view. In the following example, I bind the text value of the button to the viewModel property "name", and the className "toggled" to the viewModel property "isToggled".

<button
    js-type="ToggleButton"
    js-bind="text:name, className.toggled:isToggled">
</button>

js-bind attribute options

A js-bind statement by default binds to the view's viewModel scope.

Here is an example which binds an anchor's text to the "name" property, the "href" attribute to the "url" property, and toggles the "isEnabled" class name if the "isEnabled" property is truthy:

<a js-bind="text:name, alt:altText, href:url, className.isEnabled:isEnabled"/>

The format of the js-bind syntax is:

[destinationType][.destinationSubType]:[sourcePath]<,[...]>

Where destinationType can be:

  • text - binds the text value of the element to the source value.

  • html - binds the html value of the element to the source value. Careful, this is not encoded.

  • attr - binds an attribute (provided as the subType) to the source value. This is the default binding type if no destinationType is provided. (e.g. binding "href:foo" will bind the href attribute to foo.)

  • className - toggles a given class name (provided as the subType) based on the source value's truthiness. (e.g. binding "className.foo:bar" will enable the class "foo" on the element if the source "bar" resolves as truthy.

The sourcePath defaults to the viewModel scope.

You can use a dot notation to drill into a specific value within the viewModel:

<div js-bind="text:item.name"></div>

If you want to bind to child view scopes, you can use the $ syntax, which indicates to "start at the view to resolve the scope" rather than the viewModel.

For example, you can bind to a child view's "isToggled" viewModel state:

<div js-type="MyView" js-bind="className.isHighlighted:$toggleButton.isToggled">
    <toggle-button js-name="toggleButton" />
</div>

Binding sources and converters

Often, a source needs to be massaged to make it "fit" into the experience. For example, you may want logic that multiplies a value by 3, or upper cases a value, or makes a date string friendly. For these scenarios you can bind directly to functions and you can pass the resolves values of existing properties into them.

For example, in my viewModel, I may have a toUpper helper that I want to wrap some of my binding sources within:

src/Foo/FooModel.ts:

class FooModel extends ViewModel {
    name = "david";

    toUpper(val) {
        return val ? val.toUpperCase(): '';
    }
}

src/Foo/Foo.html:

<div js-type="Foo" js-bind="text:toUpper(name)"></div>

Renders this markup:

<div>DAVID</div>

Handling events with the js-event attribute

General DOM events can be mapped to functions either defined on the viewModel (default) or the actual view implementation when you want to hide view logic from the viewModel implementation.

Here's an example of listening to a click event on a button, which binds to the _onClick handler:

<button js-event="click:_onClick">click me</button>

When the view is "activated", the event will automatically be observed and will call the function.

Common event helpers

The toggle method on the base View class can be used to toggle values. Because it is a property of the View class, you use the $ notation to refer to it:

For example:

<div js-bind="className.visible:isVisible">I have the visible class applied</div>
<button js-event="$toggle('isVisible')">Click to toggle the visible class.</button>

Repeat blocks

Utilizing repeat blocks is simple in a OneJS Template.

Let's say we have a basic array of strings in our viewmodel:

arr = ['foo', 'bar', 'baz'];

If we want to repeat over this in our template, and print out each string in the array, we would use the js-repeat syntax:

<js-repeat source="arr" iterator="item">
    <p js-bind="text:item"></p>
</js-repeat>

Notice how our iterator value "item" becomes a locally scoped variable we can use while inside the js-repeat block.

Conditionals

OneJS templates have support for a basic if statement.

Let's say we have a boolean var in our viewmodel that tells us a string var from the viewmodel should be rendered.

shouldDisplay = true;
displayText = "Hello world?"

And we want to conditionally render it:

<js-if source="shouldDisplay">
    <p js-bind="text:displayText"></p>
</js-if>

js-if also supports truthy/falsy values given from functions in the View.

not(val: any) {
    return !val;
}
<js-if source="$not(shouldDisplay)">
    <p js-bind="text:displayText"></p>
</js-if>

Note the $ is a shorthand for view while in the template.

Nesting views

Nesting views is really simple in OneJS. Imagine this directory structure:

src/ToggleButton/ToggleButton.html src/SetView/SetView.html

Within the SetView.html template, you can embed the toggle button as such:

<div>
    <js-view 
        js-type="../ToggleButton/ToggleButton"
        js-name="myToggleButton" />
</div>

The js-type attribute is required. It defines the requireJS path to the child view.

The js-name attribute is optional. It defines the member name used to attach the child control to the parent.

The js-init attribute is optional. It defines the data to be set on the child view at initialization time. It can be a single item using binding scoping: (e.g. js-init="item" ) or it can be a collection of items: (e.g. js-init="{ item: item, isToggled: true }" )

The js-data attribute is optional. It defines the data to be set on the child view at view model change time. It uses the same format of passing in data as js-init.

Shorthanded view nesting format

The ToggleButton syntax shown above can be written in a simplified element format. For example, the above example can be shorthanded as such:

<div>
    <toggle-button />
</div>

Here's what happens:

The SetView class generated from the template will:

  • require('../ToggleButton/ToggleButton')
  • add a member that represents the toggle button view control.
  • the member name will be generated.

This will automatically instantiate the ToggleButton. Note that we assume TitleCased classes and the lower-case-dashed html tag format will automatically be inferred.

Note, for requireJS imports, we would be requiring "../src/ToggleButton/ToggleButton". This import is automatically managed for you.

Passing data to child views

Passing down data to a child view is simple. There are two scenarios that you should consider as to how you send the data: should this be done as a one time initialization setting, or re-evaluated any time the view's viewModel changes.

  • For initialization which happens once, the js-init attribute should be used.
  • For mapping data to be re-evaluated on viewmodel changes, the js-data attribute should be used.

Example:

<command-link 
    js-init="{ icon: downloadIcon, name: strings.download }" 
    js-data="{ item: item }" />