Skip to content

atmajs/mask-compo

Repository files navigation

Mask Component Library

Build Status

Getting started

<!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 and mouse* events are also mapped to corresponding touch* events, when also touch input is supported.

Api

Create a component

mask.Compo(ComponentProto: Object):Function

Returns the components constructor. You would want to add it to masks repo:

mask.registerHandler('someTagName', mask.Compo(ComponentProto));

Inheritance

mask.Compo(...base:String|Object|Function, ComponentProto)

  • String: Name of the component. The component must be registered with mask.registerHandler
  • Object: Any object. Note: deep property extending is used.
  • Function: Note: constructor is also inherited and will be automatically invoked. Also prototype data is inherited.

onRenderStart, onRenderEnd, slots and pipes are called automatically one after another starting from the first inherited component. All other functions will have super 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();
	}
})

ComponentsProto

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 the tagName sample, but to write some the templates in javascript files is not always a good idea. Better to preload the template with IncludeJS for example. Note: The Application can be built for production. All the templates are then embedded into single html 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 controllers slotName:Function. slotName ~ signalName are equivalent

    Signal 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 DOM

       	slots: {
       		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 via pipes. 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 in pipes 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') or this.find(componentSelect). But with compos 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 with jQuery | 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 and this.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 and onRenderEnd 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 to domInsert 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 from attr.

  • 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 with x- prefix, though it is not required, but if x- 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 the nodes property defined in inlined mask template behavious towards each other.

      • 'replace' - (default) Child nodes from the inlined mask markup (if any) will replace the template property

         	mask.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 and nodes 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 and nodes 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 client
      • server: Component is rendered on the backend, and will not be bootstrapped on the client
      • both : (default) Component is rendered on the backend, and will be bootstrapped(initialized) on the client

Instance

  • 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 to true

  • 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

Static

  • Compo.config:Object #

    Contains configuration functions

    • Compo.config.setDOMLibrary($:Object) #

      DOM Library is a library, which makes it easer to manipulate the DOM. When the CompoJS 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');

Animation

Transition

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

Releases

No releases published

Packages

No packages published