Inside the view of a component, other than standard HTML event handlers, it is possible to add event handlers that live inside the component's scripting scope.
This kind of event handlers can be added to an element as an attribute having the name of the event wrapped between parentheses:
(<event_name>)="<inline_code_or_handler_fn>"
The value o such event attribute is evaluated as JavaScript expression and can contain reference to variables and functions defined in the component's scripting scope, where the following predefined members are available:
Name | 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 the target element |
<context_id> | Alias of context , with the name of the component's contextId transformed to camelCase |
model | The component's data model, shortcut of context.model() |
$ | The component's view as ZxQuery object, shortcut of context.$ |
args | Optional arguments object |
trigger(ev, dt) | Triggers a component event |
Additionally, any other member explicitly declared in the component's controller code using the {ContextController}.declare(..) method, will be also available.
Scoped scripts
Scoped scripts are executed within the context of a component, and anything declared inside of it is visible only within that component's context. They can be only declared inline, wrapped in a <script>
tag, with the attribute type="jscript"
and as direct children of the component's host element, or outside it, if the script tag's attribute for="<context_id>"
is added (where <context_id>
is the value of z-context
attribute).
If multiple jscript
occurrences are found, they will be merged into a single script.
<div z-context="scope-test">
<mdl-button :class="'primary'"
(click)="localFunctionTest">
Test Local
</mdl-button>
<mdl-button :class="'primary'"
(click)="globalFunctionTest">
Test Global
</mdl-button>
<div #test-message></div>
</div>
<script type="jscript" for="scope-test">
function localFunctionTest(e, $el) {
$testMessage
.html(`I run inside the "${scopeTest.contextId}"'s component context.`);
setTimeout(() => testMessage.innerHTML = '', 3000);
}
</script>
<script>
function globalFunctionTest(e, $el) {
alert('I run in the global scope and cannot access component-local members.');
}
</script>
So, in the above example, the localFunctionTest
runs inside the component's context and can access the objects scopeTest
, testMessage
, $testMessage
and other members available in the scripting scope of the component.
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. Furthermore, the jscript
type, will be automatically recognized as JavaScript syntax by some IDE, without requiring additional plugins for syntax highlighting.
The default refresh() handler
Inside its scripting scope, a component can be provided with a default refresh handler, a function that will be called only when the component is visible in the viewport.
The default refresh handler can be implemented by adding the refresh()
callback function to a scoped script.
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.
While the main body of a scoped script is executed only once as initialization code, the refresh()
function, if defined, will be executed as long as the component is visible, with a delay of 100ms between each call or as differently specified by the refreshdelay
attribute of the enclosing <script>
tag (value is expressed in milliseconds).
<div z-context="refresh-example">
<mdl-button #button
:class="'primary'"
:model:counter="1000"
(click)="_button.model().counter = 0">
<span #counter></span>
</mdl-button>
<script type="jscript" refreshdelay="250">
function refresh() {
_button.model().counter++;
}
</script>
</div>
So, when the above button is not visible on the screen, the component will be paused and the refresh()
function will not be called.
The default ready() handler
A component might be using other components, libraries and other resources. zuix.js tries to automatically detect used dependencies and start the component only when all of them have been loaded, to prevent runtime errors when evaluating component's code that might reference one or more dependencies.
It's anyway possible to add custom code logic and set when to start the component, by implementing the ready()
function, that can be added to a scoped script to prevent such runtime errors.
When implemented, the ready()
function, will simply return false
if the conditions to start the component aren't there yet, for instance if an asynchronously loaded object, used in the code of the view's template, is null.
When the condition for starting the component are finally met, the ready()
function will 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.
<div z-context="ready-example">
<div #test>Not ready yet =/</div>
<script type="jscript">
let counter = 0;
function refresh() {
test.innerHTML = `Ready! =) ${counter++}`;
}
function ready() {
return self.testIsReady;
}
</script>
</div>
<mdl-button @disable-if="self.testIsReady"
(click)="self.testIsReady = true">
Set ready
</mdl-button>
This is the style used for the .not-ready
class effect in the above example.
<style>
.not-ready {
opacity: 0.5;
animation: pulse .5s infinite ease-in-out;
}
</style>
Interoperability
Using a component from another
Inside the component's view, it's also possible to reference other components loaded in the page by adding the z-using
option attribute to the host element.
This option attribute will contain a comma separated list of the contexts' identifiers of required components.
A variable for each referenced context id, with its name equals to the component id converted to camel case, will be then available in the component's scripting scope.
<div z-using="my-menu">
<!-- 'myMenu' component is injected
in this scripting scope -->
<mdl-button (click)="myMenu.show()">
Open menu
</mdl-button>
</div>
<context-menu z-context="my-menu">
<template #menu>
<button>Option 1</button>
<button>Option 2</button>
<button>Option 3</button>
</template>
</context-menu>
In the example above the div
containing the button is referencing the component with context id my-menu
, which is the context-menu
from zKit components.
So, in the click
event handler of the button it is possible to address the menu directly and make it open calling the show()
method of the context-menu
component.
It's also possible to add the using
attribute directly on a scoped script
.
<script type="jscript" for="my-component"
using="color-select,other-component">
// ...
let color = colorSelect.getSelected();
let test = otherComponent.test;
// ...
</script>
The name of the variable assigned to contexts referenced with the z-using/using
attribute can also be explicitly assigned by using the as
keyword:
<div z-using="my-menu as m">
<button (click)="m.show()">Open Menu</button>
</div>
Exposing public methods or properties
With a scoped script 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>
Active→Refresh handlers
Active→Refresh handlers are user-definable functions that are associated with elements of a component's view and that, like the default refresh handler, 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.
Built-in @
handlers
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)
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 component context with id form-test
is created on a div
container. It's then possible to use @
handlers and other component features inside its view.
There, four fields are declared using the #<field_name>
attribute: #check1
, #check2
, #check3
and #proceed-button
. Declared fields are then available as variables in the scoped script that is put at the end of the view and that adds a few more declarations to the scripting scope.
<div z-context="form-test">
<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"
(click)="handleClick">
Proceed
</button>
<div @hide-if="!proceedButton.disabled">
Please check both options in order to proceed.
</div>
<script type="jscript">
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>
</div>
The two state variables validFormData
, bothChecked
, and the function handleClick
, declared in the scoped script, can be employed in the view 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"
(click)="handleClick"> Proceed </button>
@sync
and two-way binding
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.
When using the data model to update component's field, the update is instantaneous because data model is implemented using the platform's built-in Proxy object, there's no need in using any refresh loop.
<div 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 currently 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 (click)="cm.color = 'Black'" class="color-Black"></div>
<div (click)="cm.color = 'DarkRed'" class="color-DarkRed"></div>
<div (click)="cm.color = 'GoldenRod'" class="color-GoldenRod"></div>
<div (click)="cm.color = 'LimeGreen'" class="color-LimeGreen"></div>
<div (click)="cm.color = 'DarkGreen'" class="color-DarkGreen"></div>
</div>
<script type="jscript" using="color-select">
const cm = colorSelect.model();
</script>
</div>
Clicking a color will then assign a new value to the "color" field of the data model, (click)="cm.color = '<color_name>'"
, and the data binding will do the rest by synchronizing the color-select component automatically.
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.
This is the case of the @sync
handler, that it's not a refresh-based handler, but rather an event based one, and will not call the refreshCallback.
The following code, for instance, 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.
Observables
zuix.observable(...)
is a helper method built around the Proxy
object, and returns an observable instance of the given object in order to be able to detect changes made to it.
const formState = {
validFormData: false,
bothChecked: false
};
const state = zuix.observable(formState).subscribe({
change: function(target, key, value, path, old) {
if (key === 'validFormData') {
if (value === true) {
$label.html('Let\'s go! =)');
} else {
$label.html('Proceed');
}
}
}
}).proxy;
// ...
// the line below, for example, will trigger
// the `change` callback implemented above
state.validFormData = true;
// ...