Evan You 12 anni fa
parent
commit
d2779aad82
9 ha cambiato i file con 244 aggiunte e 170 eliminazioni
  1. 32 31
      examples/nested-viewmodels.html
  2. 0 0
      examples/repeated-items.html
  3. 21 12
      examples/template.html
  4. 137 95
      src/compiler.js
  5. 14 8
      src/directives/each.js
  6. 20 3
      src/main.js
  7. 1 1
      src/observer.js
  8. 18 7
      src/utils.js
  9. 1 13
      src/viewmodel.js

+ 32 - 31
examples/nested-viewmodels.html

@@ -6,68 +6,69 @@
         div:not(#grandpa) {
             padding-left: 15px;
         }
+        p {
+            position: relative;
+        }
         p:not(.ancestor):before {
+            position: absolute;
+            top: 0;
+            left: -22px;
             content: "└ ";
+            color: #F00;
         }
     </style>
-    <script src="../dist/seed.js"></script>
 </head>
 <body>
-    <div id="grandpa" data-name="Andy">
-        <p class="ancestor" sd-text="name"></p>
+    <div id="grandpa" sd-viewmodel="man" data-name="Andy" data-family="Johnson">
+        <p class="ancestor">{{name}} {{family}}</p>
 
         <div sd-viewmodel="man" data-name="Jack">
-            <p><span sd-text="name"></span>, son of <span sd-text="^name"></span></p>
+            <p>{{name}}, son of {{^name}}</p>
 
             <div sd-viewmodel="man" data-name="Mike">
-                <p><span sd-text="name"></span>, son of <span sd-text="^name"></span></p>
+                <p>{{name}}, son of {{^name}}</p>
 
-                <div sd-viewmodel="man" data-name="Tim">
-                    <p>
-                        <span sd-text="name"></span>,
-                        son of <span sd-text="^name"></span>,
-                        grandson of <span sd-text="^^name"></span>
-                        and great-grandson of <span sd-text="$name"></span>
-                    </p>
+                <div sd-viewmodel="man" data-name="Tim" sd-template="offspring">
                 </div>
 
-                <div sd-viewmodel="man" data-name="Tom">
-                    <p>
-                        <span sd-text="name"></span>,
-                        son of <span sd-text="^name"></span>,
-                        grandson of <span sd-text="^^name"></span>
-                        and great-grandson of <span sd-text="$name"></span>
-                    </p>
+                <div sd-viewmodel="man" data-name="Tom" sd-template="offspring">
                 </div>
             </div>
 
             <div sd-viewmodel="man" data-name="Jason">
-                <p><span sd-text="name"></span>, son of <span sd-text="^name"></span></p>
+                <p>{{name}}, son of {{^name}}</p>
 
-                <div sd-viewmodel="man" data-name="Andrew">
-                    <p>
-                        <span sd-text="name"></span>,
-                        son of <span sd-text="^name"></span>,
-                        grandson of <span sd-text="^^name"></span>
-                        and great-grandson of <span sd-text="$name"></span>
-                    </p>
+                <div sd-viewmodel="man" data-name="Andrew" sd-template="offspring">
                 </div>
             </div>
         </div>
     </div>
 
+    <script type="text/sd-template" sd-template-id="offspring">
+        <p>
+            {{name}}, son of {{^name}},
+            <br>
+            grandson of {{^^name}},
+            <br>
+            great-grandson of {{$name}},
+            <br>
+            and offspring of family {{family}}.
+        </p>
+    </script>
+    <script src="../dist/seed.js"></script>
     <script>
         seed.config({ debug: true })
-
         var Man = seed.ViewModel.extend({
             id: 'man',
             init: function () {
                 this.name = this.$el.dataset.name
+                var family = this.$el.dataset.family
+                if (family) {
+                    this.family = family
+                }
             }
         })
-
-        var demo = new Man({ el: '#grandpa' })
-
+        seed.bootstrap('#grandpa')
     </script>
 </body>
 </html>

+ 0 - 0
examples/repeated-items.html


+ 21 - 12
examples/template.html

@@ -3,25 +3,34 @@
     <head>
         <title></title>
         <meta charset="utf-8">
-        <script src="../dist/seed.js"></script>
     </head>
     <body>
-        <div sd-template="todo">
+
+        <div id="hawaii" sd-template="test"></div>
+
+        <script type="text/sd-template" sd-template-id="test">
             <p>{{hi}}!</p>
-        </div>
+        </script>
+
+        <script src="../dist/seed.js"></script>
         <script>
-            var Test = seed.ViewModel.extend({
-                template: 'todo',
-                init: function (msg) {
-                    this.hi = msg
-                }
+            // compile an existing node with sd-template directive
+            // will replace its innerHTML with the template's content
+            var hawaii = new seed.ViewModel({
+                el: '#hawaii'
             })
 
-            var hawaii = new Test({ args: ['Aloha'] }),
-                china  = new Test({ args: ['你好'] })
-
-            document.body.appendChild(hawaii.$el)
+            // if using template without an existing node
+            // the VM's $el will be a clone of the template's content
+            // and you will have to manually append it into DOM.
+            // this is mostly to allow users to create VMs dynamically.
+            var china  = new seed.ViewModel({
+                template: 'test'
+            })
             document.body.appendChild(china.$el)
+
+            hawaii.hi = 'Aloha'
+            china.hi = '你好'
         </script>
     </body>
 </html>

+ 137 - 95
src/compiler.js

@@ -18,8 +18,6 @@ var vmAttr, eachAttr
  */
 function Compiler (vm, options) {
 
-    utils.log('\nnew Compiler instance: ', vm.$el, '\n')
-
     // need to refresh this everytime we compile
     eachAttr = config.prefix + '-each'
     vmAttr   = config.prefix + '-viewmodel'
@@ -30,17 +28,52 @@ function Compiler (vm, options) {
         this[op] = options[op]
     }
 
+    // determine el
+    var tpl = options.template,
+        el  = options.el
+    el  = typeof el === 'string'
+        ? document.querySelector(el)
+        : el
+    if (el) {
+        var tplExp = tpl || el.getAttribute(config.prefix + '-template')
+        if (tplExp) {
+            el.innerHTML = utils.getTemplate(tplExp) || ''
+            el.removeAttribute(config.prefix + '-template')
+        }
+    } else if (tpl) {
+        var template = utils.getTemplate(tpl)
+        if (template) {
+            var tplHolder = document.createElement('div')
+            tplHolder.innerHTML = template
+            el = tplHolder.childNodes[0]
+        }
+    }
+
+    utils.log('\nnew VM instance: ', el, '\n')
+
+    // set el
+    vm.$el = el
+    // link it up!
+    vm.$compiler = this
+    // possible info inherited as an each item
+    vm.$index    = options.index
+    vm.$parent   = options.parentCompiler && options.parentCompiler.vm
+
+    // now for the compiler itself...
     this.vm              = vm
-    vm.$compiler         = this
     this.el              = vm.$el
-    this.bindings        = {}
-    this.observer        = new Emitter()
     this.directives      = []
-    this.watchers        = {}
-    // list of computed properties that need to parse dependencies for
-    this.computed        = []
-    // list of bindings that has dynamic context dependencies
-    this.contextBindings = []
+    this.computed        = [] // computed props to parse deps from
+    this.contextBindings = [] // computed props with dynamic context
+
+    // prototypal inheritance of bindings
+    var parent = this.parentCompiler
+    this.bindings = parent
+        ? Object.create(parent.bindings)
+        : {}
+    this.rootCompiler = parent
+        ? getRoot(parent)
+        : this
 
     // setup observer
     this.setupObserver()
@@ -77,16 +110,49 @@ function Compiler (vm, options) {
 
 var CompilerProto = Compiler.prototype
 
+/*
+ *  Setup observer.
+ *  The observer listens for get/set/mutate events on all VM
+ *  values/objects and trigger corresponding binding updates.
+ */
+CompilerProto.setupObserver = function () {
+
+    var compiler = this,
+        bindings = this.bindings,
+        observer = this.observer = new Emitter()
+
+    // a hash to hold event proxies for each root level key
+    // so they can be referenced and removed later
+    observer.proxies = {}
+
+    // add own listeners which trigger binding updates
+    observer
+        .on('get', function (key) {
+            if (DepsParser.observer.isObserving) {
+                DepsParser.observer.emit('get', bindings[key])
+            }
+        })
+        .on('set', function (key, val) {
+            if (!bindings[key]) compiler.createBinding(key)
+            bindings[key].update(val)
+        })
+        .on('mutate', function (key) {
+            bindings[key].refresh()
+        })
+}
+
 /*
  *  Actually parse the DOM nodes for directives, create bindings,
  *  and parse dependencies afterwards. For the dependency extraction to work,
  *  this has to happen after all user-set values are present in the VM.
  */
 CompilerProto.compile = function () {
+
     var key,
         vm = this.vm,
         computed = this.computed,
         contextBindings = this.contextBindings
+
     // parse the DOM
     this.compileNode(this.el, true)
 
@@ -107,39 +173,6 @@ CompilerProto.compile = function () {
     // extract dependencies for computed properties with dynamic context
     if (contextBindings.length) this.bindContexts(contextBindings)
     this.contextBindings = null
-    
-    utils.log('\ncompilation done.\n')
-}
-
-/*
- *  Setup observer.
- *  The observer listens for get/set/mutate events on all VM
- *  values/objects and trigger corresponding binding updates.
- */
-CompilerProto.setupObserver = function () {
-
-    var bindings = this.bindings,
-        observer = this.observer,
-        compiler = this
-
-    // a hash to hold event proxies for each root level key
-    // so they can be referenced and removed later
-    observer.proxies = {}
-
-    // add own listeners which trigger binding updates
-    observer
-        .on('get', function (key) {
-            if (DepsParser.observer.isObserving) {
-                DepsParser.observer.emit('get', bindings[key])
-            }
-        })
-        .on('set', function (key, val) {
-            if (!bindings[key]) compiler.createBinding(key)
-            bindings[key].update(val)
-        })
-        .on('mutate', function (key) {
-            bindings[key].refresh()
-        })
 }
 
 /*
@@ -169,6 +202,7 @@ CompilerProto.compileNode = function (node, root) {
 
         } else if (vmExp && !root) { // nested ViewModels
 
+            node.removeAttribute(vmAttr)
             var ChildVM = utils.getVM(vmExp)
             if (ChildVM) {
                 new ChildVM({
@@ -241,10 +275,63 @@ CompilerProto.compileTextNode = function (node) {
     node.parentNode.removeChild(node)
 }
 
+/*
+ *  Add a directive instance to the correct binding & viewmodel
+ */
+CompilerProto.bindDirective = function (directive) {
+
+    this.directives.push(directive)
+    directive.compiler = this
+    directive.vm       = this.vm
+
+    var key = directive.key,
+        compiler = traceOwnerCompiler(directive, this)
+
+    var binding
+    if (compiler.vm.hasOwnProperty(key)) {
+        // if the value is present in the target VM, we create the binding on its compiler
+        binding = compiler.bindings.hasOwnProperty(key)
+            ? compiler.bindings[key]
+            : compiler.createBinding(key)
+    } else {
+        // due to prototypal inheritance of bindings, if a key doesn't exist here,
+        // it doesn't exist in the whole prototype chain. Therefore in that case
+        // we create the new binding at the root level.
+        binding = compiler.bindings[key] || this.rootCompiler.createBinding(key)
+    }
+
+    binding.instances.push(directive)
+    directive.binding = binding
+
+    // for newly inserted sub-VMs (each items), need to bind deps
+    // because they didn't get processed when the parent compiler
+    // was binding dependencies.
+    var i, dep
+    if (binding.contextDeps) {
+        i = binding.contextDeps.length
+        while (i--) {
+            dep = this.bindings[binding.contextDeps[i]]
+            dep.subs.push(directive)
+        }
+    }
+
+    // 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 viewmodel object
  */
 CompilerProto.createBinding = function (key) {
+    
     utils.log('  created binding: ' + key)
 
     var bindings = this.bindings,
@@ -320,58 +407,6 @@ CompilerProto.define = function (key, binding) {
     })
 }
 
-/*
- *  Add a directive instance to the correct binding & viewmodel
- */
-CompilerProto.bindDirective = function (directive) {
-
-    this.directives.push(directive)
-    directive.compiler = this
-    directive.vm       = this.vm
-
-    var key = directive.key,
-        compiler = this
-
-    // deal with each block
-    if (this.each) {
-        if (key.indexOf(this.eachPrefix) === 0) {
-            key = directive.key = key.replace(this.eachPrefix, '')
-        } else {
-            compiler = this.parentCompiler
-        }
-    }
-
-    // deal with nesting
-    compiler = traceOwnerCompiler(directive, compiler)
-    var binding = compiler.bindings[key] || compiler.createBinding(key)
-
-    binding.instances.push(directive)
-    directive.binding = binding
-
-    // for newly inserted sub-VMs (each items), need to bind deps
-    // because they didn't get processed when the parent compiler
-    // was binding dependencies.
-    var i, dep
-    if (binding.contextDeps) {
-        i = binding.contextDeps.length
-        while (i--) {
-            dep = this.bindings[binding.contextDeps[i]]
-            dep.subs.push(directive)
-        }
-    }
-
-    // invoke bind hook if exists
-    if (directive.bind) {
-        directive.bind(binding.value)
-    }
-
-    // set initial value
-    directive.update(binding.value)
-    if (binding.isComputed) {
-        directive.refresh()
-    }
-}
-
 /*
  *  Process subscriptions for computed properties that has
  *  dynamic context dependencies
@@ -439,4 +474,11 @@ function traceOwnerCompiler (key, compiler) {
     return compiler
 }
 
+/*
+ *  shorthand for getting root compiler
+ */
+function getRoot (compiler) {
+    return traceOwnerCompiler({ root: true }, compiler)
+}
+
 module.exports = Compiler

+ 14 - 8
src/directives/each.js

@@ -81,6 +81,11 @@ module.exports = {
         this.ref = document.createComment('sd-each-' + this.arg)
         ctn.insertBefore(this.ref, this.el)
         ctn.removeChild(this.el)
+        this.collection = null
+        this.vms = null
+        this.mutationListener = (function (mutation) {
+            mutationHandlers[mutation.method].call(this, mutation)
+        }).bind(this)
     },
 
     update: function (collection) {
@@ -95,12 +100,11 @@ module.exports = {
             this.buildItem(this.ref, null, null)
         }
         this.collection = collection
+        this.vms = []
 
         // listen for collection mutation events
         // the collection has been augmented during Binding.set()
-        collection.__observer__.on('mutate', (function (mutation) {
-            mutationHandlers[mutation.method].call(this, mutation)
-        }).bind(this))
+        collection.__observer__.on('mutate', this.mutationListener)
 
         // create child-seeds and append to DOM
         for (var i = 0, l = collection.length; i < l; i++) {
@@ -120,11 +124,13 @@ module.exports = {
             eachPrefix: this.arg + '.',
             parentCompiler: this.compiler,
             index: index,
-            data: data,
-            delegator: this.container
+            delegator: this.container,
+            data: {
+                todo: data
+            }
         })
-        if (index !== null) {
-            this.collection[index] = item
+        if (index) {
+            this.vms[index] = item
         } else {
             item.$destroy()
         }
@@ -139,7 +145,7 @@ module.exports = {
 
     unbind: function () {
         if (this.collection) {
-            this.collection.off('mutate')
+            this.collection.off('mutate', this.mutationListener)
             var i = this.collection.length
             while (i--) {
                 this.collection[i].$destroy()

+ 20 - 3
src/main.js

@@ -48,6 +48,23 @@ api.config = function (opts) {
     textParser.buildRegex()
 }
 
+/*
+ *  Angular style bootstrap
+ */
+api.bootstrap = function (el) {
+    el = (typeof el === 'string'
+        ? document.querySelector(el)
+        : el) || document.body
+    var Ctor = ViewModel,
+        vmAttr = config.prefix + '-viewmodel',
+        vmExp = el.getAttribute(vmAttr)
+    if (vmExp) {
+        Ctor = utils.getVM(vmExp)
+        el.removeAttribute(vmAttr)
+    }
+    return new Ctor({ el: el })
+}
+
 /*
  *  Expose the main ViewModel class
  *  and add extend method
@@ -57,9 +74,6 @@ api.ViewModel = ViewModel
 ViewModel.extend = function (options) {
     var ExtendedVM = function (opts) {
         opts = opts || {}
-        if (options.template) {
-            opts.template = utils.getTemplate(options.template)
-        }
         if (options.init) {
             opts.init = options.init
         }
@@ -78,4 +92,7 @@ ViewModel.extend = function (options) {
     return ExtendedVM
 }
 
+// collect templates on load
+utils.collectTemplates()
+
 module.exports = api

+ 1 - 1
src/observer.js

@@ -120,7 +120,7 @@ module.exports = {
     },
 
     unobserve: function (obj, path, observer) {
-        if (!obj.__observer__) return
+        if (!obj || !obj.__observer__) return
         path = path + '.'
         var proxies = observer.proxies[path]
         obj.__observer__

+ 18 - 7
src/utils.js

@@ -14,14 +14,25 @@ module.exports = {
 
     typeOf: typeOf,
 
-    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)
+    collectTemplates: function () {
+        var selector = 'script[type="text/' + config.prefix + '-template"]',
+            templates = document.querySelectorAll(selector),
+            i = templates.length
+        while (i--) {
+            this.storeTemplate(templates[i])
+        }
+    },
+
+    storeTemplate: function (template) {
+        var id = template.getAttribute(config.prefix + '-template-id')
+        if (id) {
+            templates[id] = template.innerHTML.trim()
         }
-        return el
+        template.parentNode.removeChild(template)
+    },
+
+    getTemplate: function (id) {
+        return templates[id]
     },
 
     registerVM: function (id, VM) {

+ 1 - 13
src/viewmodel.js

@@ -6,19 +6,7 @@ var Compiler = require('./compiler')
  *  and a few reserved methods
  */
 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. options are passed directly to compiler
+    // just compile. options are passed directly to compiler
     new Compiler(this, options)
 }