Active→Refresh handlers are user-definable functions that are associated with elements of a component's view and that get executed only when the component is visible on screen, or enter a paused state otherwise. Refresh handlers can be activated on any component's element, directly in the HTML template by using the special element's attribute prefix @
(here thus intended as a symbol for "refresh/loop").
This kind of functions are very small and very little CPU time-consuming, even because they might get executed already a bunch of times in a second. For instance think of a function that validates the user input and that enables/disables a button accordingly:
<form z-load="default">
<label for="firstName">First name</label>
<input id="firstName" type="text" minlength="3">
<button @disable-if="!firstName.value || !firstName.validity.valid">
Ok
</button>
</form>
In the above example the built-in handler @disable-if
, evaluates the condition specified in the attribute's value and if the condition is truthy, then the button is disabled, in this case if the input text is less than 3 characters long.
Built-in @
handlers
Along with the @disable-if handler, the following active→refresh handlers are available:
@if
→ evaluates the given condition; if true, executes the code specified by the@then
attribute, otherwise the@else
one@hide-if
→ sets element's visibility to hidden if the given expression is truthy@disable-if
→ sets element to disabled if the given expression is truthy@sync
→ synchronizes the value of the element with the data model's field specified by#
(z-field
) attribute or by the@sync
value@get
→ gets new values from evaluating the given expression and passes them as arguments of the script specified by the@set
attribute@set
→ sets code to execute (works also without @get)@on:<event>
→ sets an event handler
Next to any active→refresh handler it is also possible to specify the following options:
@delay="<refresh_delay_ms>"
@active
The @delay
option can be used to set the refresh rate in milliseconds (default is 100ms), while the @active
option will force the execution of the refresh handler even if the element is not visible on screen.
In the next example, a default component context is created using the z-load="default" attribute on the container div
. It's then possible to use @
handlers and other component features inside its view.
There, four fields are declared using the #<field_name>
attribute (equivalent of z-field="<field_name>"
): #check1
, #check2
, #check3
and #proceed-button
. Declared fields are then available as variables in the script of type "jscript
", that is put at the end of the view and that adds a few more declarations.
<div z-load="default">
<label for="agreed1">
<input id="agreed1" type="checkbox" #check1>
Check this
</label>
<label for="agreed2">
<input id="agreed2" type="checkbox" #check2>
Check that
</label>
<label for="agreed3" @disable-if="!bothChecked">
<input id="agreed3" type="checkbox" #check3>
Everything is in place!
</label>
<button #proceed-button
@disable-if="!validFormData"
@on:click="handleClick">
Proceed
</button>
<div @hide-if="!proceedButton.disabled">
Please check both options in order to proceed.
</div>
<!-- Main View's Refresh Script -->
<script type="jscript" refreshDelay="150">
let validFormData;
let bothChecked;
function refresh() {
validFormData =
check1.checked
&& check2.checked
&& check3.checked;
bothChecked =
check1.checked
&& check2.checked;
}
function handleClick() {
alert('Yay! This worked! =)');
}
</script>
<!-- This CSS will be applied only to this view -->
<style media="#">
:host {
/* the special selector :host referrers to the component's view */
border: solid 1px whitesmoke;
max-width: 460px;
padding: 8px;
}
label[disabled] {
opacity: 0.5;
}
</style>
</div>
The type jscript
might sound unusual, but that's just because this way the browser will not recognize the type and will ignore this script without the need of wrapping it inside a <template>
container, instead it will be loaded by zuix.js as the default refresh handler of the component. Furthermore, the jscript
type, will be automatically recognized as JavaScript syntax by some IDE, without requiring additional plugins for syntax highlighting. Next to the jscript, it's also possible to add a scoped CSS that will be so applied only to the component's view.
A jscript
can also be defined outside the component's host element if the attribute for="<context_id>"
is added to it. If multiple jscript
occurrences are found, they will be merged into a single script.
The default refresh handler
The default refresh handler of a component, like the others active → refresh handlers, will run only when the component become visible in the viewport.
When the component is not visible in the viewport, the refresh handler will enter a paused state and the event refresh:inactive
will be triggered. If it becomes visible again, the event refresh:active
will be triggered.
The main body of the script's code will be executed only once, as initialization code, while the refresh()
function, if present, will be executed, as long as the component is visible, circa ten times a second or as differently specified by the refreshDelay
attribute of the <script>
tag (value is expressed in milliseconds).
Any member declared in this script it's only visible to the component, and can be referenced also in @
handlers' value expression employed in the view's template. And so, the two state variables validFormData
, bothChecked
, and the function handleClick
, declared in the example script, can be employed in the template as values for @disable-if
and @hide-if
handlers, so that basically, the "Proceed" button will be enabled only if all the three checkboxes are checked.
<button #proceed-button
@disable-if="!validFormData"
@on:click="handleClick"> Proceed </button>
"Active → Refresh" scripting scope
When the code of a refresh handler is run, beside the context object this
, which references the target HTML element itself, also a few more predefined variables are available, either if it's the code of a default refresh handler, or an expression in the value of a @
handler:
Member | Description |
---|---|
this | The target HTMLElement |
$this | Same as "this" , but ZxQuery-wrapped |
_this | If this element is also a component, _this is its component's context object |
<field_name> | For each element in the view with a # (z-field ) attribute, there will be a variable (only one for each distinct field) |
$<field_name> | Same as above but ZxQuery-wrapped, allowing multiple element instances for each field |
_<field_name> | If the field is also a component, then, this will be its component's context object |
context | The component's context that contains this element, also available with the name of the contextId property's value |
model | The component's data model |
$ | The component's view as ZxQuery object |
args | Optional arguments object |
additionally, like seen in the previous example, to the default refresh handler script (jscript), will be also available as variables, all #
fields declared in the view's template, and vice-versa, to any of the @
handlers employed in the view, will be also available all local variables and functions declared in the default refresh handler (jscript).
Notice that members starting with an underscore _
, since they are components, are loaded asynchronously, so they will be null
until the component context of the underlying element has been loaded. So if there is any value's expression in the template that involves such asynchronous objects, the ready()
function should be provided in the default refresh handler to prevent errors from evaluating expressions before async objects become available.
When implemented, the ready()
function, will simply return false
if any of the async objects used in template's value's expressions is null, and otherwise, will finally return true
to let the template's refresh handlers start safely. As long as the ready()
function returns false
, the CSS class .not-ready
will be applied to the view of the component, and this can be used to customize a visible feedback of the not ready state.
View's fields are also accessible through the "model
" object, that is the component's data model. In this case the two-way data binding will automatically sync and map any value's change, as shown in the next example.
<div z-load="default" z-context="color-select">
<label for="color">Color</label>
<select id="color" #color @sync>
<option>Black</option>
<option>DarkRed</option>
<option>GoldenRod</option>
<option>LimeGreen</option>
<option>DarkGreen</option>
</select>
<div class="color-preview"
@get="model.color as backgroundColor"
@set="$this.css({backgroundColor})"></div>
</div>
The @sync
handler on the select
will update the bound field color
in the component's data model, anytime a new option is selected from the select
control. While the @get
handler on the color preview rectangle, will read value changes from model.color
field, and pass new values to the @set
handler. So, what happen is that whenever the value of the select is changed, also the background color of the preview rectangle will change accordingly.
So, setting directly model.color = '<color_name>'
will also synchronize any bound element of the view, with the new field value, as shown in the following example where the model
of the color-select component above, is accessed from the following component to get or set the selected color:
<div z-load="default">
<label>
Selected: <span @get="cm.color as color"
@set="$this.html(color).css({color})"> ? </span>
</label>
<div layout="rows center-left">
<div @on:click="cm.color = 'Black'" class="color-Black"></div>
<div @on:click="cm.color = 'DarkRed'" class="color-DarkRed"></div>
<div @on:click="cm.color = 'GoldenRod'" class="color-GoldenRod"></div>
<div @on:click="cm.color = 'LimeGreen'" class="color-LimeGreen"></div>
<div @on:click="cm.color = 'DarkGreen'" class="color-DarkGreen"></div>
</div>
<script type="jscript" using="color-select">
const cm = colorSelect.model();
</script>
</div>
As shown in the jscript above, to reference another component, the using
attribute is added to the script
tag, with its value containing a comma separated list of the contexts' identifiers of the required components. In this example the context id is color-select
and it was assigned to the color select component using the attribute z-context="color-select"
on its container. Like in other cases, names containing the -
symbol will be converted to camel case, so in this case the assigned variable name will be colorSelect
.
Like seen for the ready()
function and view's @
handlers, also in this case the default refresh handler of the component will not be started until the components specified with the using
attribute are loaded. And also in this case the class .not-ready
is added to the container and removed only once all components are loaded. Then the default refresh handler is started, and so, in this example, the const cm = colorSelect.model()
, will assign to the local cm
variable, the data model of the color-select component.
Clicking a color will then assign a new value to the "color" field of the data model, @on:click="cm.color = '<color_name>'"
, and the data binding will do the rest by synchronizing the color-select component automatically.
It's also possible to add public methods and properties to a component so that these can be invoked from other components as well:
<script type="jscript">
expose = {
// adds a public property getter (read only)
get test() {
console.log('test getter');
return 'ok';
},
// add a public method
getSelected: function() {
return _selected;
}
};
</script>
Adding or overriding a @
handler
An ActiveRefreshHandler can be global or component-local. In the first case, it's stored in the global store named "handlers" and can be employed in any component's view. Or it can be added to the handlers list of a component's context, in which case is recognized only within the view of the component.
Refresh handlers will call the refreshCallback to request a new "refresh" after the given delay or as soon as it becomes visible again. If the refresh handler does not call the refreshCallback, the refresh loop will end.
For example, this is the built-in @hide-if
handler that is a refresh-based handler:
zuix.store('handlers')['hide-if'] = ($view, $el, lastResult, refreshCallback) => {
const code = $el.attr('@hide-if');
const result = zuix.runScriptlet(code, $el, $view);
if (result !== lastResult) {
result ? $el.css({visibility: 'hidden'})
: $el.css({visibility: 'visible'});
lastResult = result;
}
refreshCallback(lastResult);
};
that basically reads the code to evaluate from the @hide-if
attribute's value, then it executes the code using the utility method zuix.runScriptlet(..)
and, if the result is truthy and not equal to the previous evaluation, then it will set the target element's ($el
) visibility to hidden
, or, if the result is false and different from the previous evaluation, then it will set the visibility to visible
. It will finally call the method refreshCallback(lastResult)
to request a new refresh call, passing to it the last result.
Instead below, the built-in @on
handler is an event-based handler that gets executed only once, and will never call the refreshCallback
:
<script>
zuix.store('handlers').on = ($view, $el, lastResult, refreshCallback, attributeName) => {
const handlerArgs = zuix.parseAttributeArgs(attributeName, $el, $view, lastResult);
const code = $el.attr(attributeName);
const eventName = handlerArgs.slice(1).join(':');
$el.on(eventName, (e) => {
const eventHandler = zuix.runScriptlet(code, $el, $view);
if (typeof eventHandler === 'function') {
eventHandler.call($el.get(), e, $el);
}
});
};
</script>