Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Clone in Desktop Download ZIP
MagJS - Modular Application Glue
JavaScript HTML CSS
Latest commit c575ce0 @magnumjs Update README.md

README.md

Mag.JS - Elegant DOM Bindings

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

Mag.JS - Elegant DOM Bindings

"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"

  1. state is the DOM element(s) we want to set - the element Matchers and their controls
    1. Any change to the state object will trigger a redraw of the view - it is observed.
  2. props is what we want the DOM element(s) to be set to - the data
    1. If the props have changed a new view redraw will run if triggered.
    2. props is passed from the parent and is set by mag.module()

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

Auto wiring - select menu

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):

Hello array lifecycle event:

Count:

List:

More lists:

Sortable List:

Tiny filter:

Filter list:

Filter list sort:

Filter list components:

Quiz

Forms - passFail component

Form & list - model - comps - boilerplate

Tab component: - Take2

Modal component:

Modal with select menu:

Forms - composable components - link manager

Todos: Take2 - Take3

Todo proxy (firefox)

Todos (expanded):

Contacts: - Take 2

Async:

Async - Geo Location

Mithril 2 Mag

Rotate Links

Pagination

Volunteer form application

Ajax Github Api

React 2 Mag

Navigation menu:

Timer:

TabList - key components

TabList module pattern - dynamic children keys - Video Tutorial

Real-time search: Same with different code style - creative Mag.JS!

FilterableProductTable (Thinking in React tutorial):

Occlusion culling

Tab state (From Why React is Awesome)

Weather App: Take 2

Comment Box: - Video tutorial: - Take1, Take 2 - Take3 - MagJS v0.14 - Module Pattern Video tutorial

Image app with AJAX

Employee Directory with tiny Router

News feed with undo state

Mag Redux implementation

Mag Redux Async

Angular 2 Mag

Order form:

Navigation menu:

Switchable Grid:

Contact Manager application: - Take 2

Country App - JSON/Routing

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 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]
  })
}]

JSBin example

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 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.

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>

JsBin Example

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 -

Occlusion culling

JSBin - dynamic re-rendering v0.20.7 -

JSBin - reversing 1000s of rows

Dbmon Repaint rate

JsPerf v0.20.2 - JsPerf v0.20.2

JsPerf v0.20.3 - JsPerf v0.20.3

JSPerf v0.14.4

JSPerf v0.14.9

JSPerf v0.15

JSPerf v0.15.1

Inspired By & cloned from

Mithril.js, Fill.js, React.js, Angular.js, Fastdom

Something went wrong with that request. Please try again.