| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377 |
- var config = require('./config'),
- Emitter = require('emitter'),
- DirectiveParser = require('./directive-parser'),
- TextNodeParser = require('./textnode-parser')
- var slice = Array.prototype.slice,
- ctrlAttr = config.prefix + '-controller',
- eachAttr = config.prefix + '-each'
- var depsObserver = new Emitter(),
- parsingDeps = false
- /*
- * The main ViewModel class
- * scans a node and parse it to populate data bindings
- */
- function Seed (el, options) {
- 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]
- }
- // initialize the scope object
- var dataPrefix = config.prefix + '-data'
- var scope = this.scope =
- (options && options.data)
- || config.datum[el.getAttribute(dataPrefix)]
- || {}
- el.removeAttribute(dataPrefix)
- // if the passed in data is already consumed by
- // a Seed instance, make a copy from it
- if (scope.$seed) {
- scope = this.scope = scope.$dump()
- }
- // expose some useful stuff on the scope
- scope.$seed = this
- scope.$destroy = this._destroy.bind(this)
- scope.$dump = this._dump.bind(this)
- scope.$index = options.index
- scope.$parent = options.parentSeed && options.parentSeed.scope
- // add event listener to update corresponding binding
- // when a property is set
- this.on('set', this._updateBinding.bind(this))
- // now parse the DOM
- this._compileNode(el, true)
- // if has controller function, apply it
- var ctrlID = el.getAttribute(ctrlAttr)
- if (ctrlID) {
- el.removeAttribute(ctrlAttr)
- var factory = config.controllers[ctrlID]
- if (factory) {
- factory.call(this, this.scope)
- } else {
- console.warn('controller ' + ctrlID + ' is not defined.')
- }
- }
- // extract dependencies for computed properties
- parsingDeps = true
- this._computed.forEach(this._parseDeps.bind(this))
- delete this._computed
- parsingDeps = false
- }
- /*
- * Compile a node (recursive)
- */
- Seed.prototype._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
- var child = new Seed(node, {
- child: true,
- parentSeed: seed
- })
- if (node.id) {
- seed['$' + node.id] = child
- }
- } else { // normal node
- // parse if has attributes
- if (node.attributes && node.attributes.length) {
- 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(function (child) {
- seed._compileNode(child)
- })
- }
- }
- }
- }
- /*
- * Compile a text node
- */
- Seed.prototype._compileTextNode = function (node) {
- return TextNodeParser.parse(node)
- }
- /*
- * Add a directive instance to the correct binding & scope
- */
- Seed.prototype._bind = function (directive) {
- directive.seed = this
- var key = directive.key,
- epr = this.eachPrefixRE,
- isEachKey = epr && epr.test(key),
- scope = this
- if (isEachKey) {
- key = directive.key = key.replace(epr, '')
- }
- if (epr && !isEachKey) {
- scope = this.parentSeed
- }
- var ownerScope = determinScope(directive, scope),
- binding =
- ownerScope._bindings[key] ||
- ownerScope._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)
- }
- Seed.prototype._createBinding = function (key) {
- var binding = new Binding(this.scope[key])
- this._bindings[key] = binding
- // bind accessor triggers to scope
- var seed = this
- Object.defineProperty(this.scope, key, {
- get: function () {
- if (parsingDeps) {
- depsObserver.emit('get', binding)
- }
- seed.emit('get', key)
- return binding.isComputed
- ? binding.value()
- : binding.value
- },
- set: function (value) {
- if (value === binding.value) return
- seed.emit('set', key, value)
- }
- })
- return binding
- }
- Seed.prototype._updateBinding = function (key, value) {
- var binding = this._bindings[key],
- type = binding.type = typeOf(value)
- // preprocess the value depending on its type
- if (type === 'Object') {
- if (value.get) { // computed property
- this._computed.push(binding)
- binding.isComputed = true
- value = value.get
- } else { // normal object
- // TODO watchObject
- }
- } else if (type === 'Array') {
- watchArray(value)
- value.on('mutate', function () {
- binding.emitChange()
- })
- }
- binding.value = value
- // update all instances
- binding.instances.forEach(function (instance) {
- instance.update(value)
- })
- // notify dependents to refresh themselves
- binding.emitChange()
- }
- Seed.prototype._parseDeps = function (binding) {
- depsObserver.on('get', function (dep) {
- if (!dep.dependents) {
- dep.dependents = []
- }
- dep.dependents.push.apply(dep.dependents, binding.instances)
- })
- binding.value()
- depsObserver.off('get')
- }
- Seed.prototype._unbind = function () {
- var unbind = function (instance) {
- if (instance.unbind) {
- instance.unbind()
- }
- }
- for (var key in this._bindings) {
- this._bindings[key].instances.forEach(unbind)
- }
- }
- Seed.prototype._destroy = function () {
- this._unbind()
- delete this.el.seed
- this.el.parentNode.removeChild(this.el)
- if (this.parentSeed && this.id) {
- delete this.parentSeed['$' + this.id]
- }
- }
- Seed.prototype._dump = function () {
- var dump = {}, binding, val,
- subDump = function (scope) {
- return scope.$dump()
- }
- for (var key in this._bindings) {
- binding = this._bindings[key]
- val = binding.value
- if (!val) continue
- if (Array.isArray(val)) {
- dump[key] = val.map(subDump)
- } else if (typeof val !== 'function') {
- dump[key] = val
- } else if (binding.isComputed) {
- dump[key] = val()
- }
- }
- return dump
- }
- /*
- * Binding class
- */
- function Binding (value) {
- this.value = value
- this.instances = []
- this.dependents = []
- }
- Binding.prototype.emitChange = function () {
- this.dependents.forEach(function (dept) {
- dept.refresh()
- })
- }
- // Helpers --------------------------------------------------------------------
- /*
- * determinScope()
- * determine which scope a key belongs to based on nesting symbols
- */
- function determinScope (key, scope) {
- if (key.nesting) {
- var levels = key.nesting
- while (scope.parentSeed && levels--) {
- scope = scope.parentSeed
- }
- } else if (key.root) {
- while (scope.parentSeed) {
- scope = scope.parentSeed
- }
- }
- return scope
- }
- /*
- * typeOf()
- * get accurate type of an object
- */
- var OtoString = Object.prototype.toString
- function typeOf (obj) {
- return OtoString.call(obj).slice(8, -1)
- }
- /*
- * watchArray()
- * augment an Array so that it emit events when mutated
- */
- var arrayMutators = ['push','pop','shift','unshift','splice','sort','reverse']
- var arrayAugmentations = {
- remove: function (scope) {
- this.splice(scope.$index, 1)
- },
- replace: function (index, data) {
- if (typeof index !== 'number') {
- index = index.$index
- }
- this.splice(index, 1, data)
- }
- }
- function watchArray (collection) {
- Emitter(collection)
- arrayMutators.forEach(function (method) {
- collection[method] = function () {
- var result = Array.prototype[method].apply(this, arguments)
- collection.emit('mutate', {
- method: method,
- args: Array.prototype.slice.call(arguments),
- result: result
- })
- }
- })
- for (var method in arrayAugmentations) {
- collection[method] = arrayAugmentations[method]
- }
- }
- Emitter(Seed.prototype)
- module.exports = Seed
|