- Changes to state are immediately reflected in the dom by their element matchers. Super crazy fast & 8KB Gzip!
- 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
- Built-in observables on existing data structures like objects, arrays for state and props reactivity
Introduction - 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"
state
is the DOM element(s) we want to set/get - the element Matchers and their controls- Any change to the
state
object will trigger a redraw of the view - it is observed. props
is what we want the DOM element(s) to be set to - the data- If the
props
have changed a new view redraw will run if triggered. props
are passed from the parent and is set bymag.module()
or an instanceinstance (props)
- Components can reference others Components in props to pass children elements directly into their output:
props.children = mag()
mag.module(document.body, {
controller: function() {
this.h2 = 'Hello MagJS World!';
}
});
Controllers are only called once vs views which are re-run on every change to the state. Example - Prevent initial placeholder flicker
var Component = {
controller: function() {
this.yourName = '';
}
}
Just define the default value of the selector to bind.
- Input - Checkbox - Radio - Select - Multiple
- Greedy selector
- Multiple fields
- Hookin attribute
- Delete Element - Remove & Reattach
- Intro Guide
- Guide quick example (1st component)
- Getting started with MagJS and Selectors
- Thinking in React in MagJS
- Comments Components from React
- Stateless Components
- Video Instructions
- More Tutorials..
JSBin - Ternary - Controller - Handler - Merge props - Subscribe - Proxy - No view only controller - Component prop - Parent Child Controller Comps - Componentized - Dynamic templates
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(e) {
state.name = e.target.value
}
}
}
})
Blank JSBin - Blank CodePen - Event Module - JSbin Addons - CodePen Addons (Simple Modal) - Plunker - Plunker Namespace - Plunker Templating - Stateless
Hello world - Hello world, take2 - With dynamic Node
Components
Simple Component Container - Reusable Clock component - Master detail - Stateless components perf - Clocks
Component Composition containment - Component composition specialization
props.children accessor - Access static children of placeholder from inner module - Specialization with props.children - Shared state
Reusable Tooltip Component - Controller only
Simple Wrapped component(HoC) - Reusable Container(HoC) - Switch and Remove Comps
Simple tabbed content - Initialization
Toggle Button - Custom Component - Counter Component
Async
Using fetch to render json data
Stateless
Timer - Counter - Interaction with state
Simple Todos - Stateless counter
Icons as Components - Deep Nesting (keys, children, create)
App State
Shared across modules - Synchronous state
Forms
Multiple Radio selection - Multiple Select Menu
Transform Input - Concatenate - To Upper Case
Slider - Textarea - Remove elements temporarily
Dynamic Radio Group - Pre-selected - Static group - MagJS Radio Button / Pads
Basic Math: addition - Basic Math: addition (no auto binding) - Take 3 - Take 4 - V0.12 auto wiring - Video tutorial - Nested data auto wiring
Auto wiring - select menu - Single form create/edit
Quiz - Instant validation - Field valid - Simple
Forms - input handling - AutoComplete w/spinner - Select menu - Select addon
Forms - passFail component - Search Highlight
Form & list - model - comps - boilerplate
Simple messaging component example - Video tutorial - Take 2, w/Reusable child component - Nested components
Hello world with passFail reusable component - Modal component w/CSS Transitions
Hello world (proxy w/Config) - Without controller - Without config or controller - Take2
Events
Parent Eventing - Node switching - Event handler in Child
Count - Dynamic reusable counter component - With shared state
Lists
Loading and using external data
Modals
Modal component - Reusable Modal Component
Modal with select menu - Modal Child Component
Modal with external template shared appState - Alternate with mag - Alternate with create - Addons vs Extends - mag.template
Forms - composable components - link manager
Todos Take2 - Take3 - Simple v0.2 - Simple v0.2 component, Take2 - v0.24
Todos
Animation
CSS transitions - Animation, FadeIn, FadeOut - Message Fader Component
Movie plotter service - Plot selection
Rotate Links - Alternate - Web service - Loader
Getting started with React example - Affix - Controller default
Navigation menu - As Component
TabList - key components - Without Binding
TabList module pattern - dynamic children keys - Video Tutorial
Real-time search Same with different code style - creative Mag.JS!
FilterableProductTable/static version (Thinking in React tutorial)
FilterableProductTable (Thinking in React tutorial)
Tab state (From Why React is Awesome)
Board Switcher - Stateless board
Comment Box - Video tutorial - Take1, Take 2 - Take3 - MagJS v0.14 - Module Pattern Video tutorial
Employee Directory with tiny Router - Take2 - Latest
Contact Manager application - Take 2
The scope of a components state/selectors is limited to its template elementId|Node
A nested component, child of a parent, will not be effected by the parent's state element selectors.
Statefull Example - Stateless Example
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/fopunubogi/edit?html,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/xayobawuxo/edit?html,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:
- is the element itself
- is a boolean stating if this is attaching or not, first run is always true, subsequent executions are always false
- 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)
- Index- the x path based index of the element
mag.module (String domElementID|Element Node, Object ModuleDefinition, Optional Object DefaultProperties )
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 can have a controller and a view function.
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/Node 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 (Element, currentProps, instanceId) {}
},
view: function (state, props, Element) {
this.state, this.props, this.element
}
}
this
in the view
has access to the 3 main properties of this.state
, this.props
and this.element
All _on
events context is this
This is a shortcut method to the internal makeClone function returned by mag.module
returns
a function to run the module and template with given props.
//Define Component:
var CounterComponent = {
view: function(state, props) {
state.div = "Count: " + props.count;
}
}
//Wire it:
var Counter = mag('counter', CounterComponent);
//Run:
var Element = Counter({count: state.count});
//Attach to state:
state.counter = Counter({count: state.count});
//Reflect on the component
Counter.getProps() ..
mag
can also be used to create stateless components
Which are helpful in constructing the UI.
Note: There are subtle differences between mag()
and mag.module.
- Skips
mag.module
setup- The major difference is that the normal setup in
mag.module
is not run on the template node. Therefore, theinstance
does not exist until it is called. - This means there is no pre-loading and caching in the UI and that it only runs on the template clone not the template itself. Example with mag - Direct with mag.module - Same with mag.module - Clone with key
- The major difference is that the normal setup in
- Each call to
mag()
with the same ID/Node reuses it by default mag()
defaults to a reference and does not create unique keys for you automatically. Example- In order to reuse one instance uniquely you must pass a key via props. Example or use
mag.create
from the AddOns. - Or simply call
mag()
again. Example - List - Defined - Button Factory
There are 8 life cycle events: willload, willgetprops, didload, willupdate, didupdate, isupdate, onbeforeunload, onunload
They each get the same 3 parameters, their context is the Object no need to bind to this
:
- 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.
- instance ID - Internal Mag.JS ID, can be used for reflection
- [nextProps (4th argument in willgetprops, contains the next props)]
- [done() (4th argument in onbeforeunload, function to call when completed)]
To prevent default from any Life Cycle method- stop continued processing
return false
this.willgetprops = function(node, currentProps, instanceId, nextProps) {
if (currentProps == nextProps) {
return false
}
}
Optionally, all life cycle methods can also be Object methods
var Component = {
willload: function() {
mag.merge(this.state, this.props)
this.state.input = {
_oninput: () => {
this.state.name = this.state.hello ? ' ' + this.state.hello + '!' : '?'
}
}
}
}
Try it on JSBin - With Props Update
Use the live instance or an instance ID
var instance = mag.module ('myElementId'|Element Node, 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 8 methods are bound to the exact instance
getId
draw
getState
getProps
clones
destroy
subscribe
- multiple subscribers allowed! returns
a remove function
rafBounce([Optional Boolean])
- returns Boolean. Use to change the rendering default engine of rAF throttle to rAF debounce
removeSelfFunc = mag.mod.onLCEvent('didupdate', instanceId, handlerFunc)
Available on all life cycle methods for any instanceId multiple handlers per event and instanceID are accepted.
returns
a remover function, call to stop the handler from being executed.
For inner reflection the instanceID is available in all lifecycle methods, Note that this is not the elementID but instead the internal MagJS ID for each component includes clones, example:
mag.redraw(mag.getNode(mag.getId(instanceID)), instanceID, 1);
mag.create (String elementID|Element Node, Object ModuleDefinition, Optional Object props) - In the Addons
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', {}) // not executed
var instance = myComponent({props:[]}) // executed
//add a props.key for a unique component instance or else each call reuses existing.
// instance contains 7 sub methods
instance.getId();
//returns instance UID for MagJS
//Use mag.getId(instanceId) to get the Node id and mag.getNode(ID) to get the Node itself
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)
// `returns` a Promise resolved on rAF
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, defaults to bound instance
instance.clones();
//v0.22.6 returns list of any clones with their associated instanceId, and its own subscribe handler.
instance.subscribe(function(state, props, node, previous){});
//v0.22.1 assign handler to an instance to be notified on unqiue changes after life cycle event `didupdate`
instance.destroy([Optional RemoveBoolean]);
//v0.23.5 - if optional remove boolean is true the entire node is removed.
// this calls all nodes config unloaders and the controllers onunload event which can preventDefault.
instance.rafBounce([Optional Boolean]);
//v0.27.2 - returns Boolean flag - used to change the rendering default engine of raf throttle to raf debounce
// 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 clone
Normally 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]
})
}]
Control redrawing flow
initiate a redraw manually
Optional boolean argument to force cache to be cleared
returns
a Promise which is resolved when the requestAnimationFrame is run.
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 are
Once 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.
// 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)
Option to select the requestAnimationFrame rendering strategy.
There is an optional global mag
integer (defaults to undefined) mag.rafRate
this will effect the utils.scheduleFlush
rAF refresh rate.
There is an optional global mag
boolean (defaults to undefined) mag.rafBounce
this will effect the utils.scheduleFlush
If set to true
performance/speed in rendering is enhanced but there can be a loss of smoothness in the dom painting such as jerky rendering.
You can also set per instance.rafBounce(Boolean)
the desired rAF, true
is the debounce, false
(default) is to throttle.
Returns
the current instance's boolean value.
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!' } }
- Events are bound to the module instance
this
. this
hasthis.props
,this.state
andthis.element
- Events receive arguments in this order f(event, index, node, data)
Dealing with lists are simple and intuitive, including nested lists with dynamic user based values.
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>
<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>
<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>
Data binding List with user input
This is the power and intuitive nature of MagJS. This is what allows for effortless and rapid HTML template prototyping.
With a minimal amount of code and a single row HTML template we can create a dynamic data table list that automatically stays up to date with dynamic values such as user input.
We start with our pure HTML template:
<div id="tickets">
<h2>How many tickets?</h2>
<table>
<tbody>
<tr class="ticketTypeRow">
<th class="ticketType">
Senior
</th>
<td class="numberofTickets">
<input name="quantity" type="number" min="0" maxlength="2" size="1">
</td>
<td class="timesX">x</td>
<td>$ <span class="pricePerTicket"></span>
</td>
<td class="equals">= $</td>
<td class="rowTotal">
<input name="total" size="8" readonly="readonly" tabindex="-1" value="0.00">
</td>
</tr>
</tbody>
</table>
</div>
Next we have our JavaScript data list:
var defaultProps = {
ticketTypeRow: [{
quantity: 0,
ticketType: 'senior',
total: 0.00,
pricePerTicket: 5.99
}, {
quantity: 0,
ticketType: 'adult',
total: 0.00,
pricePerTicket: 5.99
}, {
quantity: 0,
ticketType: 'child',
total: 0.00,
pricePerTicket: 3.99
}]
}
Where this data comes from or how it is loaded does not effect MagJS in any way. It can be async, iframe, web service, push, io sockets etc...
Normally we would mutate the data in some way through the native Array.map function to return a new Array that is bound by MagJS to our HTML template. In this example we are showing how that is not necessary.
Next, we create our module.
var Tickets = {}
Tickets.controller = function(props) {
// merge the props with the module's state/html
mag.utils.merge(this, props);
}
Tickets.view = function(state, props) {
state.$quantity = {
_onInput: function(event, index, node, data) {
var total = state.ticketTypeRow[data.index].quantity * state.ticketTypeRow[data.index].pricePerTicket
state.ticketTypeRow[data.index].total = total
}
}
}
As you can see we are not changing the props data array instead we are merging it directly into our state selectors.
Lastly we will now load the module for MagJS to do the DOM bindings:
mag.module("tickets", Tickets, defaultProps)
Try it on JSBin: Movie ticket quantity selection - Nested math input - Nested messaging components
_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 the target element
- 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})
- if promise is returned it will defere redraw until resolved
Life cycle events in controller:
- willload (node, props, instanceID)
- willgetprops (node, props, instanceID, nextProps)
- didload (node, props, instanceID)
- willupdate (node, props, instanceID)
- didupdate (node, props, instanceID)
- isupdate (node, props, instanceID)
- onbeforeunload (node, props, instanceID, done)
- onunload (node, props, instanceID)
return false
- will skip further execution.
It will 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)
- 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 (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 a stop by returning false
-
index is 0 based on xpath of the matcher
Allows for custom definitions, see examples below Examples: Promise, binding, custom attributes and elements.. Hookins
Tiny sub library of reusable simple tools can be found here
- router
- ajax
- Reusable utilities (copy, merge .. )
- namespace
- hookins
//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
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!
Example of extending the core Mag.JS functionality seamlessly
Allow for external template loading:
//Syntax via mag.template extends
mag.module('template.html', {view: ()});
mag('template.html', {view: ()});
mag.module({templateUrl: 'template.html', view: ()});
mag({templateUrl: 'template.html', view: ()});
-
config attribute won't be called with inner id element matchers, use other element matcher selectors. Fixed in v0.25.5 Example
-
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. Fixed: MagJS will throw a Error if it detects recursivity (a instance call within an instance call) - Try it on JSBin
-
object observe support for browsers (v0.22 uses native Proxy to observe)
<script src="//cdn.rawgit.com/MaxArt2501/object-observe/master/dist/object-observe-lite.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]-->
JSBin - dynamic re-rendering - v0.20.7 - v0.21.3 - v0.22 - Latest - Toggle rAF (Throttle vs Debounce)
Occlusion culling - v0.22 - Latest - Throttle rAF rate - Debounce
JSBin - reversing 1000s of rows - v0.22 - Latest - Optimized - Componentized
Dbmon Repaint rate - v0.23 - Latest
JsPerf v0.20.2 - JsPerf v0.20.2