< PreviousChapter 6: Solution130The rationale and illustration for this invariant and its implication are: a UI di-alog, implemented by a target Component, for instance, cannot be made visible if the UI panel, implemented by its parent Component, is not at least also already visible. If this invariant did not hold, the UI would be in both an optically and tech-nically inconsistent state. The second invariant is:Figure 6.9: Example Component State Transitionsrootleaf654321Step 1rootleaf6543215Step 2rootleaf6543215Step 3rootleaf654321543Step 4rootleaf543621Step 5rootleaf5436213Step 6rootleaf5436213Step 7rootleaf543621345Step 8rootleaf345621Step 9Hierarchical Component States131This especially implies: before decreasing the state of a target Component, all its child Components first have to decrease their state to the target state of the target Component. As a special case, if a Component is destroyed, all child Components first have to be destroyed.The rationale and illustration of this invariant and its implication are: a UI di-alog, implemented by a target Component, cannot be made invisible if its sub-dia-logs, implemented by its child Components, are still visible. If this invariant did not hold, the UI again would be in both an optically and technically inconsistent state.As a further consequence and to simplify the Component Tree management, a corresponding rule usually is applied: a Component is only technically created and destroyed if it is a leaf Component of the Component Tree.Let us illustrate the implementation of the two invariants by the Component System in practice. Assuming we have a Component Tree as visualized in Figure 6.8 on page 129 and for the marked path of Components 1, 2...6 we have the corre-sponding initial states as shown in Step 1 of Figure 6.9 on page 130. In other words: initially, the root Component 1 has the highest state, and the leaf Compo-nent 6 has the lowest state.In Step 2 one triggers a state increase for Component 5. This state transition cannot be performed immediately, because, as Step 3 illustrates, this would violate Invariant 1 as Component 5 then would be at a higher state than its parent Com-ponents 4 and 3. As a consequence, as Step 4 illustrates, the Component System first recursively transitions Component 3 to the requested target state of Component 5, then transitions Component 4 to the same state and then finally transitions the originally Component 5 to the requested target state. This way, we finally arrive at the state status as seen in Step 5.A similar action is performed by the Component System for a state decrease of Component 3, as illustrated by Step 6. Again, this state transition can-not be performed immediately, because, as Step 7 illustrates, this would violate Invariant 2 as Component 3 then would be at a lower state than its child Compo-nents 4 and 5. As a consequence, as Step 8 illustrates, the Component System first recursively transitions Component 5 to the requested target state of Component 3, then transitions Component 4 to the same state and then finally transitions the originally Component 3 to the requested target state. This way, we finally arrive at the state status as seen in Step 9.6.5.4 Component State Transition AutomatismThe Component System implicitly transitions Components only to ensure that the invariants are never violated. Otherwise, only those Components perform a state transition, when explicitly requested to do so by the application. However, some-times it can be very convenient for a set of Components to perform state transi-The rationale of the in-variants is to ensure the UI, at any time, is in a opti-cally and technically con-sistent state.Chapter 6: Solution132tions as a group. For this, a controllable state transition automatism exists which manifests in two boolean flags on each Component: ■auto-increase: automatically transitions the Component to the same high-er/increased state as its parent Component. ■auto-decrease: automatically transitions the Component to the same low-est of the lower/decreased states of its child Components.The auto-increase flag is fully practically motivated and (in combina-tion with the Component State Invariant 2) can be used to ensure that a group of Components can be managed through a single “local root” Component. An example is the triad of View, Model and Control-ler Components where the View and Model Components are implicitly managed through the Controller Component thanks to auto-increase. Compare also Section 6.12.1 on page 176 for the Controller-Created Descendants pattern.The auto-decrease flag is more esoteric and (in combination with the Compo-nent State Invariant 1) could be theoretically used implicitly to manage a partial path of ancestor Components. An example is a managed Dialog, which needs an internally enclosing parent Dialog, which should automatically tear down its par-ent Dialog in case it is requested to transition to a lower state.In addition to these two boolean flags local to each Component, the Compo-nent System, for convenience reasons, could also provide two corresponding and similarly named inherited Component properties, too (see Subsection 6.6.1 on page 135). In this case, it is strongly recommended to ensure that the inheritance is properly cut off at certain Component Tree levels or potentially the entire Com-ponent Tree would auto-increase or auto-decrease too easily.6.5.5 Component State Transition GuardsThe operations performed in some enter/leave methods of a Component’s Backing Object can be inherently asynchronous. This is especially the case for I/O-based operations. In this case, the enter/leave method has to ensure that the Component System does not immediately continue its recursive state transition procedure, once the enter/leave method returned.For this, so-called Component State Transition Guards are provided by the Component System with the help of the Component API method guard(method: MethodName, guardLevel: Number): Void. The Transition Guards are very simi-lar to the concept of Semaphores and directly constrain the internal state transition management of the Component System as long as the guardLevel is positive. A fictive ComponentJS and jQuery based example follows:class DialogFoo { prepare () { cs(this).guard(“render”, +2) Model-View-Controller/Component-Tree (MVC/CT) Pattern in practice greatly benefits from the auto-increase feature. CoHierarchical Component States133 $.get(url1, (data) => { this.data1 = data cs(this).guard(“render”, -1) }) $.get(url2, (data) => { this.data2 = data cs(this).guard(“render”, -1) }) } render () { // executed only, once both // data1 and data2 were loaded } }Here, the guardLevel is first increased for the two expected following asynchro-nous operations and then twice in sequence decreased again by each of those asynchronous operations.An alternative API for guards would be to allow all enter/leave methods to re-turn a so-called Promise/Future object and then let the Component System auto-matically wait until this object gets resolved before the recursive state transition procedure continues.6.5.6 Component Resource Allocation SpoolingIn state enter methods, resources are usually allocated and in the corresponding leave method, it has to be properly de-allocated again. There always has to be a 1:1 correspondence between resource allocation and de-allocation, or over the lifetime of the User Interface, there would be an effective resource leak. The Com-ponent System provides the following Component API method for “spooling” de-allocation operations: spool(name: string, callback: () => void): void spooled(name: string): boolean unspool(name: string): voidWith this, instead of the usual allocation/de-allocation approach...class DialogFoo { prepare () { this.resource = new Resource() // work on this.resource } release () { this.resource.destroy() } }...the de-allocation can be attached directly to the allocation operation:Chapter 6: Solution134class DialogFoo { prepare () { let resource = new Resource() cs(this).spool(“foo”, () => resource.destroy()) // work on resource } release () { cs(this).unspool(“foo”) } }This, at the first spot, certainly still looks like a minor change, not even worth the effort. It just avoids the storage of the resource inside the component. Hence, the mechanism is extended one step further: the Component System knows about certain specially named spools: on state leaving, if a spool is named exactly like the state to be left, it is au-tomatically unspooled. This further reduces the allocation/de-allocation scenario to:class DialogFoo { prepare () { let resource = new Resource() cs(this).spool(“prepared”, () => resource.destroy()) // work on resource [...] } }Finally, as most of the allocation/de-allocation scenarios are actually from the area of the following Hierarchical Component Communication (see 6.6 on page 135), all those Component API methods will accept an optional flag spool: boolean, which internally automatically spools the corresponding deallocation operation onto the spool with a name of the return value of comp.state(). This finally allows the intended convenient allocations with fully automatic and implicit de-alloca-tions at the right time in the Component life-cycle:class DialogFoo { prepare () { cs(this).subscribe({ name: “foo”, spool: true, func: (event) => { [...] } }) } }In practice, the spool flag’s default value actually should be true, because one usually always wants an automatically handled resource deallocation, of course.The automatic spooling of standard HUICA com-munication deallocation operations is the key for a convenient and boiler-plate-free programming model in practice.Hierarchical Component Communication1356.6 Hierarchical Component CommunicationThink like a wise man, but communicate in the language of the people. — William Butler Yeats Once one orchestrates the User Interface in the strict Hierarchical Component Structure (see Section 6.4 on page 122) and manages them with the strict Hierar-chical Component States (see Section 6.5 on page 126), one idea suggests itself: to also perform a strict Hierarchical Component Communication. More-over, this actually is key for mastering the important architecture princi-ple Separation of Concerns in the behavior of the User Interface during run time. For this, five distinct communication mechanisms are provided by the Component System: ■Fetchable Resource Properties (see Subsection 6.6.1 on page 135) for at-taching information to Components. ■Observable Presentation Models (see Subsection 6.6.2 on page 136) for modeling the User Interface through an abstract data structure. ■Pluggable User Interface Fragment Sockets (see Subsection 6.6.3 on page 138) to hierarchically assemble the individual UI fragments of Compo-nents. ■Subscribable Information Events (see Subsection 6.6.4 on page 141) to hierarchically distribute events to a group of Components. ■Registerable Function Services (see Subsection 6.6.5 on page 144) to be able to hierarchically call services on Components.6.6.1 Fetchable Resource PropertiesSometimes, it is necessary to attach arbitrary information to a Component. Espe-cially, if it is not a Component with MVC/CT role Model (see Subsection 6.8.2 on page 151), and hence does not have a Presentation Model (see Subsection 6.6.2 on page 136) attached to it. For this, Fetchable Resource Properties are provided by the Component System with the help of the Component API method property:property(params: { name: string, value?: any = undefined, def?: any = undefined, scope?: string = ““, bubbling?: boolean = true }): any property( Hierarchical Component Communication is the ul-timate feature of HUICA!Chapter 6: Solution136 name: string, value?: any = undefined ): anyThe name and value parameters are the primary property information. Setting value to null removes the property. If no property is found at all, the value of pa-rameter def is returned.By default, a property get operation recursively “bubbles up” the Component Tree hierarchy, up to the root Component. In other words: if a property is not found on a Component, the parent Components are checked until no more parent Component exists. If parameter bubbling is set to false, this recursive operation is disabled, and a property get operation does not resolve on any parent Components at all.The properties can optionally be scoped with a child Component name or even a name path to a descendant Component: on each attempt to resolve the property, first, the scoped variants are tried. This means, if a property was set with name equal to “quux@bar” (or with parameter name set to just “quux” and parameter scope set to “bar”) on Component /foo, if one resolves the property with cs(“/foo/bar”).property(“quux”), one gets the value. However, if one resolves the property with cs(“/foo/baz”).property(“quux”), one does not get the value, as the property is scoped at /foo to the sub-tree /foo/bar. This mechanism allows a parent Com-ponent to set the same property with different values for different child Compo-nents and is required in practice to control complex Dialogs.Additionally, the scope can be a partial Component path, too. If a property was set with name equal to “quux@bar/baz” on Component /foo, if one resolves the property with cs(“/foo/bar/baz”).property(“quux”), one gets the value. How-ever, if one resolves the property with cs(“/foo/bar/baz2”).property(“quux”), one does not get the value. This allows one to skip so-called intermediate names-pace-only components.6.6.2 Observable Presentation Models When running the MVC/CT design pattern (see 6.8 on page 150) on the Compo-nent Tree, it is essential to have Observable Presentation Models on Components playing the Model role. The Component System provides the following Compo-nent API method for defining such a Presentation Model:model(spec: { [name: string]: { value: any; valid: (string|function|RegExp); autoreset?: boolean = false; } }): void;An example Presentation Model of a fictive login Dialog would be:Properties look like sim-ple variables, but are actually more powerful, because they are option-ally scoped and resolved hierarchically.Hierarchical Component Communication137cs(this).model({ “dataUsername”: { value: ““, valid: “string” }, “stateUsernameValid”: { value: ““, valid: “boolean” }, “dataPassword”: { value: ““, valid: “string” }, “statePasswordValid”: { value: ““, valid: “boolean” }, “stateLoginEnabled”: { value: false, valid: “boolean” }, “eventLogin”: { value: false, valid: “boolean”, autoreset: true } })Then, the Component System provides a Component API method for getting/set-ting single values of Presentation Models:value( name: string, value?: any = undefined, force?: boolean = false ): any; value(params: { name: string; value?: any = undefined; force?: boolean = false; }): any;Finally, the Component System provides a Component API method for observing changes to Presentation Model values:observe(params: { name: (string|string[]); func: (...args: any[]) => void; spool?: string = undefined; touch?: boolean = false; boot?: boolean = false; }): number; observe( name: (string|string[]), func: (...args: any[]) => void ): number; unobserve( id: number ): void;A fictive Presentation Logic then can be:cs(this).observe( [ “dataUsername”, “dataPassword” ], (username, password) => { let usernameValid = username !== ““ let passwordValid = password.length >= 8 cs(this).value(“stateUsernameValid”, usernameValid) cs(this).value(“statePasswordValid”, passwordValid) cs(this).value(“stateLoginEnabled”, Chapter 6: Solution138 usernameValid && passwordValid) } )The hierarchical nature of Presentation Models can be seen on value access. On value() and observe(), the entry under name is first looked up at the target Com-ponent (in the above examples this is cs(this)). If it is not found, it is implicitly recursively looked up at the parent Components until no more parent Component exists. This way each Component actually has a (virtual) Presentation Model available, consisting of all entries of all Presentation Models towards the root Component. Entries of nearer Components overrule.To stop potentially endless loops in the observer execution, it is important that setting a Presentation Model entry to the same value it already has, results in no ob-server executions. This behavior can be disabled with the option force to value(). Finally, two important add-on functionalities can be controlled with param-eters touch and boot of method observe(). It is often the case that a state deter-mination callback in the Presentation Logic should not only be triggered by value changes but also has to be executed once initially to determine the initial state. If parameter touch is used on observe(“foo”, ...), it internally once performs a value(“foo”, value(“foo”), true) and this way forces all observers to execute. If parameter boot is used on observe(), it internally executes the specified call-back (and no other observer callbacks) just once initially, too. 6.6.3 Pluggable User Interface Fragment SocketsWhen running the MVC/CT design pattern (see 6.8 on page 150) on the Compo-nent Tree, it is essential to have Pluggable User Interface Fragment Sockets to at-tach the View Components to the Browser’s Document Object Model (DOM). The Component System provides the following Component API methods for defining a so-called “socket” and to plug and unplug information to and from such a socket:/* define a socket */ socket(params: { scope?: any = ““; ctx: any; plug: (obj: any) => void; unplug: (obj: any) => void; }): number; socket( ctx: any, plug: (obj: any) => void, unplug: (obj: any) => void ): number; /* destroy a defined socket */ Observable Presentation Models are key to the Reactive Programming of User Interfaces.Hierarchical Component Communication139unsocket(params: { id: number; }): void; unsocket( id: number ): void; /* plug information into a socket */ plug(params: { obj: any; spool?: string = ““; }): void; plug( obj: any ): void; /* unplug information from a socket */ unplug(params: { obj: any; }): void; unplug( obj: any ): void;A socket is a communication mechanism which allows one to plug and unplug an arbitrary object to and from the nearest ancestor Component without having to know the particular ancestor Component and without having to know how the underlying plug/unplug operation on the object is actually performed by this an-cestor Component.In the context of View Components, this means, a sub-dialog can plug its DOM fragment into the DOM fragment of its parent View Com-ponent, without having to know which Component on the ancestor Component path actually is the parent View Component and without having to know how and where this parent View Component attaches the sub-dialog DOM onto its own dialog DOM fragment.This way, Pluggable User Interface Fragment Sockets provide an essential loose coupling of View Components under strict Separation of Concerns. The following example illustrates this.Here an Ancestor Component AC creates its entire Component Tree of child Components. We assume that the two sub-dialogs are of the same type and are just instantiated twice. The Ancestor Component AC might render its UI fragment and use two distinct sockets to attach the DOM fragments of the Sub-Components SC1 and SC2:import $ from “jquery” import cs from “componentjs” import SC from [...] The Pluggable User In-terface Fragment Sockets mechanism especially al-lows the run-time assem-bling of DOM fragments. Next >