Intuitive, tiny, fast, JavaScript 2 HTML component templating library.
Features
- Changes to state are immediately reflected in the dom by their element matchers. Super crazy fast & 5KB Gzipped!
- Valid HTML templates - No virtual/shadow dom or new templating language!
- Semantic data binding - Use normal HTML as a template and a related module (plain JS object) as instructions for transpiling/interpolations.
- Module has a constructor, called once and a viewer called on every change to the state of that module.
- Collection rendering - No need for hand-written loops. Write templates as a part of the HTML, in plain HTML
- View logic in JavaScript - No crippled micro-template language, just plain JavaScript functions
- Native events & attributes, full life cycle events control, Hookin to modify and create custom attributes
Getting started ::: Examples - Tutorials - Api - Tests - Performance
"There is no JavaScript code in the HTML and there is no HTML code in the JavaScript!"
Initial dom:
<div id="hello">
<h1></h1>
</div>
Module:
mag.module('hello', {
view: function(state) {
state.h1 = 'Hello Mag.JS!'
}
})
Mag.JS dom!:
<div id="hello">
<h1>Hello Mag.JS!</h1>
</div>View receives 2 arguments, "state" & "props"
stateis the DOM element(s) we want to set - the element Matchers and their controls- Any change to the
stateobject will trigger a redraw of the view - it is observed.
- Any change to the
propsis what we want the DOM element(s) to be set to - the data- If the
propshave changed a new view redraw will run if triggered. propsis passed from the parent and is set bymag.module()
- If the
Tutorials
- Introduction Part1
- Introduction Part2
- Comments Components from React
- Contacts Components from Mithril
- Video Instructions
- Composable components
- MagJS version of James Longs' "why react is awesome"
- More Tutorials..
Hello world!
JSBin - Take 2 - Take3 - Take 4 - Take 5
Initial html
<div id="hello">
<label>Name:</label>
<input type="text" placeholder="Enter a name here" />
<hr/>
<h1>Hello <span class="name"></span>!</h1>
</div>Module:
mag.module("hello", {
view: function(state) {
state.input = {
_oninput: function() {
state.name = this.value
}
}
}
})Boilerplates
Boilerplate JSbin - JSbin v0.2 - Boilerplate Plunker - Boilerplate Plunker Modular - Boilerplate Plunker Modular v0.2
Examples
Hello world - Hello world, take2
Basic Math: addition - Basic Math: addition (no auto binding) - Take 3 - Take 4 - V0.12 auto wiring - Video tutorial - Nested data auto wiring
Simple messaging component example - Video tutorial - Take 2, w/Reusable child component
Hello world with passFail reusable component:
Hello world (proxy support/Firefox since v0.7.4+, polyfill object.observe since v0.8.6):
Form & list - model - comps - boilerplate
Forms - composable components - link manager
Mithril 2 Mag
React 2 Mag
TabList module pattern - dynamic children keys - Video Tutorial
Real-time search: Same with different code style - creative Mag.JS!
FilterableProductTable (Thinking in React tutorial):
Tab state (From Why React is Awesome)
Comment Box: - Video tutorial: - Take1, Take 2 - Take3 - MagJS v0.14 - Module Pattern Video tutorial
Employee Directory with tiny Router
Angular 2 Mag
Contact Manager application: - Take 2
Jasmine Specs
Statefullness
When redrawing the view method is called. To maintain statefulness we can use the controller method. Plainly these are default values.
HTML for below examples:
<div id="lister">
<h2></h2>
<ul>
<li class="item"></li>
</ul>
</div>Example without controller
mag.module('lister', {
view: function(state, props, element) {
state.item = [1, 2, 3]
state.title = 'Lister'
state.h2 = {
_text: state.title,
_onclick: function() {
state.show = state.show ? !state.show : true
state.item.reverse()
state.title = 'Gister' + state.show
}
}
}
})Example with controller
mag.module('lister', {
controller: function(props) {
this.item = [1, 2, 3]
this.title = 'Lister'
},
view: function(state, props, element) {
state.h2 = {
_text: state.title,
_onclick: function() {
state.show = state.show ? !state.show : true
state.item.reverse()
state.title = 'Gister' + state.show
}
}
}
})This link displays both for comparison: http://jsbin.com/yigoleyemu/edit?js,output
You can see that the first one when clicked nothing is changed while the second is dynamic. The reasons is simply because the controller is called once while the view is called on every redraw/action/state change.
Here's an alternative approach to the above that only uses a view method and no controller for a similar result: http://jsbin.com/juvisawici/edit?js,output
Example with config and without controller
mag.module("lister", {
view: function(state) {
var name1 = 'Yo!',
name2 = 'Joe!'
state.h2 = {
_config: function(node, isNew) {
if (isNew) {
state.span = name1
state.item = [1, 2, 3]
}
},
_onclick: function() {
state.item.reverse()
state.span = state.span == name1 && name2 || name1;
}
}
}
})This is similar to using a controller or onload. Every element has a _config to act as onload for hookins. It receives 4 arguments:
- 1. is the element itself
- 2. is a boolean stating if this is attaching or not, first run is always true, subsequent executions are always false
- 3. context is an object that can be used to pass values to the method itself on every iterative call
- a. one available sub method of context is onunload e.g. context.onunload = fun is called when the element is removed from the dom.
- - context.onunload (configContext, node, xpath)
- 4. Index- the x path based index of the element
Simple API
mag.module (String domElementID, Object ModuleDefinition, Optional Object Properties )
This is the core function to attach a object of instructions to a dom element, when called it is executed.
ModuleDefinition is the instructions it needs to have a view function, controller is optional:
var component = {
view: function (state, props, element) {
}
}view receives three arguments: state, props and element
- State is the object used to transpile the dom
- e.g. state.h1 ='Hello' converts the first h1 tag in the element to that value
- Props is the optional properties object passed to its mag.module definition
- Element is the node itself whose ID was passed to its mag.module definition
The controller function has access to the original props as well as all life cycle events, it is only called once.
var component = {
controller: function (props) {
this.didupdate = function (Event, Element, newProps) {
},
view: function (state, props, element) {
}
}There are 7 life cycle events: willload, didload, willupdate, didupdate, isupdate, onunload, onreload
They each get the same 3 parameters, their context is the controller no need to bind to this:
- Event - can be used to preventDefault - stop continued processing
- Element is the original module definition ID element
- newProps is the active state of the props, since the controller is only called once, the original props parameter contains the original default values.
var instance = mag.module ('myElementId', component);Returns a function Object that can be used to create a clone of the instance and the instances information such as InstanceID.
The function object to create a clone instance requires an index/key in its only parameter. When assigned to a state elementMatcher, MagJS does that for you.
These 4 methods are bound to the exact instance
getId
draw
getState
getProps
mag.create (String elementID, Object ModuleDefinition, Optional Object props) - v0.20
Wraps around mag.module to return a reference instance you can call later.
The reference function can also over write the defaults given in create usually it will only over write the props
var myComponent = mag.create('mydomId', {view:noop}) // not executed
var instance = myComponent({props:[]}) // executed
// instance contains 4 sub methods
instance.getId() // returns UID for MagJS
instance.draw() // redraws that unique instance, wrap in setTimeout for async
// optional boolean to force redraw i.e. clear the instance's cache instance.draw(true)
instance.getState() // returns a copy of the current state values of that instance - state is async
instance.getProps() // returns a copy of the current props values of that instance
// instance can be called directly with an index/key to clone the instance, usefull in data arrays
instance('myUniqueKeyIndex') // Usually not called directly, MagJS will create index when attached to state
// returns the live node cloneNormally there's no need to call the instance constructor function directly. When passed to a state object MagJS will create the index for you with or without a key provided in props.
state.myELementMatcher = myComponent({
props: []
})
// array
state.myELementMatcher = [myComponent({
props: [3, 2, 1]
}), myComponent({
props: [1, 2, 3]
})]
//Array object
state.myELementMatcher = [{
item: myComponent({
props: [3, 2, 1]
})
}, {
item: myComponent({
props: [1, 2, 3]
})
}]mag.redraw (node Element, idInstance magId, optional force Boolean)
initiate a redraw manually
Optional boolean argument to force cache to be cleared
mag.hookin (type, key, handler)
Allows for custom definitions, see examples below
Control redrawing flow
mag.begin ( int MagJS uid)
var instance = mag.module('app', module)
mag.begin(instance.getId())
// run some long standing process without redrawing the module no matter what the state changes areOnce called the module will not run a redraw until the corresponding mag.end(id) is called even if instance.draw() is called and even with the optional instance.draw(force true)it will not run.
mag.end ( int MagJS uid)
// run the redraw for the module
mag.end(instance.getId())This will run the last redraw for the instance assuming the number of begins match the number of ends called.
If you call mag.begin(id) for the same instance ID twice you must call mag.end(id) the same number of times before it will run the redraw.
This is typically not necessary especially since MagJS runs updates to the module state very efficiently via the rAF (requestAnimationFrame)
state object
State is the object that is watched for changes and is used to transpile the related dom parent element ID
there are 5 ways to reference an element within a module
- class name
- tag name
- data-bind attribute value
- id
- or name attribute value
state.h1 will match the first h1 element within a module (element id or parent node)
This: <h1></h1>
With: state.h1 = 'Hello!'
Makes: <h1>Hello!</h1>state.$h1 will match all h1s - greedy matcher, default only selects the first
To change the class for an element
This: <h1></h1>
With: state.h1 = { _class: 'header', _text : 'Hello!'}
Makes: <h1 class="header">Hello!</h1>_text and _html are used to fill an elements text node and not as an attribute below.
any prefix underscore will be an attribute except for _on that will be for events such as
state.h1 = { _onclick: function() { state.h1='clicked!' } } Lists
Dealing with lists are simple and intuitive, including nested lists.
The first list element is used as the template for all new items on the list For example:
<ul><li class="item-template"></li></ul>state.li = [1,2]Will render
<ul>
<li class="item-template">1</li>
<li class="item-template">2</li>
</ul>Lists of Objects
<ul><li class="item-template">People: <b class="name"></b></li></ul>state.li = [{name:'Joe'},{name:'Bob'}]Will render
<ul>
<li class="item-template">People: <b class="name">Joe</b>
</li>
<li class="item-template">People: <b class="name">Bob</b>
</li>
</ul>Nested Lists
<ul>
<li class="item-template">Project: <b class="projectName"></b>
<ul>
<li class="doneBy">
<name/>
</li>
</ul>
<tasks/>
</li>
</ul>state['item-template'] = [{
projectName: 'house cleaning',
doneBy: [{
name: 'Joe'
}, {
name: 'Bob'
}],
tasks: ['wash', 'rinse', 'repeat']
}, {
projectName: 'car detailing',
doneBy: [{
name: 'Bill'
}, {
name: 'Sam'
}],
tasks: ['wash', 'rinse', 'repeat']
}]Will render
<ul>
<li class="item-template">Project: <b class="projectName">house cleaning</b>
<ul>
<li class="doneBy">
<name>Joe</name>
</li>
<li class="doneBy">
<name>Bob</name>
</li>
</ul>
<tasks>wash</tasks>
<tasks>rinse</tasks>
<tasks>repeat</tasks>
</li>
<li class="item-template">Project: <b class="projectName">car detailing</b>
<ul>
<li class="doneBy">
<name>Bill</name>
</li>
<li class="doneBy">
<name>Sam</name>
</li>
</ul>
<tasks>wash</tasks>
<tasks>rinse</tasks>
<tasks>repeat</tasks>
</li>
</ul>Attributes
_html, _text, _on[EVENT], _config->context.onunload
to not overwrite an existing attribute use:
state.name._value = state.name._value + ''
event (e, index, node, data) default context is node
- index is the xpath index of the node -1
- data is the index data of the parent if in a list (map{path,data,node,index})
Events
Life cycle events in controller:
- willload (event, node)
- didload (event, node)
- willupdate (event, node)
- didupdate (event, node)
- isupdate (event, node)
- onunload (event, node)
- onreload (event, node)
event.preventDefault() - will skip further execution and call any onunload handlers in the current module (includes inner modules and _config onunloaders that are currently assigned)
controller -> this.willload state.matcher._onclick = function(e, index, node, data)
Native events: parameters -
- the event
- the x path based 0 index
- the node itself (default context)
- the data of the closest parent list item (In nested lists, the first parent with data).
Config (DOM hookin)
_config (node, isNew, context, index)
Available on all matchers to hookin to the DOM
arguments :
node - the element itself
isNew is true initially when first run and then is false afterwards
context is a empty object you can use to pass to itself
- context.onunload - will be attached to the current modules onunloaders and called if any lifecycle event triggers e.preventDefault()
index is 0 based on xpath of the matcher
Mag.JS AddOns!
Tiny sub library of reusable simple tools can be found here
- router
- ajax
- Reusable utilities (copy, merge .. )
- namespace
mag.namespace (String namespace, [Optional object Context])
//module library creation with single global namespace / package names
(function(namespace) {
var mod = {
controller:function(props){
},
view: function(state, props) {
}
}
namespace.CommentBox = mod;
})(mag.namespace('mods.comments'));
var CommentsComponent = mag.create("CommentBox", mag.mod.comments, props);
CommentsComponent();Allows you to easily add new namespaces to your composable components, useful in the module pattern.
Example of component Module Pattern - Video tutorial
Custom plugins
The ability to register handlers for attribute or value trans compilation.
For example, allow the attribute _className. Register a handler that on every definition will modify both the final attribute name and or the value.
mag.hookin('attributes', 'className', function(data) {
var newClass = data.value
data.value = data.node.classList + ''
if (!data.node.classList.contains(newClass)) {
data.value = data.node.classList.length > 0 ? data.node.classList + ' ' + newClass : newClass
}
data.key = 'class'
})The above is in the MagJS addons library
Another example
Hookin when a specific elementMatcher is not found and return a set of element matches
// hookin to create an element if does not exist at the root level
mag.hookin('elementMatcher', 'testme', function(data) {
// data.key, data.node, data.value
var fragment = document.createDocumentFragment(),
el = document.createElement('div');
el.setAttribute('class', data.key)
fragment.appendChild(el);
var nodelist = fragment.childNodes;
data.node.appendChild(fragment)
data.value = nodelist
})Other hookins such as key/node value etc.. Coming soon!
Notes
config attribute won't be called with inner id element matchers, use other element matcher selectors
careful with module instance constructor, can stack overflow if circular reference. Don't call instance from within itself or on state, use separate module. See examples
object observe support for browsers
<script src="//rawgit.com/MaxArt2501/array-observe/master/array-observe.min.js"></script>
<script src="//cdn.rawgit.com/MaxArt2501/object-observe/master/dist/object-observe.min.js"></script>- Promise support for IE
<!--[if IE]><script src="https://cdn.rawgit.com/jakearchibald/es6-promise/master/dist/es6-promise.min.js"></script><![endif]-->Performance
JSBin - dynamic re-rendering -
JSBin - dynamic re-rendering v0.20.7 -
JSBin - reversing 1000s of rows
JsPerf v0.20.2 - JsPerf v0.20.2
JsPerf v0.20.3 - JsPerf v0.20.3