Components
Overview
A bundle can declare components to provide instances of JavaScript objects that are created and managed by the App Runtime as needed. Other component instances — declared by the same or a different bundle — can discover and use ("consume") them.
An example
The "map-init" bundle declares a component that provides an instance of a map frame. The "scalebar" bundle declares a component that requires a map frame instance, so it can read the scale of the frame and display the scale in the bottom right corner of a map.apps app. The App Runtime creates the components and provides the the requested map frame instance to the scalebar instance as soon as it becomes available. |
In the example above the map frame component provides a service (a map frame instance) while the scalebar component requires a service (a map frame instance). The important thing is that the components have no direct dependencies: the scalebar is satisfied as soon as it gets a map frame, no matter which bundle provides the service.
Component declaration
To declare a component in a bundle, add a component description to the "components"
array of the bundle manifest like this:
manifest.json
file{
"name" : "zoom",
"version": "1.0.0",
"components" : [
{
"name": "ZoomSlider", (1)
"impl": "ZoomSliderImpl", (2)
"provides": "zoom.Control", (3)
"properties": { (4)
"+fooProperty": "foo",
"barProperty": "bar"
}
}
]
}
1 | Name of this component. This is the only required property. The name must be unique within this bundle. |
2 | Name of the implementation class of the component. |
3 | Identifier for the service provided to other components. |
4 | List of public and private component properties. |
Visit the component configuration reference for a list of all supported properties of a component description.
Implementation class
All components must have a corresponding JavaScript class that implements the component’s behavior. By default the name of the component is used to locate its implementation class.
As an example, if the component "name"
is "ZoomSlider"
, the App Runtime tries to resolve the class name ZoomSlider
by looking into the object returned by the module.js
file.
If the implementing class has a different class name, add the property "impl"
to specify the actual class name like this:
{
"name" : "ZoomSlider",
"impl" : "ZoomSliderImpl"
}
Find more details about the class loading mechanism where the bundle layer file is explained.
Providing a service
A component instance is registered as a service in the App Runtime if its component description declares the property provides
.
The property contains the names of the provided service interfaces, the value can be a string or an array.
{
"name" : "ZoomSlider",
"provides" : "zoom.Control"
},
{
"name" : "ZoomInButton",
"provides" : ["zoom.Control","zoom.Button"]
}
The properties specified in the components description are also important for service registration, see Component Properties.
It is important that each property which does not start with a _
prefix is treated as public service property and registered as service properties.
Instance factories
Sometimes it is required to register a different instance as a service than the one that you want to configure as a component implementation.
This is often the case if you have a ready-to-use class which is implemented independent of the component model and you do not want to introduce component system-relevant code inside this class, for example configuration code.
A very typical use case is the use of Dijit widgets as components.
To support this case the component description has to be marked as "instanceFactory"
, which means that the component instance acts as factory for the service.
{
"name" : "ExternalZoomSliderFactory",
"provides" : ["zoom.Control"],
"instanceFactory": true
}
A component instance which is marked as an instance factory must implement the method createInstance
, which is responsible for creating the "real" service instance.
This service instance is registered at the App Runtime.
Optionally the component instance can provide a destroyInstance
method, which is responsible for cleaning up the service instance during shutdown.
import ZoomSlider from "./zoomToolsLib";
export default class {
createInstance() {
return new ZoomSlider();
},
destroyInstance(instance) {
instance.destroyRecursive();
}
}
Properties
Properties declaration
A component description can declare properties.
These properties are used for two purposes.
The first is to provide a configuration mechanism and the second to specify service properties (if the "provides"
property is defined).
{
"name" : "ZoomSlider",
"provides" : ["myservice.Interface"],
// properties of the component
"properties": {
// underscore properties are private properties
"_offset" : 125,
// all other properties are registered as service properties (if provides exist)
"url" : "http://locahost:8080/..."
}
}
The recommended way to access component properties within an implementation class is to use the provided property _properties
.
This is a plain JavaScript object which can be used to access the configuration properties.
export default class {
aServiceMethod(){
// accessing the injected '_properties' object
let properties = this._properties;
// the url property
let url = properties.url;
// the offset property
let offset = properties._offset;
...
}
}
An alternative way is using the Component Context provided by the activate
method (see component lifecycle methods).
The Component Context provides a getProperties
method, which returns a ct.Hash
instance containing the properties, which means that the access style is slightly different.
export default class {
activate(componentContext){
// get the properties from ComponentContext
var properties = componentContext.getProperties();
// the url property
var url = properties.get("url");
// the offset property
var offset = properties.get("_offset");
...
}
}
Another way to retrieve the properties is to specify the flag "propertiesConstructor"
on the component description.
This defines that the configuration properties are transported as constructor options into the implementation class.
Here the properties are exactly like the '_properties' property a plain JavaScript object.
Besides the transportation as constructor properties, the configuration properties are still injected as _properties
property.
{
"name" : "ZoomSlider",
"provides" : ["myservice.Interface"],
// the component implementation shall get the properties as constructor options
"propertiesConstructor": true,
"properties": {
"_offset" : 125,
"url" : "http://locahost:8080/..."
}
}
export default class {
constructor(properties){
// the url property
let url = properties.url;
// the offset property
let offset = properties._offset;
...
}
});
});
Configuration properties are read only. You are not allowed to change them.
Properties visibility
Properties can be public or private. Public properties are part of the component interface and registered as service properties if a component instance is registered as a service.
By default, all properties prefixed with an underscore _
are treated as private properties, while all other properties are treated as public.
You can override the default by prefixing property names with a +
to make them public, and -
to make them private.
If you use the +
prefix for at least one property, all other properties are private.
Using -
as a prefix to make a property private has no effect on the visibility of other properties.
{
"name": "ZoomSlider",
"provides" : "anInterface",
"properties": {
"+enableFoo": true,
"-internalProp" : "prop"
}
}
Types
Immediate component
A component is an immediate component if it defined the property "immediate": true
and is not a factory component.
A component is treated as immediate if it does not declare a "provides"
property.
{
"name" : "MyComponent",
"provides" : ["myservice.Interface"],
"immediate": true
}
If the component has no references, you can consider the lifetime of the component as equal to BundleActivator, which is created on the start of a bundle and destroyed during the stop of the bundle.
Only immediate components are allowed to act asynchronously in their activation method.
This means that a component instance is allowed to return a dojo.Deferred
during its activate()
method and the App Runtime waits until activate()
finishes before it registers the component instance as a service at the App Runtime.
Delayed component
A component description which declares a "provides"
property and which is not an immediate component and not a factory component is treated as delayed.
This means that delayed components are the most common kind of components, because this is the default case if no other special property is declared.
{
"name" : "MyComponent",
"provides" : ["myservice.Interface"]
}
Delayed components are so named because of the fact that service registration is made before the component instance is created. This means that the creation of the component instance is delayed until the first use of the registered service. A delayed component instance is destroyed if it becomes unused. As a result, delayed components can have a very dynamic life because construction and destruction might happen very often and are highly dependent on usage of the service provided by the component.
If a delayed component declares the property "serviceFactory": true
, the App Runtime creates a new service instance for each requesting bundle.
Factory component
A component description which specifies the property "componentFactory": true
is treated as factory component.
{
"name" : "MyComponent",
"provides" : ["myservice.Interface"],
"componentFactory" : true
}
A factory component is a special design pattern, because it acts more as a template for the dynamic creation of component configurations based on component descriptions.
The App Runtime parses the component description.
If it is satisfied, it registers a service with the interface ct.framework.api.ComponentFactory
for this configuration.
This service provides a method newInstance
, which can be used to create new component configurations during runtime.
This newInstance
method returns a ComponentInstance object, which provides the method getInstance
for accessing the real instance, and the method dispose
for shutting down the component configuration.
The caller of newInstance
is responsible for the disposal of the created component configuration.
// Activator using a component factory
export default class{
start(bundleContext) {
// search special component factory with name "MyComponentFactory"
let references = bundleContext.getServiceReferences("ct.framework.api.ComponentFactory","(Component-Name=MyComponent)");
try {
// get the component factory service
let factory = bundleContext.getService(references[0]);
// create a new component configuration
// here the default component properties defined in the declaration of the component factory can be overwritten.
let componentInstance = factory.newInstance({ "aproperty": "newValue" });
// get component/service implementation
// this feature should rarely be used, better define a reference
let impl = componentInstance.getInstance();
impl.doSomething();
// the created component configuration has to be cleaned up later
componentInstance.dispose();
} finally {
bundleContext.ungetService(references[0]);
}
}
}
Factory Components are a very powerful feature because the newInstance
method creates new component configurations.
This means that such a new dynamically created component configuration is managed by the App Runtime as if it were a new and independent component description in the manifest.json
file.
Therefore if the property provides
is specified, the new component instance is registered as a service in the App Runtime.
Because of the ComponentInstance.getInstance
method, a component configuration created by a factory component is treated as an immediate component, therefore, regarding its using state, the component instances are directly created.
An important point is that a factory component is satisfied if all references of the component description are also satisfied.
In some circumstances this definition leads to unexpected behaviors especially if references are using placeholders in filter expressions.
// Other component using a component factory
// manifest.json
{
"name" : "OtherComponent",
"references" : [{
"name" : "_factory",
"providing" : "ct.framework.api.ComponentFactory",
"filter": "(Component-Name=MyComponent)"
}]
}
// implementation of OtherComponent
export default class {
activate() {
// create a new component configuration
// here the default component properties defined in the declaration of the component factory can be overwritten.
let componentInstance = this._factory.newInstance({ "aproperty": "newValue" });
// get component/service implementation
// this feature should rarely be used, better define a reference
let impl = componentInstance.getInstance();
impl.doSomething();
// the created component configuration has to be cleaned up later
componentInstance.dispose();
}
}
Lifecycle
Enabled
A component must be enabled before it can be used.
You can define the initial value of "enabled"
in the component description as property "enabled"
.
The default value is true
, so by default components are enabled.
{
"name" : "MyComponent",
"provides" : ["myservice.Interface"],
// disables the component
"enabled": false
}
You can programmatically control this state calling enableComponent()
and disableComponent()
on the ComponentContext.
A component can only enable/disable the components within its own bundle.
The next sample shows how a component can be used to enable/disable another component "ToolXY"
.
export default class {
activate(componentContext){
this._componentContext = componentContext;
}
deactivate(){
this._componentContext = null;
}
enableToolXY(){
this._componentContext.enableComponent("ToolXY");
}
disableToolXY(){
this._componentContext.disableComponent("ToolXY");
}
}
Satisfied
The App Runtime activates a component configuration only if the component configuration is satisfied. The following conditions must be true for a component configuration to become satisfied:
-
The component is enabled.
-
All the references are satisfied. A reference is satisfied when the reference specifies optional cardinality or there is at least one target service for the reference.
As soon as any of the listed conditions are no longer true, the component configuration becomes unsatisfied and is deactivated.
Lifecycle states
Immediate components
The lifecycle state diagram of an immediate component looks like this:
If an immediate component becomes satisfied, it is activated, and if it provides a declaration, it is registered as a service in the App Runtime. It remains in ACTIVE state until it becomes unsatisfied, which triggers the unregistration and deactivation of the component.
Delayed components
The lifecycle state diagram of a delayed component looks like this:
If a delayed component becomes satisfied, it is first registered as service in the App Runtime. When the first client gets the registered service, the component is activated. The component remains in active state until it becomes unsatisfied or if the last client "ungets" the service instance, which triggers the deactivation of the component. If unget triggered the deactivation, the component remains registered. Otherwise it becomes unsatisfied.
Factory components
The lifecycle state diagram of a factory component looks like this:
If a factory component becomes satisfied, the factory service is created and registered at the App Runtime.
The factory service is registered until it becomes unsatisfied.
If a client calls newInstance
on the factory, a new component configuration is created and activated.
The lifecycle of a component created by a factory is equal to the lifecycle of an immediate component.
Lifecycle methods
A component instance can provide following lifecycle methods.
Name | Description |
---|---|
|
Signals the start of the injection phase of the component.
It is the first method called by the App Runtime after the construction of the instance.
Services are injected after this method is called, but the special properties |
|
Signals the end of the injection phase and that the component instance is ready for use. The App Runtime guarantees that service consumers can access the component instance only after activate is called on the instance. [NOTE]
Only immediate components are allowed to return a |
|
This method is called after |
|
Signals that the component properties(configuration) is changed.
This method is only called between |
|
This method is called directly before |
|
Signals the start of the shutdown process of the component instance. All injected services are still available and the ejection phase starts after this method. |
|
Signals the end of the life of the component instance.
All services are ejected before this method, but |
These methods are called in the life of a component in the order as listed.
Activation
-
constructor: properties are handed over (if
"propertiesConstructor"
istrue
) -
init: is called before references are injected,
_properties
and_i18n
are available -
set<ReferenceName>, add<ReferenceName>: App Runtime injects services
-
activate: is called after references are injected
-
createInstance: called if
"instanceFactory"
istrue
and must return the service instance
Running
-
modified: called if configuration is modified;
_properties
contains the new configuration properties. -
set<ReferenceName>, add<ReferenceName>: App Runtime injects new registered services, if
"policy": "dynamic"
. -
unset<ReferenceName>, remove<ReferenceName>: App Runtime ejects unregistered services, if
"policy": "dynamic"
.
Deactivation
-
destroyInstance: called if
"instanceFactory"
istrue
and should clean up the service instance -
deactivate: is called before references are ejected from the component
-
unset<ReferenceName>, remove<ReferenceName>: App Runtime ejects services
-
destroy or destroyRecursive: is called after references are removed,
_properties
and_i18n
are still available
Service references
A very powerful feature of the component system is the declarative expression of references to other services registered in the App Runtime. The declarative component model significantly simplifies the handling of the dynamics of service dependencies.
A reference expression is a declarative wrapper of the method BundleContext.getServiceReferences
(described in Programmatic service access), which requires an interface as selector together with an optional filter expression.
Services which are selected by these conditions are called target services.
The properties providing
, filter
and cardinality
of a reference specify rules which are checked by the App Runtime.
If all rules of a reference are fulfilled, the reference is said to be satisfied.
A component configuration becomes satisfied when all references are satisfied.
If a reference becomes unsatisfied, the component configuration also becomes unsatisfied and is deactivated.
If a component configuration is activated by the App Runtime, some of the target services are bound to the configuration.
These services are called bound services.
References are expressed as part of the component description.
manifest.json
file: references of components{
"name" : "mybundle",
...
"components" : [
{
"name" : "MyComponent",
...
"references" : [
{
"name" : "logService",
"providing" : "ct.framework.api.LogService"
},
...
]
},
...
]
}
A reference needs at least a name
and a providing
property.
The name
property must have a unique value inside the references of the component description and identifies the reference.
The providing
property defines the interface which the target service must provide.
Filtering of references
The property filter
at a reference is used to further reduce the target services selected by the providing property.
The value of filter
is a filter expression described in the section Searching for bundles.
{
"name" : "myFactory",
"providing" : "ct.framework.api.ComponentFactory",
"filter" : "(Component-Factory=MyComponentFactory)"
}
It is allowed to use property placeholders in filter expressions, like in the following code sample.
properties: {
"storeId" : "sample-store"
},
references:[{
"name": "store",
"providing": "ct.api.Store",
// the string '{storeId}' is replaced by the value of the property 'storeId' of the component configuration
"filter": "(&(useIn=selection)(id={storeId}))"
}]
Placeholders in filter expressions allow the change of the target set of a reference based on configuration properties.
Reference cardinality
By specifying the property "cardinality"
on a reference you can declare target set conditions.
{
"name" : "myFactory",
"providing" : "myservice.Interface",
// optional reference, only one target service needed
"cardinality" : "0..1"
}
Following cardinality values are supported:
Expression | Description |
---|---|
|
Selects exactly one target service from the set of target services and a target service must be available to become satisfied. This is the default if no property is defined. |
|
The reference is satisfied if none or more target services are available. Selects one service from the set of target services, if available. |
|
Selects all target services from the set of target services and a target service must be available to become satisfied. |
|
The reference is satisfied if none or more target services are available. Selects all services from the set of target services, if available. |
If only one target service is selected then the first service returned by BundleContext.getServiceReferences
is bound, which is the one with the highest ranking or the one first registered.
Reference policy
A special lifecycle property is the "policy"
property on a reference.
It can have the value "dynamic"
or "static"
and declares the capability of a component instance to react on runtime changes of the matching target services of a reference.
The policy is only important if changes in the target services still keep the reference satisfied.
For example, this is the case if a reference has a 1..n
cardinality and a new target service is registered at the system.
Value | Description |
---|---|
|
Defines that the component instance is able to react dynamically on changes in the bound service set itself. Therefore the App Runtime is not restarting the component configuration. This is the default value. |
|
Defines that the component instance is not able to react on dynamic changes in the bound service set. This means that the App Runtime restarts the component configuration on such changes. |
{
"name" : "myFactory",
"providing" : "myservice.Interface",
"cardinality" : "0..n",
// defines that the component cannot handle dynamic changes of that reference
"policy" : "static"
}
Accessing references
The bound service set of declared references are provided by the App Runtime to a component instance in different ways. The App Runtime distinguishes between following main strategies:
-
Injection Strategy: References are transparently injected into the component instance and access is made via member variables in the component instance.
-
Event Strategy: References are injected into the component instance by method calls. The App Runtime calls specific methods provided by the component instance to inform about new services and service removal.
-
Lookup Strategy: References are manually looked up by the component instance in the component context.
All strategies can be mixed inside a component instance, very common is a mix of Injection Strategy and Event Strategy. The Lookup Strategy is rarely used.
All these strategies are discussed in the following sections on this sample component description:
{
"name" : "mybundle",
...
"components" : [
{
"name" : "MyComponent",
"references" : [
{
"name" : "logService",
"providing" : "ct.framework.api.LogService"
},
{
"name" : "stores",
"providing" : "ct.api.Store",
"cardinality" : "0..n"
}
]
}
]
}
Injection strategy
The injection strategy means that references are provided as member properties inside the component instance without any further effort on the side of the component instance.
The component instance can directly access the injected properties.
The injected properties are named exactly like the name property of the reference.
To access the service properties of an injected reference, the special property <name>_info
is injected.
export default class {
doSomething(){
// simply access the logService service instance,
// because of the default 1..1 cardinality it is guaranteed to be available
this.logService.info("DoSomething started!");
// read service properties
let logserviceProperties = this.logService_info;
// this.stores contains all stores in the system (0..n cardinality)
// if no stores are in the system, the array is still present but empty
let stores = this.stores;
// read service properties of stores, is an array, too
let storeProperties = this.stores_info;
stores.forEach((store,index)=>{
// get service properties of current store
let storeProps = storeProperties[index];
let ranking = storeProps["Service-Ranking"];
...
});
}
}
Injection/ejection happens before activate and after deactivate and of course in between if the policy is dynamic and the target service set changes.
For details, see component lifecycle methods.
|
Event strategy
The event strategy is used by the App Runtime to inject services into the component instance if the instance provides event methods. The names of the event methods are calculated based on the multiplicity of the cardinality and the name of the reference.
Cardinality | Name Calculation Pattern | Sample |
---|---|---|
|
|
|
|
|
|
The name of the reference is capitalized to create the method name, so logService is converted into LogService .
|
export default class {
constructor(){
this.logger = null;
this.stores = [];
}
// called by App Runtime to bind the log service
setLogService(logservice, serviceproperties){
this.logger = logservice;
}
// called by App Runtime to inform about unregistration of the log service
unsetLogService(logservice, serviceproperties){
this.logger = null;
}
// called by App Runtime to inform about a new store
addStores(store, serviceproperties){
this.stores.push(store);
}
// called by App Runtime to inform about unregistration of a store
removeStores(store, serviceproperties){
var index = this.stores.indexOf(store);
this.stores.splice(index,1);
}
doSomething(){
this.logger.info("DoSomething started!");
this.stores.forEach((store)=>{
...
});
}
}
These default method names can be overwritten by defining the "bind"
and "unbind"
properties at the reference description.
{
"name" : "mybundle",
...,
"components" : [
{
"name" : "MyComponent",
"references" : [
{
"name" : "logService",
"providing" : "ct.framework.api.LogService",
// declare custom event methods (setLogging is used for bind and unbind)
"bind" : "setLogging",
"unbind" : "setLogging"
}
]
}
]
}
This results in an implementation like this:
export default class {
constructor(){
this.logger = null;
}
// called by App Runtime to bind and unbind the log service
setLogging(logservice, serviceproperties){
if (this.logger){
// unset
this.logger = null;
}else{
this.logger = logservice;
}
}
doSomething(){
this.logger.info("DoSomething started!");
}
}
The event methods are called (like injection) before activate and after deactivate and of course also between if the policy is dynamic and the target service set changes.
For details, see component lifecycle methods.
|
Lookup strategy
A third variant for retrieving the services is to use the ComponentContext.
The ComponentContext is provided by the activate
method (see component lifecycle methods).
Reference-relevant methods of the ComponentContext are:
Method | Description |
---|---|
|
Returns an array of service references. The matching target services. |
|
Returns an array of service instances. The bound service set. |
|
Returns service instance selected by the service reference. |
The usage style is similar to working with the BundleContext.
Services, which are located by using locateService
or locateServices
, should not be stored in member variables.
The 'locate*' methods should only be called between activate
and deactivate
.
For details, see component lifecycle methods.
export default class {
activate(componentContext){
// store component context in member variable
this.componentContext = componentContext;
}
deactivate(componentContext){
// clear member variable
this.componentContext = null;
}
doSomething(){
let componentContext = this.componentContext;
// cardinality is 1..1 so the result array has exactly 1 item, the log service
let logger = componentContext.locateServices("LogService")[0];
logger.info("DoSomething started!");
// get all stores
let stores = componentContext.locateServices("Stores");
stores.forEach((store)=>{
...
});
}
}
If the lookup style is used in a component instance, the special property "noInjection"
should be set on the reference description to ensure that the App Runtime does not inject the references into the component instance.
{
"name" : "mybundle",
...,
"components" : [
{
"name" : "MyComponent",
"references" : [
{
"name" : "logService",
"providing" : "ct.framework.api.LogService",
// disable injection behavior
"noInjection" : true
},
{
"name" : "stores",
"providing" : "ct.api.Store",
"cardinality" : "0..n",
// disable injection behavior
"noInjection" : true
}
]
}
]
}
Declarative event binding to references
A further feature of the component system is the declarative event binding. This is of interest if a component instance likes to listen on events on a reference and to execute a special method if that event is fired.
It can be distinguished between three kinds of event binding styles: "connect", "on", and "watch".
Style: on
This style uses the dojo/on
module to connect to events provided by services.
The "on"
property on a reference declares the binding.
{
"name" : "aTool",
"providing" : "ct.tools.Tool",
"filter" : "(id=atoolname)",
// on is an object with event names to event handle methods
"on" : {
// Click = event name fired by tool
// _handleOnClick = the method which should be called if the event fires
"Click" : "_handleOnClick"
}
}
The code of the component instance might look like this:
export default class {
_handleOnClick(event){
// do something...
console.debug("clicked..", event);
}
}
Style: connect
This style should be replaced by using the on style, when possible. |
This style uses the dojo/_base/connect
module to connect to events provided by services.
The "connect"
property on a reference declares the binding.
{
"name" : "aTool",
"providing" : "ct.tools.Tool",
"filter" : "(id=atoolname)",
// connect is an object with event names to event handle methods
"connect" : {
// onClick = event fired by tool
// _handleOnClick = the method which should be called if the event fires
"onClick" : "_handleOnClick"
}
}
The code of the component instance might look like this:
export default class {
_handleOnClick(event){
// do something...
console.debug("clicked..", event);
}
}
Style: watch
This style declares a property change listener.
It can be used at services which provides watch
methods, like defined by dojo/Stateful
.
All ct/Stateful
and dijit/_Widget
instances provide such a method.
The "watch"
property on a reference declares the binding.
{
"name" : "aStateful",
"providing" : "a.datamodel.Instance",
// watch is an object with property names to property change listener methods
"watch" : {
// selectedItem = name of the property about whose value changes should be informed
// _handleSelectedItemChange = the property change listener method
"selectedItem" : "_handleSelectedItemChange"
}
}
The code of the component instance might look like this:
export default class {
_handleSelectedItemChange(propertyname, oldvalue, newvalue){
// do something...
}
}