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 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:
- Responders are the objects which provide potential handlers for a signal.
- Responder chains are ordered sequences of responders.
- Signal-specific policies are used to compute the chains.
- Policies are selected based on both signal origin and signal type.
- 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 inhandle*
methods. - Handlers declared via
observe
reside inTP.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 fromTP.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.