| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265 |
- var config = require('./config'),
- Scope = require('./scope'),
- Binding = require('./binding'),
- DirectiveParser = require('./directive-parser'),
- TextParser = require('./text-parser'),
- depsParser = require('./deps-parser')
- var slice = Array.prototype.slice,
- ctrlAttr = config.prefix + '-controller',
- eachAttr = config.prefix + '-each'
- /*
- * The main ViewModel class
- * scans a node and parse it to populate data bindings
- */
- function Seed (el, options) {
- config.log('\ncreated new Seed instance.\n')
- if (typeof el === 'string') {
- el = document.querySelector(el)
- }
- this.el = el
- el.seed = this
- this._bindings = {}
- this._computed = []
- // copy options
- options = options || {}
- for (var op in options) {
- this[op] = options[op]
- }
- // check if there's passed in data
- var dataAttr = config.prefix + '-data',
- dataId = el.getAttribute(dataAttr),
- data = (options && options.data) || config.datum[dataId]
- if (dataId && !data) {
- config.warn('data "' + dataId + '" is not defined.')
- }
- data = data || {}
- el.removeAttribute(dataAttr)
- // if the passed in data is the scope of a Seed instance,
- // make a copy from it
- if (data.$seed instanceof Seed) {
- data = data.$dump()
- }
- // initialize the scope object
- var key,
- scope = this.scope = new Scope(this, options)
- // copy data
- for (key in data) {
- scope[key] = data[key]
- }
- // if has controller function, apply it so we have all the user definitions
- var ctrlID = el.getAttribute(ctrlAttr)
- if (ctrlID) {
- el.removeAttribute(ctrlAttr)
- var factory = config.controllers[ctrlID]
- if (factory) {
- factory(this.scope)
- } else {
- config.warn('controller "' + ctrlID + '" is not defined.')
- }
- }
- // now parse the DOM
- this._compileNode(el, true)
- // for anything in scope but not binded in DOM, create bindings for them
- for (key in scope) {
- if (key.charAt(0) !== '$' && !this._bindings[key]) {
- this._createBinding(key)
- }
- }
- // extract dependencies for computed properties
- if (this._computed.length) depsParser.parse(this._computed)
- delete this._computed
- }
- // for better compression
- var SeedProto = Seed.prototype
- /*
- * Compile a DOM node (recursive)
- */
- SeedProto._compileNode = function (node, root) {
- var seed = this
- if (node.nodeType === 3) { // text node
- seed._compileTextNode(node)
- } else if (node.nodeType === 1) {
- var eachExp = node.getAttribute(eachAttr),
- ctrlExp = node.getAttribute(ctrlAttr)
- if (eachExp) { // each block
- var directive = DirectiveParser.parse(eachAttr, eachExp)
- if (directive) {
- directive.el = node
- seed._bind(directive)
- }
- } else if (ctrlExp && !root) { // nested controllers
- new Seed(node, {
- child: true,
- parentSeed: seed
- })
- } else { // normal node
- // parse if has attributes
- if (node.attributes && node.attributes.length) {
- // forEach vs for loop perf comparison: http://jsperf.com/for-vs-foreach-case
- // takeaway: not worth it to wrtie manual loops.
- slice.call(node.attributes).forEach(function (attr) {
- if (attr.name === ctrlAttr) return
- var valid = false
- attr.value.split(',').forEach(function (exp) {
- var directive = DirectiveParser.parse(attr.name, exp)
- if (directive) {
- valid = true
- directive.el = node
- seed._bind(directive)
- }
- })
- if (valid) node.removeAttribute(attr.name)
- })
- }
- // recursively compile childNodes
- if (node.childNodes.length) {
- slice.call(node.childNodes).forEach(seed._compileNode, seed)
- }
- }
- }
- }
- /*
- * Compile a text node
- */
- SeedProto._compileTextNode = function (node) {
- var tokens = TextParser.parse(node)
- if (!tokens) return
- var seed = this,
- dirname = config.prefix + '-text',
- el, token, directive
- for (var i = 0, l = tokens.length; i < l; i++) {
- token = tokens[i]
- el = document.createTextNode()
- if (token.key) {
- directive = DirectiveParser.parse(dirname, token.key)
- if (directive) {
- directive.el = el
- seed._bind(directive)
- }
- } else {
- el.nodeValue = token
- }
- node.parentNode.insertBefore(el, node)
- }
- node.parentNode.removeChild(node)
- }
- /*
- * Add a directive instance to the correct binding & scope
- */
- SeedProto._bind = function (directive) {
- var key = directive.key,
- seed = directive.seed = this
- // deal with each block
- if (this.each) {
- if (key.indexOf(this.eachPrefix) === 0) {
- key = directive.key = key.replace(this.eachPrefix, '')
- } else {
- seed = this.parentSeed
- }
- }
- // deal with nesting
- seed = traceOwnerSeed(directive, seed)
- var binding = seed._bindings[key] || seed._createBinding(key)
- // add directive to this binding
- binding.instances.push(directive)
- directive.binding = binding
- // invoke bind hook if exists
- if (directive.bind) {
- directive.bind(binding.value)
- }
- // set initial value
- directive.update(binding.value)
- if (binding.isComputed) {
- directive.refresh()
- }
- }
- /*
- * Create binding and attach getter/setter for a key to the scope object
- */
- SeedProto._createBinding = function (key) {
- config.log(' created binding: ' + key)
- var binding = new Binding(this, key)
- this._bindings[key] = binding
- if (binding.isComputed) this._computed.push(binding)
- return binding
- }
- /*
- * Call unbind() of all directive instances
- * to remove event listeners, destroy child seeds, etc.
- */
- SeedProto._unbind = function () {
- var i, ins
- for (var key in this._bindings) {
- ins = this._bindings[key].instances
- i = ins.length
- while (i--) {
- if (ins[i].unbind) ins[i].unbind()
- }
- }
- }
- /*
- * Unbind and remove element
- */
- SeedProto._destroy = function () {
- this._unbind()
- this.el.parentNode.removeChild(this.el)
- }
- // Helpers --------------------------------------------------------------------
- /*
- * determine which scope a key belongs to based on nesting symbols
- */
- function traceOwnerSeed (key, seed) {
- if (key.nesting) {
- var levels = key.nesting
- while (seed.parentSeed && levels--) {
- seed = seed.parentSeed
- }
- } else if (key.root) {
- while (seed.parentSeed) {
- seed = seed.parentSeed
- }
- }
- return seed
- }
- module.exports = Seed
|