A view is the visual part of a component, and it consists of an .html
template file, and a .css
file, both with the same base name:
<component_id>.html
<component_id>.css
(optional)
A view can also be implemented inline, rather than in separate files, and can be declared by adding the z-view
attribute to its container element.
The value of z-view
attribute is meant to be a unique identifier ([<path>/]component_name
) and should not match the identifier of any other view placed in a file or inline in the same page, unless we want to override it.
Inline declaration of the view inline/example/hello-world
:
the above code is just a declaration of a view and will not produce any visible content, to actually load an instance of the hello_word
view, the following code is used:
<div view z-load="inline/example/hello-world"></div>
or, if a custom element tag has been defined like in the example below,
customElements.define('hello-world', class extends HTMLElement {
connectedCallback() {
zuix.loadComponent(this, 'inline/example/hello-world', 'view');
}
});
then the view template can be loaded using the custom element tag:
<hello-world></hello-world>
Hello World!
Data binding
In the previous example, the text "Hello World" is static and cannot be customized, but an HTML template can also have elements of the view whose value is bound to fields of a data model that can be provided with the component's loading options.
To bind the value of an element in the HTML template to a field of a data model, the #<field_name>
attribute (shorthand of z-field="<field_name>"
), is added to the element in order to specify the name of the corresponding field in the data model.
Declaration of an inline view with title
and subtitle
fields:
<div z-view="inline/example/article-header">
<h1 #title>
Example title
</h1>
<p #subtitle>
Example article subtitle
</p>
</div>
Example title
Example article subtitle
Then, to set the actual data to display in #<field_name>
elements, the :model
attribute can be added to the host element, to pass a JSON data object with the actual data to display:
<div view z-load="inline/example/article-header" :model="{
title: 'Image from Mars',
subtitle: 'A Perseverance rover scientist’s favorite shot of Jezero Crater\'s \'Delta Scarp\'.'
}"></div>
It's also possible to map a field with a different name, other than <field_name>
, by specifying a value for the #
attribute with the name of the mapped field: #<field_name>="<mapped_field_name>"
(equivalent of z-field="<field_name>" z-bind="<mapped_field_name>"
).
To declare a data model with nested fields, dotted names syntax can be used, like with link.url
and link.title
fields in the following template example of a Material Design Lite card.
templates/mdl-card.html
<!-- Template of a Material Design card -->
<div class="mdl-card mdl-shadow--8dp">
<div class="mdl-card__title">
<img #image src="examples/images/card_placeholder.jpg" class="portrait" alt="Cover image">
<h1 #title class="mdl-card__title-text mdl-color-text--white animate__animated animate__fadeInDown">
Card title
</h1>
</div>
<div #text class="mdl-card__supporting-text animate__animated animate__fadeInUp">
Lorem ipsum dolor sit amet, consectetur adipisicing elit.
Aliquam accusamus, consectetur.
</div>
<div class="mdl-card__actions mdl-card--border animate__animated animate__fadeInRight">
<a #url="link.url" class="mdl-button mdl-js-button mdl-js-ripple-effect mdl-color-text--accent">
<span #link.title>
Open
</span>
</a>
</div>
</div>
When loading a view, the data provided through the :model
attribute, can be passed in either of the following ways:
- as inline string (like in the previous example)
<div view z-load="templates/mdl-card" :model="{
title: 'Down the rabbit hole',
image: 'examples/images/card_cover_2.jpg',
text: 'Luckily for Alice, the little magic bottle had now had its full effect, and she grew no larger…',
link: {
title: 'Read more',
url: '#'
}
}" class="visible-on-ready"></div>
- or with a variable
<div view z-load="templates/mdl-card" :model="myCardData"></div>
<script>
myCardData = {
title: 'Down the rabbit hole',
image: 'examples/images/card_cover_2.jpg',
text: 'Luckily for Alice, the little magic bottle had now had its full effect, and she grew no larger…',
link: {
title: 'Read more',
url: '#'
}
};
</script>
Binding Adapters
Sometimes it might be required to have more control over how the value of a data model's field affects the view. For this purpose binding adapters can be used.
A binding adapter is a function that gets called to update bound elements of a view, based on the actual values of the data model's fields.
A binding adapter gets automatically called everytime a new data model is set, or if the context.modelToView()
method is called programmatically. In both cases, the binding adapter function, will get called for each #<field_name>
or z-bind
attribute declared in the view's template and will provide required code to update the view:
/** @type {BindingAdapterCallback} */
function vm_binding_adapter($element, fieldName, $view, refreshCallback) {
// TODO: adapter code to update `$element` bound
// to `fieldName` data model's field
}
where $element
is the element of the view with #<field_name>
/z-bind
attribute, fieldName
is the attribute value, that is the name of the bound field in the data model, and $view
is the view's element itself.
The refreshCallback
is a callback that can be used either for postponing the update of field's bound $element
(eg. data is not available yet), or to request a cyclic refresh of the same $element
/fieldName
.
<div view z-load="templates/mdl-card"
z-model="sampleCardAdapter"></div>
<script>
counter = 0;
sampleData = null; // example data for the card
// let's pretend the data is fetched asynchronously via http
setTimeout(() => {
sampleData = {
title: () => 'Hello! ' + counter++,
text: 'A binding adapter can be used to customize the way data model\'s fields are mapped to the view elements.',
image: () => 'https://picsum.photos/320/160?' + counter
}
}, 1);
function sampleCardAdapter($element, fieldName, $view, refreshCallback) {
if (sampleData === null) {
// no data yet
// return and retry again in 500ms
return refreshCallback();
}
// data is available, update only
// if the `$element` is visible on screen
if (!$element.position().visible) {
// postpone field binding of 1s if element is not visible
return refreshCallback(1000);
}
// $element is visible, resolve field binding
switch(fieldName) {
case 'title':
$element.html(sampleData.title());
// this field will be updated every second
return refreshCallback(1000);
case 'image':
$element.attr('src', sampleData.image());
// get the component's context
const ctx = zuix.context($view);
// to animate the `title` field also
ctx.field('title')
.playAnimation('animate__fadeInDown animate__bounce');
// this field will be updated every 5 seconds
return refreshCallback(5000);
case 'text':
$element.html(sampleData.text);
// no `refreshCallback`, this field
// will be updated only once
break;
}
}
</script>
In the above example the binding adapter is used to handle all bound fields of the data model. This binding adapter also checks if the component is visible before updating view's fields. If it's not visible then it will enter an idle state postponing the refresh.
In the case of a JSON
data model, it's also possible to use a binding adapter for each single field, like with the field name
in the following example:
<!-- Foo Bar chip -->
<div view z-load="inline/common/contact_chip"
:model="foo_bar_contact" style="min-height: 36px"></div>
<!-- Jane Doe chip -->
<div view z-load="inline/common/contact_chip"
:model="a_random_contact" style="min-height: 36px"></div>
<script>
// example inline data model
foo_bar_contact = {
image: 'images/avatar_02.png',
name: 'Foo Bar'
};
// the field `name` is updated using
// a binding adapter
a_random_contact = {
image: 'images/avatar_01.png',
name: function($element, field, $view, refreshCallback) {
const name = a_random_name();
$element.html(name);
}
};
function a_random_name() {
// let's pretend this is random =))
return 'Jane Doe';
}
</script>
Accessibility
The data model can also be set directly inside the host element through HTML tags, and this will provide a default visualization in case the view's template is still loading or JavaScript is not enabled.
The following example shows how to embed data model's fields with HTML code inside the host element using #<field_name>
attribute:
<div view z-load="templates/mdl-card">
<h1 #title>Let's code!</h1>
<img #image src="examples/images/card_cover_3.jpg"
alt="Cover image" role="presentation" width="460">
<p #text>
Yes we can!
</p>
<a #link.url href="#">
<span #link.title>Take me there</span>
</a>
</div>
and the above code, as is, will also provide a default visualization, that will also work without JavaScript:
- Load view "mdl-card"
- Unload
to actually load or unload the templates/mdl-card
view, use the Try Me button above.
This is just an example to show what happen when the HTML view code is enhanced by the component, since by default, in JavaScript enabled browsers, components loading starts right away.
So, in the previous example, the component data model fields are HTML elements within the host element (attributes with the #
prefix), which are automatically mapped to a certain property of the corresponding element in the loaded component's view, depending on its type.
For instance, if the target element is img
, then the mapped property will be .src
, while if it's a div
or a p
, it will be the .innerHTML
property. A binding adapter can eventually be used to override the default elements mapping strategy.
The host element body can also be used to simply provide an alternative text description for browsers where Javascript is disabled, or a loading message to show while the component is loading for browsers where Javascript is enabled:
<div view z-load="templates/mdl-card" :model="{
title: 'Some title',
image: 'examples/images/card_cover_4.jpg',
text: 'Some great encouraging text.',
link: {
title: 'Just do it!',
url: '#'
}
}">
<h1>-=| loading |=-</h1>
<div class="mdl-spinner mdl-js-spinner is-active"></div>
<noscript>
This component works only in JavaScript enabled browsers.
</noscript>
</div>
-=| loading |=-
- Load view "mdl-card"
- Unload
Since components can be dynamically loaded, unloaded or replaced, it is also possible to select a specific layout of the component, based for instance, on the device screen size/orientation or a user selectable theme. All without changing the HTML code of the page, that will basically host the components' data models itself, and also provide through it a default visualization of the page that will even work without Javascript.
Behaviors
Behavior Handlers determine how a view will react and behave upon certain events, like user interaction or state-change events. It's a different thing from regular events, since behavior are highly generic and reusable on groups or categories of elements. They are also very different from CSS animations, since behind behaviors there is a user-definable logic implemented with JavaScript code.
In a similar way to Event Handlers, Behavior Handlers can be declared in the component's options through the property behavior
:
<script>
options = {
// behavior handlers
behavior: {
'<event_name>': function(e, data, $el?) {
// the context object `this` is same as `$el`
},
// other behaviors ...
},
// event handlers
on: { /* ... */ }
// other component's options...
}
</script>
<div z-load="my/component" :options="options"></div>
where <event_name>
is the name of the event (eg. click
, mouseover
, ...), and function(e, data, $el?)
is the associated EventCallback that will be called each time the <event_name>
occurs.
Behavior Handlers can also be directly declared with the :behavior
attribute on the host element:
<script>
componentBehavior = {
'<event_name>': function(e, data, $el?) {
// the context object `this` is same as `$el`
},
// other behaviors ...
};
</script>
<div z-load="my/component" :behavior="componentBehavior"></div>
Using the internal default component, it is possible to get advantage of behaviors and other component's features, also on standard HTML elements as shown in the example below, where a standard input
element is enhanced with a visual feedback to report user input errors using HTML5 built-in form validation.
<input :behavior="checkValidityBehavior"
type="text" value=""
placeholder="Enter nickname"
pattern="[a-zA-Z0-9]+" minlength="4" maxlength="10"
message="Choose a name between 4 and 10 chars long, only letters and numbers."
aria-describedby="input-error">
<small id="input-error"></small>
The behavior CheckValidityBehavior
in this example, checks validation properties of the input
element, and reports validation hints inside the element with the id
specified by the aria-describedby
attribute. The message
attribute is used instead to configure a default hint message to show when the input value is empty.
In this other example, a view switch behavior is applied to some buttons and text elements. Each of them, also specify the target
view associated to be shown on click, and a transition animation
to be used. So, when a view switch button is clicked, the associated view will be shown using the transition animation effect specified or a default fade
. ViewSwitchBehavior takes one argument that can be used to specify the transition direction that can be 'horizontal' (default), or 'vertical'.
<script>
view_switch_h = ViewSwitchBehavior('horizontal');
view_switch_v = ViewSwitchBehavior('vertical');
</script>
<!-- Clicking the "1" button, will show "view-1" -->
<button :behavior="view_switch_h"
target="view_1" animation="bounce">
1
</button>
<!-- Clicking the "2" button, will show "view-2" -->
<button :behavior="view_switch_h"
target="view_2">
2
</button>
// ...
<!-- the pictures container --->
<div style="overflow: hidden; position: relative; width: 320px;height: 240px;">
<div #view_1>
<img src="picture-1.jpg">
</div>
<div #view_2 tab-selected="true">
<img src="picture-2.jpg">
</div>
// ...
</div>
The possible values of the animation
attribute are:
'bounce', 'fade', 'slide', 'zoom', 'back',
'lightSpeed' // (<-- this one only works with horizontal orientation)
Random content 1
Random content 2
Random content 3
Random content 4
Random content 5
TEST one, two, three, four, five! ... =)