Cookie Consent by FreePrivacyPolicy.com TIBET
TIBET Logo

TIBET

Signaling

Signals & Exceptions


Wins

  • Single unified approach to dealing with async interfaces of various types.
  • Simple semantic event firing from DOM elements via on: attributes.
  • Boilerplate-free responder signaling with integrated controller stack.
  • Change notification via TIBET set, fully-integrated with Data Binding.
  • Recoverable error handling via TIBET Exception signal types.

Contents

Concepts

Cookbook

Triggering

Observing

Handling

Signals

Signal Data

Origins

Responders

Handlers

Signal Mapping

Change Notification

Code

Concepts

The three things a web application needs to communicate with most: the user, the browser, and the server, all operate asynchronously.

To unify interactions with these elements while maintaining loose coupling between application components TIBET relies on an integrated signaling subsystem.

Via TIBET signaling you can trigger semantic events, respond to UI events, handle input from a web socket, communicate with a worker thread, Request services from a TIBET Service, raise or handle TIBET Exceptions, respond to Route, State, or object change notifications, leverage data binding, and much more.

TIBET Signaling
Signaling Overview

TIBET signaling focuses on finding/invoking handlers for TP.sig.Signal instances.

Explicit registrations via TP.observe are kept in TP.sig.SignalMap.INTERESTS. Component registrations via defineHandler are stored as handle* methods.

When a signal triggers via TP.signal() or TP.raise() a signal-specific firing policy is used to find and invoke handlers. All policies check the explicit interest map, non-DOM policies also check for handlers along the signal origin's responder chain.

Unified Approach

TIBET provides APIs that allow you to register handlers explicitly (TP.observe) or by component (defineHandler). Once registered your handler will be invoked in response to signals based on each signal's firing policy. For W3C compatibility TIBET DOM* signals only check explicit registrations. We recommend using ResponderSignal observations for most UI work since these signals also check responder chain registrations.

Semantic DOM Firing

DOM Events are generic, providing little or no semantic value; a click is a click is a click. TIBET allows you to change this via on: attributes. For example, you can alter a native DOM click to be a SaveEmployee signal using on:click as shown here:

<button on:click="SaveEmployee">Save</button>

Responder Firing

Events remapped via on: as well as most signal instances generated by TIBET use what we call responder firing. This approach checks a signal's potential responders such as tag types and controllers. Responder signal handlers are normally registered implicitly via defineHandler, eliminating the need for properly paired observe and ignore calls.

Change Notification

All TIBET types as well as native Array objects can signal Change in response to updates via set or other mutators. Observing Change is a central aspect of MVC and a central feature of TIBET. Change notification from URIs/URNs drives TIBET Data Binding.

Recoverable Exceptions

Native try/catch logic can be too final for many application errors since it alters the call stack. TIBET's TP.sig.Exception is a Signal type used by TP.raise() to signal a potentially recoverable error. Signaling does not flush the call stack like try/catch.

Cookbook

Triggering A Signal

You can trigger a signal a variety of ways, by signaling from a String, Signal type, or Signal instance, by using the TP.signal primitive, or by signaling from the origin itself.

Signaling via fire()

String, Signal type, and Signal instance triggering all rely on a fire method which takes a signal origin, an optional signal payload, and an optional firing policy.

Here's the method signature for the fire call, which works on Strings, Signal types, and Signal instances:

//  `fire`
function(anOrigin, aPayload, aPolicy) {}

Below are a few examples of using fire() from String sources:

//  Fire a 'SignalName' signal with no origin, payload or policy.
'SignalName'.fire();

//  Fire a 'SignalName' signal from the 'Fluffy' origin with a
//  TP.core.Hash containing some payload data:
'SignalName'.fire('Fluffy', TP.hc('age', 2));

You can also fire() a Signal type or instance:

//  Fire directly from a Signal type:
TP.sig.Change.fire('currentEmployee', TP.hc('aspect', 'lastName'));

//  Fire a constructed Signal instance:
sig = TP.sig.Change.construct();
sig.fire('currentEmployee', TP.hc('aspect', 'lastName'));

Signaling via signal()

When you are working within an object method or other context where you have an origin you can signal directly from that origin:

//  Signal from an origin, in this case the Array instance:
arr = [1, 2, 3];
arr.signal('Change');

You can also use TP.signal() which takes origin, signal, payload, and policy:

arr = [1, 2, 3];
TP.signal(arr, 'Change');

Signaling via raise()

Exception signals are triggered via the raise() call, either from the origin or via the TP.raise primitive:

...
if (arguments.length < 1) {
    return this.raise('InvalidParameter');
}
...

if (TP.notValid(employee)) {
    return TP.raise('EmployeeValidator', 'InvalidEmployee');
}

Observing A Signal

Observation is TIBET's term for expressing interest in one or more signals.

Observing via defineHandler

The preferred method for expressing interest in a signal is defineHandler, a method found on all TIBET objects which lets you associate handler logic with an object.

Using defineHandler ultimately results in the invocation of defineMethod with a normalized method name which ensures proper handler matching can occur.

APP.hello.Application.Inst.defineHandler('Fluffy', function(signal) {
    APP.info('Saw ' + signal.getSignalName() + ' from: ' + signal.getOrigin());
});

Because defineHandler ultimately defines methods it is the preferred approach since the resulting methods can be inherited, overridden, or otherwise specialized.

Here's the full description of the defineHandler method:

TP.defineMetaInstMethod('defineHandler',
function(aDescriptor, aHandler, isCapturing) {

    /**
     * @method defineHandler
     * @summary Defines a new signal handler. The descriptor can be a
     *     simple string when describing just a signal name, or you can
     *     provide a Hash or Object defining additional criteria such
     *     as origin, state, capturing, etc. which further restrict when
     *     the handler should be matched for use with a signal.
     * @param {String|Object} descriptor The 'descriptor' parameter can
     *     be either a simple String denoting signal name or a property
     *     descriptor. That property descriptor should be a plain JS
     *     object containing one or more of the following properties:
     *          signal (TIBET type or String signal name)
     *          origin (object or String id)
     *          state (String state name)
     *          phase (TP.CAPTURING, TP.AT_TARGET, TP.BUBBLING).
     * @param {Function} aHandler The function body for the event
     *     handler.
     * @param {Boolean} [isCapturing=false] Should this be considered a
     *     capturing handler? Can also be specified via
     *     'phase: TP.CAPTURING' in the descriptor property.
     * @return {Object} The receiver.
     */

Observing via observe()

The observe() calls in TIBET let you express an explicit interest in one or more signals from one or more origins. The observe API, much like the signal() API, is available both in primitive form and as an object method.

First let's look at the object method forms:

//  Define an observation in which the object expressing interest is
//  the handler. In this form the object's `handleChange` method will
//  be called automatically. This approach is recommended since handler
//  functions are methods which can be inherited or specialized.
this.observe(currentRecord, 'Change');

//  Define an observation, providing an explicit handler function. In
//  this case the function provided will be invoked in response to
//  'Change' signals from currentRecord object.
this.observe(currentRecord, 'Change', function(aSignal) {...});

You can also use the TP.observe primitive to express interest:

//  The primitive version always requires a handler, but it can be
//  either an object containing an appropriate method, or a function:
TP.observe(currentRecord, 'Change', recordUpdater);
...
TP.observe(currentRecord, 'Change', function(aSignal) {...});

Ignoring via ignore()

To stop observing use the primitive or method form of TIBET's ignore call:

this.ignore(currentRecord, 'Change');
...
TP.ignore(currentRecord, 'Change', recordUpdater);

You can also suspend() and resume() observations if you like. Note that these operations only apply to observe operations, not those done via defineHandler.

Handling A Signal

Handler Basics

All TIBET signal handlers are invoked with the specific signal instance as their one and only parameter. This object contains common signal data you can access to drive your handler and it's processing:

APP.hello.Application.Inst.defineHandler('Fluffy', function(signal) {
    //  Common data for signals is the signal name and origin.
    APP.info('Saw ' + signal.getSignalName() + ' from: ' +
        signal.getOrigin());

    //  Log any payload data as well. Payloads vary by signal type but
    //  allow you to pass what amount to arguments to signal handlers.
    var payload = signal.getPayload();
    if (TP.notEmpty(payload)) {
        APP.info(TP.str(payload));
    }
});

Stopping Signal Propagation

As you might expect TIBET provides simple methods on Signal instances which allow you to stop their propagation. You can use stopPropagation to set the propagation flag and shouldStop to test it.

APP.hello.Application.Inst.defineHandler('Fluffy', function(signal) {
...
    signal.stopPropagation();
});

//  Test the status of the propagation flag...although this is
//  contrived since the signaling system would likely have skipped
//  invocation:
APP.hello.Application.Inst.defineHandler('Fluffy', function(signal) {
...
    if (signal.shouldStop()) {
        //  processing last level of handlers...do something special
    }
});

Variations of these methods also exist for immediately stopping, namely stopImmediatePropagation and shouldStopImmediately. These variants are used to stop propagation within a specific "level" of handler, particularly during DOM firing. See the W3C standard for more specific details.

Preventing Default Actions

TIBET's signal instances respond to preventDefault and shouldPrevent, methods which allow you to set/get the current status of the signal with respect to default action processing.

APP.hello.Application.Inst.defineHandler('Fluffy', function(signal) {

    //  do some stuff...then:
    signal.preventDefault();

    return;
});

//  Check status and act accordingly:
APP.hello.Application.Inst.defineHandler('Fluffy', function(signal) {

    if (signal.shouldPrevent()) {
        //  skip doing the default stuff...
    }

    return;
});

Signals

Signals are TIBET's answer to Events in native JavaScript. In TIBET when you want to notify observers something has happened you trigger a Signal. When a native event has occurred TIBET automatically wraps it in a Signal and triggers it for you.

All native event-handling in TIBET is ultimately transformed into Signal handling. Handlers invoked by TIBET take a Signal as their first parameter, even handlers whose ultimate goal is to process a native Event.

In response to a signal TIBET computes what we call a responder chain, a list of objects that should be searched for matching handlers. Once computed, each signal's responder chain is scanned and any matching handlers are invoked and passed the Signal.

Signal Categories

There are five primary categories of TIBET Signal to be aware of:

  • Event signals
  • DOM signals
  • Responder signals
  • Generic signals
  • Exceptions

Event Signals

Event signals are the subset of TIBET Signal types which wrap native Event instances. There's an event-specific subtype in TIBET for each Event type and the methods on these signal types ensure cross-container Event differences are handled properly.

You don't normally create and fire Event signals, they're created by the signaling system after the low-level Event is captured by TIBET. This is particularly true with respect to DOM signals which TIBET handles through a single document-level handler.

When working with a native object such as a worker thread, web socket, or other component which might normally rely on onmessage or other callback interfaces you can use one of TIBET's types to interface with that component instead. These types provide Promise and/or signaling interfaces which eliminate the need for low-level callbacks.

If you must you can create and fire low-level event signals for unit tests, automation scripts, or other purposes. Doing so will cause a synthetic event to be constructed and wrapped. Any handlers that are invoked will be unaware of the difference.

For non-DOM interfaces see the documentation on each individual TIBET type for the specific Promise interfaces and/or signals used to interface with that component. For DOM-specific signals read on.

DOM Signals

TIBET's DOM* signal types are Event signals specific to standard browser DOM Events like click, keyup, change, etc. Each of the low-level DOM events has a counterpart (such as DOMClick for click) which is fired by TIBET automatically.

You never have to use addListener or other low-level DOM APIs to observe DOM events in TIBET. Instead, TIBET uses a single document-level handler on each window or iframe which routes events into the signaling system for processing. In addition, objects you wish to observe do not need to exist provided you know a public ID you can observe.

To register a handler for a DOMClick event you use TIBET's observe API, either TP.observe or obj.observe as your application requires. These APIs create explicit handler registrations in TIBET's TP.sig.SignalMap.INSTANCES dictionary. Registrations done in this fashion use string IDs rather than hard object references, avoiding problems with memory leaks, handler re-registration upon redraw, and many other issues.

In the example below we define an observation for a low-level DOM click via TIBET's native event wrapper DOMClick and then trigger it by signaling one directly:

//  NOTE we need to use `observe` for DOM signals, not `defineHandler`
TP.sys.getApplication().observe(TP.ANY, 'DOMClick', function(signal) {
    APP.info('Got a click event: ' + TP.dump(signal.getEvent()));
});

TP.sig.DOMClick.construct(TP.hc('x', 123, 'y', 456)).fire();

In response to our DOMClick the JavaScript console should print something like:

Got a click event: TP.core.Hash :: (x => 123, y => 456)

Note that for consistency with native DOM event firing all of TIBET's DOM* signals use DOM-centric responder chains. This implies that DOM events do not incorporate the TIBET controller stack, nor do they leverage TIBET's defineHandler API. You must use observe() to express interest in a DOM signal.

Responder Signals

As mentioned in the previous section TIBET maintains consistency with standard DOM dispatch logic for all DOM* signals. Unfortunately, adhering to the DOM standard limits UI event dispatch in ways that can negatively impact application development.

For more control and flexibility TIBET triggers what we call responder signals in addition to DOM signals. These signals include things like UIActivate, UIDeactivate, UIFocus, UIBlur, UIFocusNext, UIFocusPrevious, UIOpen, UIClose, and many others.

The key difference between responder and DOM signals is that responder signals use a TIBET-specific firing policy. Responder signals are tag-aware, can delegate to a tag-level controller, and include TIBET's controller stack in their responder chain.

As a concrete example, our earlier DOMClick event could have been observed via a UIActivate handler on a hypothetical hello:app tag using:

//  Assume the 'app' tag for a `hello` project, we can define a handler as:
APP.hello.app.Inst.defineHandler('UIActivate', function(aSignal) {
    APP.info('UIActivate');
});

With the previous handler definition you can now activate any hello:app tag via mouse or keyboard and the console will log. All defineHandler registrations are component level registrations, eliminating the need for addListener or observe entirely.

Interactions with TIBET's custom tags are best handled via responder signals and TIBET automatically triggers most of these in response to standard operations such as focusing, blurring, valid data, invalid data, changes in required state, etc.

Here's the current list:

TP.sig.ResponderSignal.getSubtypeNames(true).sort().join('\n')

"TP.sig.ResponderInteractionSignal
TP.sig.ResponderNotificationSignal
TP.sig.UIActivate
TP.sig.UIAlert
TP.sig.UIBlur
TP.sig.UIBusy
TP.sig.UIClose
TP.sig.UICollapse
TP.sig.UIDataConstruct
TP.sig.UIDataDestruct
TP.sig.UIDataFailed
TP.sig.UIDataReceived
TP.sig.UIDataSent
TP.sig.UIDataSerialize
TP.sig.UIDataSignal
TP.sig.UIDataWillSend
TP.sig.UIDeactivate
TP.sig.UIDelete
TP.sig.UIDeselect
TP.sig.UIDidActivate
TP.sig.UIDidAlert
TP.sig.UIDidBlur
TP.sig.UIDidBusy
TP.sig.UIDidClose
TP.sig.UIDidCollapse
TP.sig.UIDidDeactivate
TP.sig.UIDidDelete
TP.sig.UIDidDeselect
TP.sig.UIDidDuplicate
TP.sig.UIDidEndEffect
TP.sig.UIDidExpand
TP.sig.UIDidFocus
TP.sig.UIDidHelp
TP.sig.UIDidHide
TP.sig.UIDidHint
TP.sig.UIDidIdle
TP.sig.UIDidInsert
TP.sig.UIDidOpen
TP.sig.UIDidPopFocus
TP.sig.UIDidPushFocus
TP.sig.UIDidScroll
TP.sig.UIDidSelect
TP.sig.UIDidShow
TP.sig.UIDisabled
TP.sig.UIDuplicate
TP.sig.UIEdit
TP.sig.UIEnabled
TP.sig.UIExpand
TP.sig.UIFocus
TP.sig.UIFocusAndSelect
TP.sig.UIFocusChange
TP.sig.UIFocusComputation
TP.sig.UIFocusFirst
TP.sig.UIFocusFirstInGroup
TP.sig.UIFocusFirstInNextGroup
TP.sig.UIFocusFirstInPreviousGroup
TP.sig.UIFocusFollowing
TP.sig.UIFocusLast
TP.sig.UIFocusLastInGroup
TP.sig.UIFocusNext
TP.sig.UIFocusPreceding
TP.sig.UIFocusPrevious
TP.sig.UIHelp
TP.sig.UIHide
TP.sig.UIHint
TP.sig.UIIdle
TP.sig.UIInRange
TP.sig.UIInsert
TP.sig.UIInvalid
TP.sig.UIOpen
TP.sig.UIOptional
TP.sig.UIOutOfRange
TP.sig.UIReadonly
TP.sig.UIReadwrite
TP.sig.UIRefresh
TP.sig.UIRequired
TP.sig.UIScroll
TP.sig.UISelect
TP.sig.UIShow
TP.sig.UIStateChange
TP.sig.UIValid
TP.sig.UIValueChange"

Generic Signals

In TIBET a generic signal is a signal that doesn't fit into a specific category such as Event, responder signal, or Exception. Generic signals are often fired using strings.

You can trigger a generic signal easily by using a string with the signal name:

'Foo'.fire();

That's it. Using the fire() method on a string converts that string into a Signal instance with a signalName matching the string. The fire method then queues the signal and any matching handlers in the signal's responder chain will be invoked.

For generic signals TIBET normally includes the current controller stack in the responder chain, meaning you can see generic signaling in action by defining a handler for your application instance (always at the top of the stack) and then triggering the signal.

//  Assume a 'hello' app with default application type...
APP.hello.Application.Inst.defineHandler('Foo', function(signal) {
    alert('Foo!');
});

//  Trigger the signal and we should see an alert();
'Foo'.fire();

If your signal needs custom behavior or needs to inherit from a specific Signal supertype you can use defineSubtype() to create a specific signal type, then fire from that:

//  Define a handler to help us see when the signal is handled.
APP.hello.Application.Inst.defineHandler('FancySignal', function(signal) {
    alert('Fancy!');
});

//  Create a new signal subtype and fire an instance of that via the type:
TP.sig.Signal.defineSubtype('FancySignal');
TP.sig.FancySignal.fire();

If you need to define a specific payload or other instance data you can construct the signal instance first, then fire it:

//  Assume we have the FancySignal type from the prior example:
var sig = TP.sig.FancySignal.construct(TP.hc('foo', 'bar'));
sig.fire();

To fire a signal from a specific origin use signal instead:

arr = [1,3,5];
arr.signal('Change');

The signal method is used when triggering a signal from a specific origin and also takes parameters for payload, policy, etc. See TIBET's API documention for more.

Exception Signals

JavaScript incorporates try/catch error handling triggered by the throw() call. This standard error-handling mechanism is extremely useful but limited in that you can't recover and continue from a throw. TIBET Exception is different.

TIBET includes a signal-based Exception subsystem that relies on a hierarchy of Signal subtypes rooted at TP.sig.Exception. When you encounter a problem in your TIBET code you should raise() an Exception instead of throwing an Error:

this.raise('FileAccessException', ...);

The raise() call is a variation of the signal() call specific to exception signals. Handlers in your code are automatically invoked to process the Exception as with any Signal type. If any handler invokes preventDefault() the exception is considered handled, otherwise after all handlers have run a throw() call is made for you.

Exception types in TIBET are often constructed in hierarchies to support handling at different levels of abstraction, just as you might expect. For example, you might define FileAccessException as a subtype of IOException which is a subtype of Exception. Using defineHandler with any of these types will cause your handler to be invoked since TIBET dispatches exception signals using an inheritance-aware firing policy.

If there are handlers for the Exception's supertypes and you want to avoid having them invoked you can call stopPropagation in any handler and the exception will stop propagating through the inheritance chain and any additional handlers.

APP.hello.Application.Inst.defineHandler('FileAccessException', function(signal) {
    var payload = signal.getPayload();

    APP.info(signal.getSignalName() + ': ' + TP.dump(payload));

    //  Exceptions fire via inheritance-aware processing. If we leave
    //  this out we'll continue looking for a handler for supertypes etc.
    signal.stopPropagation();

    //  Consider this exception handled so don't `throw` at the end of
    //  the exception firing process.
    signal.preventDefault();
});

By using signaling to support exception handling TIBET gives you a way to perform comprehensive and recoverable error handing. Exceptions don't imply the call sequence will be terminated and the call stack altered as with try/catch.

Signal Data

Generic signals are typically triggered using signal or fire. Event signals trigger in response to native Events. Exceptions are triggered via raise().

In all three cases TIBET uses a twist on "Who, What, Where, When, Why, How?" with respect to the information it likes to have regarding a particular signal:

  • Who: The origin of the signal in TIBET terms. For DOM signals the target.
  • What: The signal name/type: a string, Signal type or Signal instance.
  • Where: Optional identifier for distributed signaling. XMPP signals only.
  • When: Timestamp for the signal. Created automatically, not a parameter.
  • Why: Payload information (often a dictionary or Error instance).
  • How: Responder/Dispatch policy specification. Literally how to process.

Since Where and When are more internal you typically use origin, signal, payload, and policy. These are the parameters you'll see in some form in all TIBET signaling.

Generic signals are often fired with a simple combination of origin and signal type. If the signal needs to communicate other data that can be passed to the fire() or signal() call as a payload parameter:

//  Signal `Foo` from an object and include payload data.
this.signal('Foo', TP.hc('bar', 1, 'baz', 2 ));

Event signals typically contain a normalized Event object as their payload. The origin is set to the DOM target if there is one. Otherwise any information from the Event which might provide a meaningful origin is used.

Exceptions raised outside of a try/catch typically contain a payload with an error message and contextual information on the error. Exceptions raised within a try/catch typically contain an Error instance as their payload.

// NOTE: Strings for Exceptions are automatically localized.
this.raise('InvalidParameter', TP.hc('msg', 'Age cannot be negative.'));

Whether it's a generic signal, a DOM event wrapper, a UI signal, or an Exception, the API for accessing meaningful signal instance data can be summarized as:

//  Get the origin, the object that originated the signal:
signal.getOrigin();

//  Get the signal name, the actual (current) name of the instance:
signal.getSignalName();

//  Get the payload, error, or other content of the signal:
signal.getPayload();

Origins

There is no way to create an observation which relies on a direct memory reference in TIBET. This is by design since such references are a source of memory leaks and maintenance overhead. Instead, all explicit observations are based on IDs.

Object IDs

Every object in TIBET has a unique OID which is auto-generated. Objects can also be assigned a public ID which is independent of the internal OID. For element wrappers this public ID is a normalized form of the element's id attribute if present.

You can access an object's ID via getID, which will return the internal OID if no public ID has been set. Once you set an ID that value is returned:

arr = [1,2,3];

//  Get the ID (defaults to OID):
arr.getID();
"Array$1ac8in81dhnv4rnehqv8"

//  Set and output a public ID:
arr.setID('123');
arr.getID();
"123"

//  Access the internal OID:
arr.$getOID();
"Array$1ac8in81dhnv4rnehqv8"

For non-DOM signal types the signaling system's approach to responder chain and handler processing means instance IDs are rarely required. Still, when signaling, all objects will provide their ID as the signal origin.

If you want to use a public name rather than the internal OID for signal origin the best approach is to leverage a TIBET URN, essentially a registered public name.

URNs as Origins

If you're not familiar with URN syntax the standard form is:

urn:<NID>:<NSS>

According to the URN specification the NID is a namespace identifier and the NSS is a namespace-specific string. When we create a URN in TIBET the default NID is tibet and the NSS is set to whatever string you provide, essentially the name.

In TIBET we sugar our URN constructors so that using urn::currentEmployee will effectively default the NID to tibet. You'll see us use urn::* in most of our documentation, just be aware the standard requires urn:tibet:.

In the following snippet we create a hypothetical instance of Employee and assign it to the URN urn::currentEmployee. Using TP.uc (TIBET's URI construct primitive), we can then access that instance from anywhere in TIBET:

var employeeInst = Employee.construct();
TP.uc('urn::currentEmployee', employeeInst);

URNs of this form are used extensively in TIBET Data Binding (which requires all data sources to be expressed as URIs of some form).

Responders

Put simply, responders are objects which make up the set of potential handlers for a signal. The ordered list of responders makes up what we call a responder chain.

Responder chain computation in TIBET depends on at least two key factors:

  • The type of the signal
  • The origin of the signal

Let's take a look at a familiar DOM signal to see how this works in practice.

DOM Responders

Assume the following HTML5:

<html>
<head>
  ...
</head>
<body>
  <header>...</header>
  <div class="content">
    <form>
      <button class="help" id="help-button">Help</button>
    </form>
  <div>
  <footer>...</footer>
</body>
</html>

Now, imagine a user clicks on the help-button element.

Using DOM bubbling semantics we can think of the click responder chain as:

button -> form -> div.content -> body -> html -> document

Now, many of these elements will not have a click handler installed, but the point is that any of them could and hence the list is what we call the responder chain.

Adjusting for DOM capture/at-target/bubble behavior the full responder chain is:

document -> html -> body -> div.content -> form     (capture phase)
button                                              (at-target phase)
form -> div.content -> body -> html -> document     (bubble phase)

This expanded list of elements (or more accurately their TIBET wrapper types) makes up the responder chain for this particular instance of click. A similar computation is done for any Event signal in TIBET that is DOM-based.

Tag Responders

As mentioned earlier, the DOM firing policy can be restrictive when it comes to factoring and reusing signal handling logic. For improved control TIBET typically triggers one or more responder signals based on actions in the UI.

While responder signaling looks similar to DOM signaling on the surface, the computation of the responder chain is significantly different.

Let's look at a slightly modified version of our earlier DOM:

<html>
<head>
  ...
</head>
<body>
  <header>...</header>
  <div tibet:ctrl="help:panel" class="content">
    <form>
      <button tibet:tag="help:button" id="help-button">Help</button>
    </form>
  <div>
  <footer>...</footer>
</body>
</html>

In the variation above we've got a tibet:tag reference for our help:button and a tibet:ctrl attribute on our div referring to help:panel.

Again, let's imagine a user clicks on the help-button element.

In responder firing the fully-computed responder chain for our signal will be:

ControllerStack -> help:panel -> help:button    (capture phase)
help:button                                     (at-target phase)
help:button -> help:panel -> ControllerStack    (bubbling phase)

Notice that in this case the responder chain is sparse, focusing entirely on elements that have tibet:tag (tag type) or tibet:ctrl (controller) attributes. This list is augmented by what we refer to as the controller stack (which we'll cover momentarily).

Let's recap.

For DOM signals the responder chain is always the target element's ancestor chain (adjusted for capture/bubble obviously). And of course, DOM signals do not traverse to entities outside the DOM.

Responder signals on the other hand focus on tag type and controller mappings which imply handler. Responder signals also explicitly incorporate the TIBET controller stack during both capture and bubbling phases.

The Controller Stack

As you may know, all TIBET applications include an Application object. You can get a handle to this object via TP.sys.getApplication() or APP.getApplication().

When computing responder chains for the majority of Signal types (DOM* signals are an exception) TIBET accesses the application instance and queries it to get the current list of controllers via getControllers(). The application itself is always in this list.

During the capturing phase the controller stack is traversed from the application instance in; during bubbling the order is inverted and traversed from the top-most controller to the application instance. This approach allows your application instance the first chance to capture a signal and the final opportunity to handle a bubbling signal.

pushController/popController

While your application runs you can use the pushController, popController, and setControllers API on the application instance to manipulate the list of controller objects. By doing so you alter signal handling via the responder chain.

Say your application opens a help panel. You might pushController a HelpController while the panel is open, then popController when the panel is closed.

While the help panel is open TIBET's responder chain computation will include the HelpController, allowing handlers on the HelpController to respond to signals.

Manipulating the controller stack in response to Route or State signals is another common way to keep application logic factored across controllers in TIBET.

Responder Recap

Here are the key points to keep in mind relative to responders:

  1. Responders are the objects which provide potential handlers for a signal.
  2. Responder chains are ordered sequences of responders.
  3. Signal-specific policies are used to compute the chains.
  4. Policies are selected based on both signal origin and signal type.
  5. The application instance and its controllers are often responders.

With that recap in mind let's move on to the last piece of the puzzle: Handlers.

Handlers

Handlers are functions invoked in response to signals.

When a signal is triggered the responder chain for that signal is computed (either directly or by component) and those objects are then checked for matching signal handlers.

In older versions of TIBET all handlers were registered using a common observe() call and de-registered using a parallel ignore() call. This is no longer necessary unless you are trying to handle a low-level DOM* signal (instead of the more powerful UI signals).

In TIBET 5.x you express interest in a signal by using the defineHandler call on an appropriate tag or controller type. These handlers will automatically be found during responder chain processing, eliminating repetitive sequences of observe/ignore.

The new TIBET 5.x API for defining handlers serves multiple purposes:

  • makes the code more self-documenting than anonymous handler functions
  • provides filtering metadata such as which states the handler supports
  • supports automated tooling for both searching and generating handlers

Using the defineHandler API ultimately calls defineMethod after normalizing the handler name to match strict naming conventions that are necessary for handler invocation.

Handler Naming

There are three main keys used in the definition of any signal handler name:

  • signal name
  • signal origin
  • application state

These keys define the axes along which handler lookup/filtering is done.

Each of our keys is assigned a prefix (handle, From, and When respectively). An optional 'Capture' can be included so the verbose form of every handler method name matches:

/^handle([A-Z0-9$][a-zA-Z0-9_]*?)(Capture)*(From([A-Z][a-zA-Z0-9_]*?))*?(When([A-Z][a-zA-Z0-9_]*?))*?$/;

That might be a bit hard to parse visually so let's look at a few concrete examples:

handleUIActivate                       => Any UIActivate signal
handleUIActivateCapture                => Capture UIActivate
handleUIActivateFromHelpButton         => UIActivate from id HelpButton
handleUIActivateWhenHelp               => UIActivate in Help state
handleUIActivateFromHelpButtonWhenHelp => UIActivate of HelpButton in Help state

Thanks to defineHandler you don't have to remember all this. You just sparsely provide the signal name, phase, origin, and state your handler cares about along with the actual handler function and TIBET ensures your handler method is named properly.

Handling Multiple Signals

You can leverage TIBET's inheritance-aware signaling to observe branches of the signal hierarchy, or observe 'Signal' to see signals of all types. For example you can observe all Signals which reach the application by using:

TP.hello.Application.Inst.defineHandler('Signal', function(signal) {
    APP.info('Application handled: ' + signal.getSignalName());
});

In this case the handler will explicitly match all signals (and implicitly match all origins and states). As a result the console will log the specific signal names for every event which has Application in the responder chain which isn't stopped before it bubbles.

Alternatively you can use TIBET's observe API to observe a list of potentially unrelated signals (at least in terms of inheritance):

TP.sys.getApplication().observe(TP.ANY, ['Fluffy', 'Fuzzy'],
function(signal) {
    APP.info('Application handled: ' + signal.getSignalName());
});

In the code above we've registered interest in two hypothetical generic signals, Fluffy and Fuzzy, coming from any origin.

We can verify this observation using the following triggering code in a console:

> TP.signal(TP.ANY, 'Fluffy');
Application handled: Fluffy
> TP.signal(TP.ANY, 'Fuzzy');
Application handled: Fuzzy
>

Handling Multiple Origins

Observing events from any origin is easy as long as you keep responder chain implications in mind (handlers only fire for valid responders).

Below we're observing UIActivate events from any origin:

APP.core.Application.Inst.defineHandler('UIActivate', function(signal) {
    APP.info('click event at: ' + signal.getOrigin());
}, true);

First, notice that we assign the handler to an object which is in the responder chain for UIActivate. Since we're trying to handle events from any origin we need to place this on either a document-level handler or in the controller stack.

Second, we provide true as a third parameter to define this as a capturing handler. The capture flag is helpful for our demo. If elements chose to stop propagation of their activation events our handler might never see them.

As with signals we can use TIBET's observe API to observe a list of origins:

//  Define an array of origins and mark it as being an origin set.
//  This is necessary so we don't simply observe the array instance
//  as the origin.
origins = ['Foo', 'Bar'];
origins.isOriginSet(true)

//  Use `observe` with the origins list as the origin to observe:
TP.sys.getApplication().observe(origins, 'Fluffy',
function(signal) {
    APP.info('Application handled: ' + signal.getSignalName() +
        ' from: ' + signal.getOrigin());
});

//  Signal from 'Foo'
'Foo'.signal('Fluffy')
Application handled: Fluffy from: Foo

//  Signal from 'Bar'
'Bar'.signal('Fluffy')
Application handled: Fluffy from: Bar``js

Handling States

State-specific filtering follows the patterns outlined for signal name and origin. For example we can implement application click processing while in a Help state via:

APP.hello.Application.Inst.defineHandler({signal: 'UIActivate', state: 'Help'},
function(signal) {
    TP.todo('Show help for the control as: ' + signal.getOrigin());
    return;
}, true);

In this case we're taking advantage of a descriptor as the first parameter to defineHandler which allows us to specify both a signal name and application state which much match for our handler to trigger. Additional options include phase and origin to support filtering based on capture/bubble/at-target or by signal source.

Handler Recap

Here are the key points to keep in mind relative to handlers:

  • Handlers are functions invoked in response to signal triggers.
  • Handler names use 'handle', 'From', and 'When' for type, origin, and state.
  • Handlers declared via defineHandler result in handle* methods.
  • Handlers declared via observe reside in TP.sig.SignalMap.INTERESTS.

Signal Mapping

As mentioned earlier, the Event objects created by the native environments of both client and server are powerful, but they lack any kind of semantic meaning with respect to applications. A click is a click is a click. A keyup is a keyup. Etc.

Unfortunately, a one-event-fits-all approach for click or mousemove or keyup rarely scales well to widgets and applications. What you often want is a semantically meaningful event, something like HelpRequest not click.

Default Mapping

When an event is triggered from an element both the element's type and the event's signal type are queried to determine how things should proceed. One of these queries is "What signal name should be used?"

As an example, TIBET's UIActivate signal is a translation of any event which can cause a control to activate (click and certain keyboard events). Other examples are UIFocus, UIBlur and other TIBET's ResponderSignal subtypes, which are often translations of low-level DOM* events.

These default mappings are typically wired in to the low-level event handling machinery in TIBET, but you can also create mappings yourself via markup.

on: Signal Mapping

Semantic events are just as important to maintainability as semantic markup. We wanted to be able to quickly map native events to semantic signals directly from markup.

TIBET lets you remap native DOM events via a custom on: namespace. When an on:{event} attribute is used, events with the attribute name are remapped to the attribute value. For example, on:click remaps click, on:focus remaps focus, and so on.

The following markup tells TIBET you want click events from the Help button to pass through the signaling system as HelpRequest signals:

<button id="Help" on:click="HelpRequest">Help</button>

By translating events into semantic signals you not only optimize event dispatch you dramatically improve the readability and maintainability of your code.

Thanks to defineHandler and the naming conventions it supports your tags and controllers have handleHelpRequest, handleAboutRequest, and other semantic signal handlers, handlers which could be triggered by a wide variety of means other than via a mouse click. The result is code that's far more granular, literate, and easy to maintain.

One additional benefit of using the on: namespace attribute to remap a signal is that signals mapped in this fashion are fired as responder signals, meaning the pick up the ability to leverage tag types, tag controllers, and the TIBET controller stack.

Change Notification

The majority of our discussion to this point has focused on what we might call top-down signals, i.e. signals which originate as events in the UI layer and are sent downward through the application layers toward a responder.

Some of the most interesting signals are those which work their way bottom up from a socket, worker thread, or other source; signals that cause one or more objects to undergo a state change that needs to be reflected elsewhere.

The classic name for such objects would be models.

Change notification is a core functional characteristic of Model/View/Controller (MVC) designs as well as virtually all of their alternatives.

In TIBET every object is capable of signaling Change. It's not necessary to inherit from a particular type to serve as a data source, nor are change notifications restricted to the UI or special types. Any object can observe any other object for Change.

Change Signals

When an object's set() method is invoked TIBET performs simple checks to see if the inbound value matches the existing value. If so the operation is ignored. If the value is going to change however, TIBET will capture the old value, the new value, and the 'aspect' which is changing and trigger a notification.

As with UI signals the controller stack is a part of the responder chain for Change signals. This means, regardless of the nature of the object undergoing a state change, the object's Change events can be managed by the Application or other controller.

Inheritance-Based Dispatch

Change events are typically dispatched using an inheritance-aware policy. This means that the inheritance hierarchy of the signal is used to compute a signal name that changes as the dispatch process unfolds. An example helps to illustrate.

Let's assume we update the phone number of an employee instance. The result of that change is a ValueChange signal specific to the 'phoneNumber' aspect. From a TIBET perspective the signal's inheritance hierarchy is computed as:

TP.sig.PhoneNumberChange -> TP.sig.ValueChange -> TP.sig.Change

As the signal is dispatched TIBET looks for PhoneNumberChange handlers, followed by ValueChange handlers, followed by Change handlers. This approach allows you to manage change at whatever granularity makes sense.

Note that not all changes are value changes. For example, the readonly status of a field might change, which would result in signaling that the readonly aspect of the phoneNumber field changed. Specifics on the change are found in the signal payload.

ValueHolders

Smalltalk, which is arguably the origin of the MVC pattern, relied on a common idiom of a 'ValueHolder', an object that served as a fixed point for observation whose internal value could change over time. This is a common pattern for master/detail and other related situations where the observation needs to remain stable while the content changes.

Let's say we want to build a simple master/detail screen that lets us view details on employee records. The master portion of the screen is a simple table of employee rows. When the user selects one of the rows we want to update the details to reflect data from the currently selected employee record.

We start by registering a simple URN to serve as our ValueHolder via:

var currentEmp = TP.uc('urn::currentEmployee');

A URN's name and ID are synonymous which makes them particularly well-suited to serving as references to local JavaScript objects. If the URN provided already exists the TP.uc() call will return the existing instance. This latter feature means that if you know the public name by which things are registered you can access them from anywhere.

With our value holder URN in place, any time we want to set the current employee to a different object (such as in our master table's Click handler) we get a handle to that URN and set its content:

var currentEmp = TP.uc('urn::currentEmployee');

// Update content to be the latest emp record from the master table.
currentEmp.setContent(emp);

URI's setContent() method works just like set(), and signals ContentChange (the aspect changing is the 'content'). The result is that any handler in the responder chain watching for ContentChange from 'currentEmployee' will be invoked. If we've implemented the following handler we'll be notified every time the user clicks a new row:

APP.hello.Application.Inst.defineHandler({
    signal: 'ContentChange',
    origin: 'urn:tibet:currentEmployee'     //  use full NID here
}, function(signal) {
    // Update the detail form with data from signal.getPayload().
    ...
});

By using URN instances with well-known names you can easily connect observers to their data sources. This approach maintains a key level of indirection and isolation since no direct object references ever exist between components.

Change Notification Recap

  • Any object is capable of signaling Change (no special 'Model' type is needed).
  • Change notification leverages TIBET responder chains and inheritance.
  • Change notifications are triggered in TIBET by using the .set() method.
  • Change signals are generated based on the 'aspect' of the object that changed (i.e. .set('lastName', 'Smith') would trigger a 'LastNameChange' signal). These signals inherit from TP.sig.ValueChange, which inherits from TP.sig.Change.
  • URNs are useful as 'value holders' - objects which allow their contents to

Code

~lib/src/tibet/kernel/TIBETNotification.js contains the core signaling logic.

Individual signal types can be found throughout the TIBET codebase.