<!DOCTYPE html>
<body>
<script type='text/mask' data-run='true'>
// mask template
customPanel {
input #name placeholder='Enter name' > :dualbind value='name';
button x-signal='click: sendData' > 'Submit'
}
</script>
<script>
// Sample component controller. Refer to `API` for full documentation
mask.registerHandler('customPanel', mask.Compo({
slots: {
sendData: function(event){
// `this` references the current `customPanel` instance
// handle click
}
},
events: {
// e.g. bind event
'change: input#name' : function(event){
this.$ // (domLib wrapper over the component elements)
}
},
onRenderStart: function(){
this.model = { name: 'Baz' };
}
}));
mask.run();
</script>
</body>
click
andmouse*
events are also mapped to correspondingtouch*
events, when also touch input is supported.
mask.Compo(ComponentProto: Object):Function
Returns the components constructor. You would want to add it to masks repo:
mask.registerHandler('someTagName', mask.Compo(ComponentProto));
mask.Compo(...base:String|Object|Function, ComponentProto)
String
: Name of the component. The component must be registered withmask.registerHandler
Object
: Any object. Note: deep property extending is used.Function
: Note: constructor is also inherited and will be automatically invoked. Alsoprototype
data is inherited.
onRenderStart
,onRenderEnd
, slots and pipes are called automatically one after another starting from the first inherited component. All other functions will havesuper
function.
var A = mask.Compo({
slots: {
doSmth: function(){
console.log('slot-a');
}
},
foo: function(){
console.log('fn-foo-a');
}
})
var B = mask.Compo(A, {
slots: {
doSmth: function(){
console.log('b');
}
},
foo: function(){
console.log('fn-foo-b');
this.super();
}
})
All properties are optional, any amount of custom properties and functions are allowed.
-
constructor : Function
# -
tagName : String
#(optional) Component renders its template only, but when
tagName
is defined, then it will also creates appropriete wrapper element, and renders the template (if any) into the element.mask.registerHandler('Foo', mask.Compo({ tagName: 'section', template: 'span > "Hello"', onRenderEnd: function(){ this.$.get(0).outerHTML === '<section><span>Hello</span></section>' } })
-
template : String
#There are many ways to define the template for a component:
-
in-line the template of the component directly into the parents template
h4 > 'Hello' MyComponent { // here goes components template span > 'My Component' ul { li > 'A' li > 'B' } }
-
via the
template
property. This approach is much better, as it leads to the separation of concerns. Each component loads and defines its own templates. Direct inline template was shown in thetagName
sample, but to write some the templates in javascript files is not always a good idea. Better to preload the template withIncludeJS
for example. Note: The Application can be built for production. All the templates are then embedded into singlehtml
file. Style and Javascript files are also combined into single files.// myComponent.mask span > 'My Component' /*..*/
// Example: Load the template and the styles with `IncludeJS` include .css('./myComponent.less') .load('./myComponent.mask') .done(function(resp){ mask.registerHandler('MyComponent', mask.Compo({ template: resp.load.myComponent }); })
So now, the component is a standalone unit, which can be easily tested, separately developed, embedded(defined in the templates) anywhere else in the project, or moved to the next project. Just load the controller:
include.js('/scripts/myComponent/myComponent.js')
and the component is ready to use. -
more deeper way is setting the parsed template directly to
nodes
property:... onRenderStart: function(){ this.nodes = mask.parse('h4 > "Hello"'); }
-
via the
:template
component// somewhere before :template #myComponentTmpl { h4 > "Hello" } // ... later MyComponent { :import #myComponentTmpl; }
-
via external the
script type=text/mask
node.<script type='text/mask' id='#myComponentTmpl'> h4 > "Hello" </script> <script> mask.registerHandler('MyComponent', mask.Compo({ template: '#myComponent' }); </script>
You see, there are too many ways to define the template. It is up to you to decide which one is the most appropriate in some particular situation. We prefer to store the templates for each component in external files, as from example with
IncludeJS
. -
-
slots : Object
#Defines list of
slots
, which are called, when the signal is emitted and riches the controllersslotName:Function
. slotName ~ signalName are equivalentSignal can be sent in several ways:
-
from the template itself, when
x-signal
attribute is defined for the element:div x-signal='eventName: signalName; otherEventName: otherSignalName;'; // attribute aliases: x-click, x-tap, x-taphold, x-keypress, x-keydown, x-keyup, x-mousedown, x-mouseup
-
from any parent controller:
this.emitIn('signalName', arg1, arg2);
-
from any child controller:
this.emitOut('signalName', arg1, arg2);
Slot Handler. Can terminate the signal, or override the arguments.
slots: { /* * - sender: * 1) Controller which sent the signal * 2) When called from the template `sender` is the `event` Object \*/ fooSlot: function(sender[, ...args]){ // terminate signal return false; // override arguments return [otherArg, otherArg2]; } }
Predefined signals
-
domInsert
- is sent to all components, when they are inserted into the live DOMslots: { domInsert: function(){ this.$.innerWidth() // is already calculable } }
-
-
pipes : Object
#Generic
signal-slots
signals traverse the controllers tree upwards and downwards.Pipped
signals are used to join(couple) two or more controllers viapipes
. Now anyone can emit a signal in a pipe, and that signal will traverse the pipe always starting with the last child in a pipe and goes up to the first child. Pipe is a one dimensional array of the components bound to the pipe. Signal bindings are also declarative, and are defined inpipes
Object of a Compo definition.mask.registerHandler(':any', Compo({ logoutUser: function(){ Compo.pipe('user').emit('logout'); } })); mask.registerHandler('FooterUserInfo', Compo({ pipes: { // pipe name user: { logout: function(){ this.$.hide(); } // ... // other pipe signals } } }));
Piped signals could be also triggered on dom events, such as normal signals.
button x-pipe-signal='click: user.logout' > 'Logout'
-
events : Object
#Defines list of delegated events captured by the component
events: { 'eventName: delegated_Selector': function(event){ // this === component instance }, // e.g 'click: button.hideComponent': function(){ this.$.fadeOut(); } }
-
compos : Object
#Defines list of Component, jQuery or DOM Element object, which should be queried when the component is rendered.
It is also possible to find needed nodes later with
this.$.find('domSelector')
orthis.find(componentSelect)
. But withcompos
object there is always the overview off all dom referenced nodes, and the performance is also better, as the nodes are queried once.For better debugging warning message is raised, when it fails to match the elements.
Syntax:
'compoName': 'selectorEngine: selector',
. Selector Engine:$
: query the dom nodes withjQuery | Zepto
.compo
: query the components dom to match a component- ``: none means to use native
querySelector
.
Example:
mask.registerHandler('Foo', mask.Compo({ template: 'input type=text; span.msg; SpinnerCompo;', compos: { input: '$: input', spinner: 'compo: SpinnerCompo', messageEl: '.msg' }, someFunction: function(){ // samples this.compos.input.val('Lorem ipsum'); this.compos.spinner.start(); this.compos.messageEl.textContent = '`someFunction` was called'; } }))
-
attr : Object
#Add additional attributes to the component. This object will also store the attributes defined from the template.
Foo name='fooName';
mask.registerHandler('Foo', mask.Compo({ tagName: 'input', attr: { id: 'MyID' }, someFunction: function(){ this.attr.name === 'fooName'; this.attr.id === 'MyID' } })); // result: <input name='fooName' id='MyID' />
-
onRenderStart : function(model, ctx, container:DOMElement): void | Deferred
#Is called before the component is rendered. In this function for example
this.nodes
andthis.model
can be overridden. Sometimes you have to fetch model data before proceeding, and from here this component rendering can be paused:onRenderStart: function(model, ctx, container){ var resume = Compo.pause(this, ctx); $.getJSON('/users').done(array => { this.model = array; resume(); }); // or just return defer object return $ .getJSON('/users') .done(array => this.model = array); }
Note Only this component is paused, if there are more async components, then awaiting and rendering occurs parallel
-
render : function(model, ctx, container)
#(rare used. Usually for some exotic rendering). When this function is defined, then the component should render itself and all children on its own, and the
onRenderStart
andonRenderEnd
are not called. -
onRenderEnd : function(elements:Array<DOMElement>, model, ctx, container)
#Is called after the component and all children are rendered.
this.$
, the DomLibrary(jQuery, Zepto) wrapper over the elements is now accessible.Note DOMElements are created in the
DocumentFragment
, and not the live dom. Refer todomInsert
if you need, for example, to calculate the elements dimensions. -
dispose : function()
#Is called when the component is removed.
-
setAttribute : function(key, val)
#Set attribute value. Sets also as property if defined in
meta.attributes
object -
getAttribute : function(key, val)
#Get property value if defined in
meta.attributes
object, or attribute value fromattr
. -
onAttributeSet : function(key, val)
#Is called after the attribute is set. Target value is used, even when transition is used to tween the value.
-
`onEnterFrame: function() #
Is called onRenderEnd and each time when attributes are changed.
requestAnimationFrame
is used -
meta : Object
#Stores some additional information for the component: for some validations and transforms
-
attributes
#Attributes, which are declared here, are then bound directly to the instance in
camelCase
manner. When some attribute values are not valid, the component is not rendered, and instead the error message is rendered. For better consistance custom attributes should start withx-
prefix, though it is not required, but ifx-
prefix missed, the it will be added for the property names.// Foo x-foo='5' x-quux='some value'; mask.registerHandler('Foo', mask.Compo({ meta: { attributes: { // required custom attribute, value is parsed to number 'x-foo': 'number', // optional custom attribute, value is parsed to boolean '?x-baz': 'boolean', // required 'x-quux': function (value) { // perform some custom check if (check(value) === false) return Error('Attributes value is not valid'); // optionally perform some object transformations/parsing return transform(value); }, // optional default values, the values are also converted to match the type. 'my-foo': 5, '' // via Object Configuration 'some-value': { // define type of the value, if not specified, will try to guess the type from `default` type: 'number', // make attribute optional, and provide default value default: 0, // validate value, return string or Error if any validate: function (val) { // `this` is a current component instance return 'Error message here' }, // transform value to something else transform: function (val, containerEl) { return val * 1000; }, // optionaly define the tweening rules // You can define, or redefine the rules also via attributes with the name, // e.g: `some-value-transition`' transition: '200ms easeInBounce' } } }, onRenderStart: function(){ this.xFoo === 5 this.xQuux } }));
-
template
#Defines how
template
property defined via the component declaration and thenodes
property defined in inlined mask template behavious towards each other.-
'replace'
- (default) Child nodes from the inlined mask markup (if any) will replace thetemplate
propertymask.registerHandler('Foo', mask.Compo({ template: 'h4 > "Hello"' }); mask.render('Foo') // `h4 > "Hello"` template is rendered mask.render('Foo > h1 > "World"') // `h1 > "World"` template is rendered
-
'merge'
-template
andnodes
will be merged using merge syntax// very basic sample, usually it would be much greater encapsulation mask.registerHandler('Foo', mask.Compo({ meta: { template: 'merge' }, template: 'h4 > @title;' }); mask.render('Foo > @title > "Foo"') // `h4 > "Foo"` template is rendered
-
'join'
-template
andnodes
will be concatenated
@see tests /test/meta/template.test for more examples
-
-
mode
#Render Mode. Relevant only to the NodeJS.
client
: Component is not rendered on the backend, but will be serialized and the rendered on the clientserver
: Component is rendered on the backend, and will not be bootstrapped on the clientboth
: (default) Component is rendered on the backend, and will be bootstrapped(initialized) on the client
-
-
Instance::$
#DOM Library wrapper of the elements (jQuery/Zepto/Kimbo).
-
Instance::find(selector:String)
#Find the child component. Selector:
// compo name this.find('Spinner') // id this.find('#mySpinner'); // class this.find('.mySpinner');
-
Instance::closest(selector:String)
#Find the first parent matched by selector.
-
Instance::remove()
#Removes elements from the DOM and calls
dispose
function on itself and all children -
Instance::slotState(slotName, isActive)
#Disable/Enable single slot signal - if is disabled, it will be not fired. And if no more active slots are available for a signal, then all HTMLElements with this signal get
disabled
property set totrue
-
Instance::signalState(signalName, isActive)
#Disables/Enables the signal - all slots in all controllers up in the tree will be also
enabled/disabled
// Foo > button x-signal='click: performAction' mask.registerHandler('Foo', mask.Compo({ slots: { performAction: function(){ this.signalState('performAction', false); // disable signal, so even when it is sent one more time, it wont be called // (button is also disabled as no more slots available for the signal) // fake some async job, and once again enable the signal setTimeout(() => this.signalState('performAction', true), 200); } } })
-
Instance::emitIn(signalName [, ...arguments])
#Send signal to itself and then DOWN in the controllers tree
-
Instance::emitOut(signalName [, ...arguments])
#Send signal to itself and then UP in the controllers tree
-
Compo.config:Object
#Contains configuration functions
-
Compo.config.setDOMLibrary($:Object)
#DOM Library
is a library, which makes it easer to manipulate the DOM. When theCompoJS
is loaded, it will try to pick up from globals some of this dom libraries: JQuery, Zepto or Kimbo. Each time the component is rendered, it will wrap its DOM child nodes using the DOM library and you can access it under$
property: e.g.this.$
-
-
Compo.pipe(name:String):Pipe
#Get the Pipe.
-
Pipe::emit(signal:String [, ...args])
#Emits the signal in a pipe.
mask.registerHandler('Some', Compo({ pipes: { 'foo': { // registers `bazSignal` signal in a `foo` pipe. bazSignal: function(...args){} } } })); Compo.pipe('foo').emit('bazSignal', 'Hello');
-
Attribute transitions are similar to css transition. Animation is performed via MaskJS bindings, additionally you can controll it manually in defined onEnterFrame
callback.
-
Timing Functions
linear
linearEase
easeInQuad
easeOutQuad
easeInOutQuad
easeInCubic
easeOutCubic
easeInOutCubic
easeInQuart
easeOutQuart
easeInOutQuart
easeInQuint
easeOutQuint
easeInOutQuint
easeInSine
easeOutSine
easeInOutSine
easeInExpo
easeOutExpo
easeInOutExpo
easeInCirc
easeOutCirc
easeInOutCirc
easeInElastic
easeOutElastic
easeInOutElastic
easeInBack
easeOutBack
easeInOutBack
easeInBounce
easeOutBounce
easeInOutBounce
Model binding sample:
mask.define('Foo', Compo({
meta: {
attributes: {
width: 0,
transition: '200ms linear'
}
}
}));
mask.render(`
input type=range min=0 max=250 > dualbind value=size;
Foo > h3 > '~[bind: $.xWidth]';
`, { size: 123 });
©️ Atma.js Project
- 2015 - MIT