Evan You 13 роки тому
батько
коміт
761b643baa
11 змінених файлів з 213 додано та 1903 видалено
  1. 4 1680
      dist/seed.js
  2. 0 0
      dist/seed.min.js
  3. 26 0
      examples/new-api-test.html
  4. 78 77
      examples/todomvc/js/app.js
  5. 31 59
      src/compiler.js
  6. 0 10
      src/config.js
  7. 6 5
      src/deps-parser.js
  8. 3 2
      src/directive-parser.js
  9. 23 60
      src/main.js
  10. 23 1
      src/utils.js
  11. 19 9
      src/viewmodel.js

Різницю між файлами не показано, бо вона завелика
+ 4 - 1680
dist/seed.js


Різницю між файлами не показано, бо вона завелика
+ 0 - 0
dist/seed.min.js


+ 26 - 0
examples/new-api-test.html

@@ -0,0 +1,26 @@
+<!DOCTYPE html>
+<html lang="en">
+    <head>
+        <title></title>
+        <meta charset="utf-8">
+        <script src="dist/seed.js"></script>
+    </head>
+    <body>
+        <div sd-template="todo" sd-text="hi" sd-on="click:hello"></div>
+        <script>
+            var Test = Seed.ViewModel.extend({
+                template: 'todo',
+                initialize: function (msg) {
+                    this.hi = 'Aloha'
+                },
+                properties: {
+                    hello: function () {
+                        console.log('Aloha')
+                    }
+                }
+            })
+            var app = new Test()
+            document.body.appendChild(app.$el)
+        </script>
+    </body>
+</html>

+ 78 - 77
examples/todomvc/js/app.js

@@ -8,11 +8,9 @@ window.addEventListener('hashchange', function () {
     Seed.broadcast('filterchange')
 })
 
-Seed.controller('todos', {
+var Todos = Seed.ViewModel.extend({
 
-    // initializer, reserved
-    init: function () {
-        window.app = this
+    initialize: function () {
         // listen for hashtag change
         this.updateFilter()
         this.$on('filterchange', this.updateFilter.bind(this))
@@ -21,88 +19,91 @@ Seed.controller('todos', {
         this.remaining = this.todos.filter(filters.active).length
     },
 
-    // computed properties ----------------------------------------------------
-    total: {get: function () {
-        return this.todos.length
-    }},
-
-    completed: {get: function () {
-        return this.total - this.remaining
-    }},
-
-    // dynamic context computed property using info from target viewmodel
-    todoFiltered: {get: function (ctx) {
-        return filters[this.filter]({ completed: ctx.vm.completed })
-    }},
-
-    // dynamic context computed property using info from target element
-    filterSelected: {get: function (ctx) {
-        return this.filter === ctx.el.textContent.toLowerCase()
-    }},
-
-    // two-way computed property with both getter and setter
-    allDone: {
-        get: function () {
-            return this.remaining === 0
+    properties: {
+
+        updateFilter: function () {
+            var filter = location.hash.slice(2)
+            this.filter = (filter in filters) ? filter : 'all'
         },
-        set: function (value) {
-            this.todos.forEach(function (todo) {
-                todo.completed = value
-            })
-            this.remaining = value ? 0 : this.total
-            todoStorage.save(this.todos)
-        }
-    },
 
-    // event handlers ---------------------------------------------------------
-    addTodo: function () {
-        var value = this.newTodo && this.newTodo.trim()
-        if (value) {
-            this.todos.unshift({ title: value, completed: false })
-            this.newTodo = ''
-            this.remaining++
-            todoStorage.save(this.todos)
-        }
-    },
+        // computed properties ----------------------------------------------------
+        total: {get: function () {
+            return this.todos.length
+        }},
+
+        completed: {get: function () {
+            return this.total - this.remaining
+        }},
+
+        // dynamic context computed property using info from target viewmodel
+        todoFiltered: {get: function (ctx) {
+            return filters[this.filter]({ completed: ctx.vm.completed })
+        }},
+
+        // dynamic context computed property using info from target element
+        filterSelected: {get: function (ctx) {
+            return this.filter === ctx.el.textContent.toLowerCase()
+        }},
+
+        // two-way computed property with both getter and setter
+        allDone: {
+            get: function () {
+                return this.remaining === 0
+            },
+            set: function (value) {
+                this.todos.forEach(function (todo) {
+                    todo.completed = value
+                })
+                this.remaining = value ? 0 : this.total
+                todoStorage.save(this.todos)
+            }
+        },
 
-    removeTodo: function (e) {
-        this.todos.remove(e.vm)
-        this.remaining -= e.vm.completed ? 0 : 1
-        todoStorage.save(this.todos)
-    },
+        // event handlers ---------------------------------------------------------
+        addTodo: function () {
+            var value = this.newTodo && this.newTodo.trim()
+            if (value) {
+                this.todos.unshift({ title: value, completed: false })
+                this.newTodo = ''
+                this.remaining++
+                todoStorage.save(this.todos)
+            }
+        },
 
-    toggleTodo: function (e) {
-        this.remaining += e.vm.completed ? -1 : 1
-        todoStorage.save(this.todos)
-    },
+        removeTodo: function (e) {
+            this.todos.remove(e.vm)
+            this.remaining -= e.vm.completed ? 0 : 1
+            todoStorage.save(this.todos)
+        },
 
-    editTodo: function (e) {
-        this.beforeEditCache = e.vm.title
-        e.vm.editing = true
-    },
+        toggleTodo: function (e) {
+            this.remaining += e.vm.completed ? -1 : 1
+            todoStorage.save(this.todos)
+        },
 
-    doneEdit: function (e) {
-        if (!e.vm.editing) return
-        e.vm.editing = false
-        e.vm.title = e.vm.title.trim()
-        if (!e.vm.title) this.removeTodo(e)
-        todoStorage.save(this.todos)
-    },
+        editTodo: function (e) {
+            this.beforeEditCache = e.vm.title
+            e.vm.editing = true
+        },
 
-    cancelEdit: function (e) {
-        e.vm.editing = false
-        e.vm.title = this.beforeEditCache
-    },
+        doneEdit: function (e) {
+            if (!e.vm.editing) return
+            e.vm.editing = false
+            e.vm.title = e.vm.title.trim()
+            if (!e.vm.title) this.removeTodo(e)
+            todoStorage.save(this.todos)
+        },
 
-    removeCompleted: function () {
-        this.todos = this.todos.filter(filters.active)
-        todoStorage.save(this.todos)
-    },
+        cancelEdit: function (e) {
+            e.vm.editing = false
+            e.vm.title = this.beforeEditCache
+        },
 
-    updateFilter: function () {
-        var filter = location.hash.slice(2)
-        this.filter = (filter in filters) ? filter : 'all'
+        removeCompleted: function () {
+            this.todos = this.todos.filter(filters.active)
+            todoStorage.save(this.todos)
+        }
     }
 })
 
-Seed.bootstrap()
+var app = new Todos({ el: '#todoapp' })

+ 31 - 59
src/compiler.js

@@ -1,9 +1,9 @@
 var config          = require('./config'),
-    ViewModel       = require('./viewmodel'),
+    utils           = require('./utils'),
     Binding         = require('./binding'),
     DirectiveParser = require('./directive-parser'),
     TextParser      = require('./text-parser'),
-    depsParser      = require('./deps-parser'),
+    DepsParser      = require('./deps-parser'),
     eventbus        = require('./utils').eventbus
 
 var slice           = Array.prototype.slice,
@@ -14,15 +14,19 @@ var slice           = Array.prototype.slice,
  *  The DOM compiler
  *  scans a DOM node and compile bindings for a ViewModel
  */
-function Compiler (el, options) {
+function Compiler (vm, options) {
 
-    config.log('\ncreated new Compiler instance.\n')
-    if (typeof el === 'string') {
-        el = document.querySelector(el)
+    utils.log('\ncreated new Compiler instance.\n')
+
+    // copy options
+    options = options || {}
+    for (var op in options) {
+        this[op] = options[op]
     }
 
-    this.el              = el
-    el.compiler          = this
+    this.vm              = vm
+    vm.$compiler         = this
+    this.el              = vm.$el
     this.bindings        = {}
     this.directives      = []
     this.watchers        = {}
@@ -32,68 +36,37 @@ function Compiler (el, options) {
     // list of bindings that has dynamic context dependencies
     this.contextBindings = []
 
-    // 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 viewmodel of a Compiler instance,
-    // make a copy from it
-    if (data instanceof ViewModel) {
-        data = data.$dump()
-    }
-
-    // check if there is a controller associated with this compiler
-    var ctrlID = el.getAttribute(ctrlAttr), controller
-    if (ctrlID) {
-        el.removeAttribute(ctrlAttr)
-        controller = config.controllers[ctrlID]
-        if (controller) {
-            this.controller = controller
-        } else {
-            config.warn('controller "' + ctrlID + '" is not defined.')
+    // copy data if any
+    var data = options.data
+    if (data) {
+        if (data instanceof vm.constructor) {
+            data = utils.dump(data)
+        }
+        for (var key in data) {
+            vm[key] = data[key]
         }
-    }
-    
-    // create the viewmodel object
-    // if the controller has an extended viewmodel contructor, use it;
-    // otherwise, use the original viewmodel constructor.
-    var VMCtor = (controller && controller.ExtendedVM) || ViewModel,
-        viewmodel = this.vm = new VMCtor(this, options)
-
-    // copy data
-    for (var key in data) {
-        viewmodel[key] = data[key]
     }
 
-    // apply controller initialize function
-    if (controller && controller.init) {
-        controller.init.call(viewmodel)
+    // call user init
+    if (options.initialize) {
+        options.initialize.apply(vm, options.args || [])
     }
 
     // now parse the DOM
-    this.compileNode(el, true)
+    this.compileNode(this.el, true)
 
     // for anything in viewmodel but not binded in DOM, create bindings for them
-    for (key in viewmodel) {
-        if (key.charAt(0) !== '$' && !this.bindings[key]) {
+    for (var key in vm) {
+        if (vm.hasOwnProperty(key) &&
+            key.charAt(0) !== '$' &&
+            !this.bindings[key])
+        {
             this.createBinding(key)
         }
     }
 
     // extract dependencies for computed properties
-    if (this.computed.length) depsParser.parse(this.computed)
+    if (this.computed.length) DepsParser.parse(this.computed)
     this.computed = null
     
     // extract dependencies for computed properties with dynamic context
@@ -198,7 +171,7 @@ CompilerProto.compileTextNode = function (node) {
  *  Create binding and attach getter/setter for a key to the viewmodel object
  */
 CompilerProto.createBinding = function (key) {
-    config.log('  created binding: ' + key)
+    utils.log('  created binding: ' + key)
     var binding = new Binding(this, key)
     this.bindings[key] = binding
     if (binding.isComputed) this.computed.push(binding)
@@ -304,7 +277,6 @@ CompilerProto.destroy = function () {
         this.bindings[key].unbind()
     }
     // remove el
-    this.el.compiler = null
     this.el.parentNode.removeChild(this.el)
 }
 

+ 0 - 10
src/config.js

@@ -2,19 +2,9 @@ module.exports = {
 
     prefix      : 'sd',
     debug       : false,
-    datum       : {},
-    controllers : {},
 
     interpolateTags : {
         open  : '{{',
         close : '}}'
-    },
-
-    log: function (msg) {
-        if (this.debug) console.log(msg)
-    },
-    
-    warn: function(msg) {
-        if (this.debug) console.warn(msg)
     }
 }

+ 6 - 5
src/deps-parser.js

@@ -1,5 +1,6 @@
 var Emitter  = require('emitter'),
     config   = require('./config'),
+    utils    = require('./utils'),
     observer = new Emitter()
 
 var dummyEl = document.createElement('div'),
@@ -33,11 +34,11 @@ function catchDeps (binding) {
  */
 function filterDeps (binding) {
     var i = binding.deps.length, dep
-    config.log('\n─ ' + binding.key)
+    utils.log('\n─ ' + binding.key)
     while (i--) {
         dep = binding.deps[i]
         if (!dep.deps.length) {
-            config.log('  └─ ' + dep.key)
+            utils.log('  └─ ' + dep.key)
             dep.subs.push(binding)
         } else {
             binding.deps.splice(i, 1)
@@ -47,7 +48,7 @@ function filterDeps (binding) {
     if (!ctdeps || !config.debug) return
     i = ctdeps.length
     while (i--) {
-        config.log('  └─ ctx:' + ctdeps[i])
+        utils.log('  └─ ctx:' + ctdeps[i])
     }
 }
 
@@ -115,11 +116,11 @@ module.exports = {
      *  parse a list of computed property bindings
      */
     parse: function (bindings) {
-        config.log('\nparsing dependencies...')
+        utils.log('\nparsing dependencies...')
         observer.isObserving = true
         bindings.forEach(catchDeps)
         bindings.forEach(filterDeps)
         observer.isObserving = false
-        config.log('\ndone.')
+        utils.log('\ndone.')
     }
 }

+ 3 - 2
src/directive-parser.js

@@ -1,4 +1,5 @@
 var config     = require('./config'),
+    utils      = require('./utils'),
     directives = require('./directives'),
     filters    = require('./filters')
 
@@ -188,8 +189,8 @@ module.exports = {
         var dir   = directives[dirname],
             valid = KEY_RE.test(expression)
 
-        if (!dir) config.warn('unknown directive: ' + dirname)
-        if (!valid) config.warn('invalid directive expression: ' + expression)
+        if (!dir) utils.warn('unknown directive: ' + dirname)
+        if (!valid) utils.warn('invalid directive expression: ' + expression)
 
         return dir && valid
             ? new Directive(dirname, expression, oneway)

+ 23 - 60
src/main.js

@@ -1,5 +1,4 @@
 var config      = require('./config'),
-    Compiler    = require('./compiler'),
     ViewModel   = require('./viewmodel'),
     directives  = require('./directives'),
     filters     = require('./filters'),
@@ -7,11 +6,7 @@ var config      = require('./config'),
     utils       = require('./utils')
 
 var eventbus    = utils.eventbus,
-    controllers = config.controllers,
-    datum       = config.datum,
-    api         = {},
-    reserved    = ['datum', 'controllers'],
-    booted      = false
+    api         = {}
 
 /*
  *  expose utils
@@ -25,38 +20,6 @@ api.broadcast = function () {
     eventbus.emit.apply(eventbus, arguments)
 }
 
-/*
- *  Store a piece of plain data in config.datum
- *  so it can be consumed by sd-data
- */
-api.data = function (id, data) {
-    if (!data) return datum[id]
-    datum[id] = data
-}
-
-/*
- *  Store a controller function in config.controllers
- *  so it can be consumed by sd-controller
- */
-api.controller = function (id, properties) {
-    if (!properties) return controllers[id]
-    // create a subclass of ViewModel that has the extension methods mixed-in
-    var ExtendedVM = function () {
-        ViewModel.apply(this, arguments)
-    }
-    var p = ExtendedVM.prototype = Object.create(ViewModel.prototype)
-    p.constructor = ExtendedVM
-    for (var prop in properties) {
-        if (prop !== 'init') {
-            p[prop] = properties[prop]
-        }
-    }
-    controllers[id] = {
-        init: properties.init,
-        ExtendedVM: ExtendedVM
-    }
-}
-
 /*
  *  Allows user to create a custom directive
  */
@@ -79,37 +42,37 @@ api.filter = function (name, fn) {
 api.config = function (opts) {
     if (opts) {
         for (var key in opts) {
-            if (reserved.indexOf(key) === -1) {
-                config[key] = opts[key]
-            }
+            config[key] = opts[key]
         }
     }
     textParser.buildRegex()
 }
 
 /*
- *  Compile a single element
+ *  Expose the main ViewModel class
+ *  and add extend method
  */
-api.compile = function (el) {
-    return new Compiler(el).vm
-}
+api.ViewModel = ViewModel
 
-/*
- *  Bootstrap the whole thing
- *  by creating a Compiler instance for top level nodes
- *  that has either sd-controller or sd-data
- */
-api.bootstrap = function (opts) {
-    if (booted) return
-    api.config(opts)
-    var el,
-        ctrlSlt = '[' + config.prefix + '-controller]',
-        dataSlt = '[' + config.prefix + '-data]'
-    /* jshint boss: true */
-    while (el = document.querySelector(ctrlSlt) || document.querySelector(dataSlt)) {
-        new Compiler(el)
+ViewModel.extend = function (options) {
+    var ExtendedVM = function (opts) {
+        opts = opts || {}
+        if (options.template) {
+            opts.template = utils.getTemplate(options.template)
+        }
+        if (options.initialize) {
+            opts.initialize = options.initialize
+        }
+        ViewModel.call(this, opts)
+    }
+    var p = ExtendedVM.prototype = Object.create(ViewModel.prototype)
+    p.constructor = ExtendedVM
+    if (options.properties) {
+        for (var prop in options.properties) {
+            p[prop] = options.properties[prop]
+        }
     }
-    booted = true
+    return ExtendedVM
 }
 
 module.exports = api

+ 23 - 1
src/utils.js

@@ -1,8 +1,12 @@
-var Emitter       = require('emitter'),
+var config        = require('./config'),
+    Emitter       = require('emitter'),
     toString      = Object.prototype.toString,
     aproto        = Array.prototype,
     arrayMutators = ['push','pop','shift','unshift','splice','sort','reverse']
 
+// hold templates
+var templates = {}
+
 var arrayAugmentations = {
     remove: function (index) {
         if (typeof index !== 'number') index = index.$index
@@ -100,5 +104,23 @@ module.exports = {
         for (method in arrayAugmentations) {
             collection[method] = arrayAugmentations[method]
         }
+    },
+
+    log: function (msg) {
+        if (config.debug) console.log(msg)
+    },
+    
+    warn: function(msg) {
+        if (config.debug) console.warn(msg)
+    },
+
+    getTemplate: function (id) {
+        var el = templates[id]
+        if (!el && el !== null) {
+            var selector = '[' + config.prefix + '-template="' + id + '"]'
+            el = templates[id] = document.querySelector(selector)
+            if (el) el.parentNode.removeChild(el)
+        }
+        return el
     }
 }

+ 19 - 9
src/viewmodel.js

@@ -1,15 +1,26 @@
-var utils   = require('./utils')
+var utils    = require('./utils'),
+    Compiler = require('./compiler')
 
 /*
  *  ViewModel exposed to the user that holds data,
  *  computed properties, event handlers
  *  and a few reserved methods
  */
-function ViewModel (compiler, options) {
-    this.$compiler = compiler
-    this.$el       = compiler.el
-    this.$index    = options.index
-    this.$parent   = options.parentCompiler && options.parentCompiler.vm
+function ViewModel (options) {
+
+    // determine el
+    this.$el = options.template
+        ? options.template.cloneNode(true)
+        : typeof options.el === 'string'
+            ? document.querySelector(options.el)
+            : options.el
+
+    // possible info inherited as an each item
+    this.$index  = options.index
+    this.$parent = options.parentCompiler && options.parentCompiler.vm
+
+    // compile
+    new Compiler(this, options)
 }
 
 var VMProto = ViewModel.prototype
@@ -49,12 +60,11 @@ VMProto.$watch = function (key, callback) {
     var self = this
     // yield and wait for compiler to finish compiling
     setTimeout(function () {
-        var viewmodel   = self.$compiler.vm,
-            binding = self.$compiler.bindings[key],
+        var binding = self.$compiler.bindings[key],
             i       = binding.deps.length,
             watcher = self.$compiler.watchers[key] = {
                 refresh: function () {
-                    callback(viewmodel[key])
+                    callback(self[key])
                 },
                 deps: binding.deps
             }

Деякі файли не було показано, через те що забагато файлів було змінено