Events and Active→Refresh

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:

NameDescription
thisThe target HTMLElement
$thisSame as "this", but ZxQuery-wrapped
_thisIf 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
contextThe component's context that contains the target element
<context_id>Alias of context, with the name of the component's contextId transformed to camelCase
modelThe component's data model, shortcut of context.model()
$The component's view as ZxQuery object, shortcut of context.$
argsOptional 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>
Test LocalTest Global

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>
Not ready yet =/
Set ready

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>
Open 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.

try Example on CodePen

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:

Next to any active→refresh handler it is also possible to specify the following options:

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>

report
Please check all options to proceed.

@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;

// ...
View
Controller
GitHub logo
JavaScript library for component-based websites and applications.