AppSuite:Extending the UI: Difference between revisions
mNo edit summary |
No edit summary |
||
Line 37: | Line 37: | ||
= Extension Point System = | = Extension Point System = | ||
<pre class="language-javascript">//load extension points module | <pre class="language-javascript"> //load extension points module | ||
require(['io.ox/core/extensions'], function (ext) { | require(['io.ox/core/extensions'], function (ext) { | ||
//insert code here | |||
});</pre> | });</pre> | ||
= Registry = | = Registry = | ||
Line 49: | Line 49: | ||
== list points == | == list points == | ||
<pre class="language-javascript">// returns array of point ids | <pre class="language-javascript"> // returns array of point ids | ||
var keys = ext.keys();</pre> | var keys = ext.keys();</pre> | ||
== get/create point == | == get/create point == | ||
<pre class="language-javascript">//also registers point if not created yet | <pre class="language-javascript"> //also registers point if not created yet | ||
var mypoint = ext.point('io.ox/calendar/detail');</pre> | var mypoint = ext.point('io.ox/calendar/detail');</pre> | ||
= Extension Point = | = Extension Point = | ||
Line 72: | Line 72: | ||
'''''example''''' | '''''example''''' | ||
<pre class="language-javascript">//get a point and get it's description | <pre class="language-javascript"> //get a point and get it's description | ||
var point = ext.point('io.ox/mail/links/toolbar'), | var point = ext.point('io.ox/mail/links/toolbar'), | ||
descr = point.description = ''; | |||
</pre> | </pre> | ||
Line 86: | Line 86: | ||
'''''example''''' | '''''example''''' | ||
<pre class="language-javascript">// chainable (returns mypoint) | <pre class="language-javascript"> // chainable (returns mypoint) | ||
point.extend({ | point.extend({ | ||
id: 'example1', // Every extension is supposed to have an id | |||
index: 100, // Extensions are ordered based on their indexes | |||
draw: function () { | |||
//draw something | |||
} | |||
});</pre> | });</pre> | ||
Line 104: | Line 104: | ||
'''''example''''' | '''''example''''' | ||
<pre class="language-javascript">// chainable (returns mypoint) | <pre class="language-javascript"> | ||
// chainable (returns mypoint) | |||
mypoint.replace({ | mypoint.replace({ | ||
id: 'example1', | id: 'example1', | ||
Line 120: | Line 121: | ||
* baton forwarded within programmatic flow and used for storing and exchanging data between extensions | * baton forwarded within programmatic flow and used for storing and exchanging data between extensions | ||
<pre class="language-javascript">mypoint.invoke(name, context, baton);</pre> | <pre class="language-javascript"> | ||
mypoint.invoke(name, context, baton);</pre> | |||
'''''example''''' | '''''example''''' | ||
<pre class="language-javascript">//call 'draw' of all registered extensions (order defined by index attribute) | <pre class="language-javascript"> | ||
//call 'draw' of all registered extensions (order defined by index attribute) | |||
//node used as context ('draw' function is called via apply(node, args)) | //node used as context ('draw' function is called via apply(node, args)) | ||
//baton's data property contains relevant information about current entity (for example a mail) | //baton's data property contains relevant information about current entity (for example a mail) | ||
Line 133: | Line 136: | ||
== access extensions == | == access extensions == | ||
<pre class="language-javascript">//returns array containing all extension ids | <pre class="language-javascript"> | ||
//returns array containing all extension ids | |||
mypoint.keys();</pre> | mypoint.keys();</pre> | ||
<pre class="language-javascript">//returns array containing all extensions | <pre class="language-javascript"> | ||
//returns array containing all extensions | |||
mypoint.all();</pre> | mypoint.all();</pre> | ||
<pre class="language-javascript">//executes callback for a specific extension | <pre class="language-javascript"> | ||
//executes callback for a specific extension | |||
//chainable (returns point) | //chainable (returns point) | ||
mypoint.get(id, callback);</pre> | mypoint.get(id, callback);</pre> | ||
<pre class="language-javascript">//disabled extension will return true also; | <pre class="language-javascript"> | ||
//disabled extension will return true also; | |||
var exists = mypoint.has(id);</pre> | var exists = mypoint.has(id);</pre> | ||
Line 146: | Line 153: | ||
'''''enabled only''''' | '''''enabled only''''' | ||
<pre class="language-javascript">//returns array containing all enabled extensions | <pre class="language-javascript"> | ||
//returns array containing all enabled extensions | |||
mypoint.list()</pre> | mypoint.list()</pre> | ||
<pre class="language-javascript">//returns number containing enabled extensions | <pre class="language-javascript"> | ||
//returns number containing enabled extensions | |||
mypoint.count() | mypoint.count() | ||
</pre> | </pre> | ||
Line 155: | Line 164: | ||
== enabling/disabling == | == enabling/disabling == | ||
<pre class="language-javascript">var enabled = mypoint.isEnabled();</pre> | <pre class="language-javascript"> | ||
<pre class="language-javascript">//chainable (returns mypoint) | var enabled = mypoint.isEnabled();</pre> | ||
<pre class="language-javascript"> | |||
//chainable (returns mypoint) | |||
mypoint.enable(id);</pre> | mypoint.enable(id);</pre> | ||
<pre class="language-javascript">//chainable (returns mypoint) | <pre class="language-javascript"> | ||
//chainable (returns mypoint) | |||
mypoint.disable(id);</pre> | mypoint.disable(id);</pre> | ||
'''example''' | '''example''' | ||
<pre class="language-javascript">//disable | <pre class="language-javascript"> //disable | ||
ext.point('io.ox/mail/detail/header').disable('receiveddate');</pre> | ext.point('io.ox/mail/detail/header').disable('receiveddate');</pre> | ||
Line 171: | Line 183: | ||
* take a look at http://underscorejs.org/ for more details | * take a look at http://underscorejs.org/ for more details | ||
<pre class="language-javascript">mypoint.chain() | <pre class="language-javascript"> | ||
mypoint.chain() | |||
mypoint.each(callback) | mypoint.each(callback) | ||
mypoint.map(callback) | mypoint.map(callback) | ||
Line 179: | Line 192: | ||
'''example''' | '''example''' | ||
<pre class="language-javascript">// Shuffle extension order | <pre class="language-javascript"> | ||
// Shuffle extension order | |||
ext.point('io.ox/calendar/detail').each(function (e) { | ext.point('io.ox/calendar/detail').each(function (e) { | ||
e.index = Math.random() * 1000 >> 0; | e.index = Math.random() * 1000 >> 0; | ||
Line 190: | Line 204: | ||
* differences documentated for each function | * differences documentated for each function | ||
<pre class="language-javascript">// attach listener | <pre class="language-javascript"> | ||
// attach listener | |||
mypoint.on(type, data, function)</pre> | mypoint.on(type, data, function)</pre> | ||
<pre class="language-javascript">// detach listener | <pre class="language-javascript"> | ||
// detach listener | |||
mypoint.off(type, function)</pre> | mypoint.off(type, function)</pre> | ||
<pre class="language-javascript">// attach listener for a single execution | <pre class="language-javascript"> | ||
// attach listener for a single execution | |||
mypoint.one(type, data, function)</pre> | mypoint.one(type, data, function)</pre> | ||
<pre class="language-javascript">// trigger event | <pre class="language-javascript"> | ||
// trigger event | |||
// difference: allows multiple types separated by spaces. | // difference: allows multiple types separated by spaces. | ||
// difference: actually calls triggerHandler since we are not in the DOM. | // difference: actually calls triggerHandler since we are not in the DOM. | ||
mypoint.trigger(types)</pre> | mypoint.trigger(types)</pre> | ||
<pre class="language-javascript">// explicit destroy to clean up. | <pre class="language-javascript"> | ||
// explicit destroy to clean up. | |||
mypoint.destroy()</pre> | mypoint.destroy()</pre> | ||
Line 219: | Line 238: | ||
''''example''' | ''''example''' | ||
<pre class="language-javascript">//defining a extension for some extension point that requires a draw function | <pre class="language-javascript"> | ||
//defining a extension for some extension point that requires a draw function | |||
{ | { | ||
id: 'example1', | id: 'example1', | ||
Line 289: | Line 309: | ||
== disable extensions == | == disable extensions == | ||
<pre class="language-javascript">//disable | <pre class="language-javascript"> //disable | ||
baton.disable(pointid, extensionid); | baton.disable(pointid, extensionid); | ||
//is disabled | //is disabled | ||
Line 298: | Line 318: | ||
'''example''' | '''example''' | ||
<pre class="language-javascript">var pointid = 'io.ox/mail/detail', | <pre class="language-javascript"> var pointid = 'io.ox/mail/detail', | ||
extensionid = 'example3', | |||
node = $('div'), | |||
baton = ext.Baton(); | |||
//disable extension | //disable extension | ||
//returns undefined | //returns undefined | ||
baton.disable(pointid,extensionid); | baton.disable(pointid,extensionid); | ||
//invoke extension with baton instance | //invoke extension with baton instance | ||
ext.point(pointid).invoke('draw', node, baton)</pre> | ext.point(pointid).invoke('draw', node, baton)</pre> | ||
Line 316: | Line 336: | ||
'''example''' | '''example''' | ||
<pre class="language-javascript">//extension using baton to store data | <pre class="language-javascript"> //extension using baton to store data | ||
{ | |||
{ | id: 'example1', | ||
index: 100, | |||
draw: function (baton) { | |||
//get the currenty process mail object | |||
var mail = baton.data; | |||
//append subject to current node referenced as this | |||
this.append( | |||
$('div').text(mail.subject); | |||
) | |||
//extend mail object to store some flag | |||
mail.drawn = mail.drawn || {}; | |||
mail.drawn.subject = true; | |||
//disable extension3 | |||
baton.disabel() | |||
} | |||
}; | |||
}; | { | ||
{ | id: 'example2', | ||
index: 200, | |||
draw: function (baton) { | |||
//get the currenty process mail object | |||
var mail = baton.data; | |||
//use value set by 'example1' | |||
if(mail && mail.drawn && mail.drawn.subject) { | |||
//do something | |||
} | |||
} | |||
}; | |||
}; | { | ||
{ | id: 'example3', | ||
index: 300, | |||
draw: function (baton) { | |||
//wil not be executed if baton from 'disable example' is used | |||
} | |||
};</pre> | |||
}; | |||
</pre> | |||
Line 365: | Line 383: | ||
* return new baton instance ***where baton.data*** is extended by obj **or** obj.date (if exists) | * return new baton instance ***where baton.data*** is extended by obj **or** obj.date (if exists) | ||
<pre class="language-javascript"> | <pre class="language-javascript"> baton = ext.Baton.ensure(obj) ``` | ||
baton = ext.Baton.ensure(obj) ``` | |||
</pre> | </pre> | ||
Line 372: | Line 389: | ||
'''example''' | '''example''' | ||
<pre class="language-javascript">//new baton.data extended by object | <pre class="language-javascript"> //new baton.data extended by object | ||
ext.Baton.ensure({ id: 2 })</pre> | ext.Baton.ensure({ id: 2 })</pre> | ||
[[File:Ui_ext_02.png|frame|border|left]] | [[File:Ui_ext_02.png|frame|border|left]] | ||
Line 387: | Line 404: | ||
'''example''' | '''example''' | ||
<pre class="language-javascript">//new baton object extended by obj | <pre class="language-javascript"> //new baton object extended by obj | ||
ext.Baton.wrap({ id: 2 })</pre> | ext.Baton.wrap({ id: 2 })</pre> | ||
[[File:Ui_ext_03.png|Ui_ext_03.png]] | [[File:Ui_ext_03.png|Ui_ext_03.png]] |
Revision as of 13:22, 11 April 2013
Introduction
Abstractly speaking extension points are an architecture for letting plugins contribute functionality to other parts of the program. They form the core of the OX App Suite plugin architecture. A less detailed hands-on introduction can be found here. Some basics about the extention point concept and advantages compared to inheritance.
inheritance vs. extension points
OX App Suite uses the extension point concept to create extension points that allow a simple and flexible way to extending functionality. When system reaches a part that can be extended it asks a central registry if extensions are registered. In that case these extensions will be executes independent of the providing component (some plugin or OX App Suite itself).
The illustrated example compares inheritance and extension points. The main benefit of using extension points ist that the programm is still the active component and is in controll. This leads to the following advantages:
- Reduced coupling
- Increased cohesion
- Modularity- Re-usability
- Dynamic
Some characteristics
- good fences: extension points unregister corrupt extenders
- lazy loading: extenders are loaded if they are used
- fair play: all extenders have equal rights
- diversity: extension points support different extension
Components
The extension point system lives in the 'io.ox/core/extensions' module and consists of these elements:
- extension point system: accessing the outer parts
- registry: manages extension points, extensions and their state
- extension point: part of the systems that can be extended, referenced by a unique id
- extension: adding/replacing functionality during runtime, referenced by a unique id
- baton: object used to store context, passed back through callbacks
Extension Point System
//load extension points module require(['io.ox/core/extensions'], function (ext) { //insert code here });
Registry
- manages extension points, extensions and their state
list points
// returns array of point ids var keys = ext.keys();
get/create point
//also registers point if not created yet var mypoint = ext.point('io.ox/calendar/detail');
Extension Point
- part of the systems that can be extended, referenced by a unique id
- defienes as some kind of contract about what it expects its extensions to provide
attributes
- id
- description (optional)
example
//get a point and get it's description var point = ext.point('io.ox/mail/links/toolbar'), descr = point.description = '';
add extension
- important: existing extensions with same id will not be overwritten - use replace insted
- example: add extension with id 'date'
example
// chainable (returns mypoint) point.extend({ id: 'example1', // Every extension is supposed to have an id index: 100, // Extensions are ordered based on their indexes draw: function () { //draw something } });
replace extension
- important: only extension properties will be replaced (jquery extend)
- hint: replace can also be executed before extension is intially created with extend function
example
// chainable (returns mypoint) mypoint.replace({ id: 'example1', index: 100, draw: function (baton) { //draw something completely different } });
use extensions
- invoking point extensions by defining functionname, context and baton
- node used as context (function is called via apply(node, args))
- baton forwarded within programmatic flow and used for storing and exchanging data between extensions
mypoint.invoke(name, context, baton);
example
//call 'draw' of all registered extensions (order defined by index attribute) //node used as context ('draw' function is called via apply(node, args)) //baton's data property contains relevant information about current entity (for example a mail) mypoint.invoke('draw', node, baton);
access extensions
//returns array containing all extension ids mypoint.keys();
//returns array containing all extensions mypoint.all();
//executes callback for a specific extension //chainable (returns point) mypoint.get(id, callback);
//disabled extension will return true also; var exists = mypoint.has(id);
enabled only
//returns array containing all enabled extensions mypoint.list()
//returns number containing enabled extensions mypoint.count()
enabling/disabling
var enabled = mypoint.isEnabled();
//chainable (returns mypoint) mypoint.enable(id);
//chainable (returns mypoint) mypoint.disable(id);
example
//disable ext.point('io.ox/mail/detail/header').disable('receiveddate');
underscore equivalents
- only considers enabled extensions
- functions returns underscore-chain object of enabled extensions
- take a look at http://underscorejs.org/ for more details
mypoint.chain() mypoint.each(callback) mypoint.map(callback) mypoint.select(callback) //filter alias mypoint.inject(callback, memo) //reduce alias mypoint.pluck(propertyName)
example
// Shuffle extension order ext.point('io.ox/calendar/detail').each(function (e) { e.index = Math.random() * 1000 >> 0; }).sort();
Event Hub
- Event Hub based on jQuery's on, off, one, and trigger
- differences documentated for each function
// attach listener mypoint.on(type, data, function)
// detach listener mypoint.off(type, function)
// attach listener for a single execution mypoint.one(type, data, function)
// trigger event // difference: allows multiple types separated by spaces. // difference: actually calls triggerHandler since we are not in the DOM. mypoint.trigger(types)
// explicit destroy to clean up. mypoint.destroy()
Extension
- adding/replacing functionality during runtime
- referenced by a unique id
Attributes
- id
- index (optional): numeric value used for specify order of execution (valid values also 'first', 'last')
- functions: as required by the extension point contract
'example
//defining a extension for some extension point that requires a draw function { id: 'example1', index: 100, draw: function () { //draw something } };
extensions patterns
io.ox/backbone/forms.js
- CheckBoxField
- ControlGroup
- DateControlGroup
- DatePicker
- ErrorAlert
- Header
- InputField
- Section
- SectionLegend
- SelectBoxField
- SelectControlGroup
io.ox/backbone/views.js
- AttributeView
io.ox/calendar/edit/recurrence-view
- RecurrenceView
io.ox/core/extPatterns/links
- Button
- DropdownLinks
- InlineLinks
- link
- ToolbarLinks
io.ox/core/tk/attachments
- AttachmentList
- EditableAttachmentList
io.ox/contacts/widgets/pictureUpload.js
io.ox/preview/main.js
- Engine
Baton
Part of extension points system is a structure called baton which serves as an context object. The baton ipassed back through callbacks within programmatic flow so data exchange between extension points is simple.
attributes
- data: usually contains current entity object, also used for data exchange
- options:
- flow:
- disabled: stores disabled extensions (managed via baton.disable(pointId, extensionId))
- disabled: stores disabled extensions (managed via baton.disable(pointId, extensionId))
- $: used to reference a jquery node object
disable extensions
//disable baton.disable(pointid, extensionid); //is disabled var isDisabled = baton.isDisabled(pointid, extensionid);
example
var pointid = 'io.ox/mail/detail', extensionid = 'example3', node = $('div'), baton = ext.Baton(); //disable extension //returns undefined baton.disable(pointid,extensionid); //invoke extension with baton instance ext.point(pointid).invoke('draw', node, baton)
data exchange
example
//extension using baton to store data { id: 'example1', index: 100, draw: function (baton) { //get the currenty process mail object var mail = baton.data; //append subject to current node referenced as this this.append( $('div').text(mail.subject); ) //extend mail object to store some flag mail.drawn = mail.drawn || {}; mail.drawn.subject = true; //disable extension3 baton.disabel() } }; { id: 'example2', index: 200, draw: function (baton) { //get the currenty process mail object var mail = baton.data; //use value set by 'example1' if(mail && mail.drawn && mail.drawn.subject) { //do something } } }; { id: 'example3', index: 300, draw: function (baton) { //wil not be executed if baton from 'disable example' is used } };
utils
ensure
- ensure that submitted object is instanceof baton
- return obj if it's an instanceof baton
- return new baton instance ***where baton.data*** is extended by obj **or** obj.date (if exists)
baton = ext.Baton.ensure(obj) ```
example
//new baton.data extended by object ext.Baton.ensure({ id: 2 })
wrapp
- wrap a baton 'around' submitted object
- return if it's an instanceof baton
- return new baton instance where baton is extended by obj or obj.date (if exists)
example
//new baton object extended by obj ext.Baton.wrap({ id: 2 })