component refactor

This commit is contained in:
Evan You 2013-12-18 00:45:55 -05:00
parent e04553a0a4
commit 628c42cc9f
17 changed files with 112 additions and 258 deletions

View File

@ -24,7 +24,8 @@
"src/directives/if.js",
"src/directives/repeat.js",
"src/directives/on.js",
"src/directives/model.js"
"src/directives/model.js",
"src/directives/component.js"
],
"dependencies": {
"component/emitter": "*"

View File

@ -71,8 +71,9 @@ function Compiler (vm, options) {
// set parent VM
// and register child id on parent
var childId = utils.attr(el, 'id')
var childId = utils.attr(el, 'component-id')
if (parent) {
parent.childCompilers.push(compiler)
def(vm, '$parent', parent.vm)
if (childId) {
compiler.childId = childId
@ -217,18 +218,20 @@ CompilerProto.setupObserver = function () {
*/
CompilerProto.compile = function (node, root) {
var compiler = this
var compiler = this,
nodeType = node.nodeType,
tagName = node.tagName
if (node.nodeType === 1) { // a normal node
if (nodeType === 1 && tagName !== 'SCRIPT') { // a normal node
// skip anything with v-pre
if (utils.attr(node, 'pre') !== null) return
// special attributes to check
var repeatExp,
componentId,
componentExp,
partialId,
customElementFn = compiler.getOption('elements', node.tagName.toLowerCase())
directive
// It is important that we access these attributes
// procedurally because the order matters.
@ -242,21 +245,25 @@ CompilerProto.compile = function (node, root) {
if (repeatExp = utils.attr(node, 'repeat')) {
// repeat block cannot have v-id at the same time.
var directive = Directive.parse(config.attrs.repeat, repeatExp, compiler, node)
directive = Directive.parse(config.attrs.repeat, repeatExp, compiler, node)
if (directive) {
compiler.bindDirective(directive)
}
// custom elements has 2nd highest priority
} else if (!root && customElementFn) {
// v-component has 2nd highest priority
} else if (!root && (componentExp = utils.attr(node, 'component'))) {
addChild(customElementFn)
// v-component has 3rd highest priority
} else if (!root && (componentId = utils.attr(node, 'component'))) {
var ChildVM = compiler.getOption('components', componentId)
if (ChildVM) addChild(ChildVM)
directive = Directive.parse(config.attrs.component, componentExp, compiler, node)
if (directive) {
// component directive is a bit different from the others.
// when it has no argument, it should be treated as a
// simple directive with its key as the argument.
if (componentExp.indexOf(':') === -1) {
directive.isSimple = true
directive.arg = directive.key
}
compiler.bindDirective(directive)
}
} else {
@ -277,27 +284,12 @@ CompilerProto.compile = function (node, root) {
compiler.compileNode(node)
}
} else if (node.nodeType === 3) { // text node
} else if (nodeType === 3) { // text node
compiler.compileTextNode(node)
}
function addChild (Ctor) {
if (utils.isConstructor(Ctor)) {
var child = new Ctor({
el: node,
child: true,
compilerOptions: {
parentCompiler: compiler
}
})
compiler.childCompilers.push(child.$compiler)
} else {
// simply call the function
Ctor(node)
}
}
}
/**
@ -412,13 +404,13 @@ CompilerProto.bindDirective = function (directive) {
binding.instances.push(directive)
directive.binding = binding
var value = binding.value
// invoke bind hook if exists
if (directive.bind) {
directive.bind(value)
directive.bind()
}
// set initial value
var value = binding.value
if (value !== undefined) {
if (binding.isComputed) {
directive.refresh(value)
@ -454,11 +446,11 @@ CompilerProto.createBinding = function (key, isExp, isFn) {
bindings[key] = binding
// make sure the key exists in the object so it can be observed
// by the Observer!
Observer.ensurePath(compiler.vm, key)
if (binding.root) {
// this is a root level binding. we need to define getter/setters for it.
compiler.define(key, binding)
} else {
Observer.ensurePath(compiler.vm, key)
var parentKey = key.slice(0, key.lastIndexOf('.'))
if (!hasOwn.call(bindings, parentKey)) {
// this is a nested value binding, but the binding for its parent
@ -488,9 +480,10 @@ CompilerProto.define = function (key, binding) {
// computed property
compiler.markComputed(binding)
} else if (type === 'Object' || type === 'Array') {
// observe objects later, becase there might be more keys
// to be added to it. we also want to emit all the set events
// after all values are available.
// observe objects later, because there might be more keys
// to be added to it during Observer.ensurePath().
// we also want to emit all the set events after all values
// are available.
compiler.observables.push(binding)
}

View File

@ -9,10 +9,10 @@ var Emitter = require('./emitter'),
function catchDeps (binding) {
if (binding.isFn) return
utils.log('\n─ ' + binding.key)
var depsHash = utils.hash()
var has = []
observer.on('get', function (dep) {
if (depsHash[dep.key]) return
depsHash[dep.key] = 1
if (has.indexOf(dep) > -1) return
has.push(dep)
utils.log(' └─ ' + dep.key)
binding.deps.push(dep)
dep.subs.push(binding)

View File

@ -0,0 +1,40 @@
var utils = require('../utils')
module.exports = {
bind: function () {
if (this.isSimple) {
this.build()
}
},
update: function (value) {
if (!this.component) {
this.build(value)
} else {
this.component.model = value
}
},
build: function (value) {
var Ctor = this.compiler.getOption('components', this.arg)
if (!Ctor) utils.warn('unknown component: ' + this.arg)
var options = {
el: this.el,
compilerOptions: {
parentCompiler: this.compiler
}
}
if (value) {
options.scope = {
model: value
}
}
this.component = new Ctor(options)
},
unbind: function () {
this.component.$destroy()
}
}

View File

@ -3,10 +3,11 @@ var utils = require('../utils'),
module.exports = {
on : require('./on'),
repeat : require('./repeat'),
model : require('./model'),
'if' : require('./if'),
on : require('./on'),
repeat : require('./repeat'),
model : require('./model'),
'if' : require('./if'),
component : require('./component'),
attr: function (value) {
this.el.setAttribute(this.arg, value)

View File

@ -42,15 +42,6 @@ ViewModel.component = function (id, Ctor) {
return this
}
/**
* Allows user to register/retrieve a Custom element constructor
*/
ViewModel.element = function (id, Ctor) {
if (!Ctor) return utils.elements[id]
utils.elements[id] = utils.toConstructor(Ctor)
return this
}
/**
* Allows user to register/retrieve a template partial
*/
@ -141,12 +132,12 @@ function inheritOptions (child, parent, topLevel) {
* that are used in compilation.
*/
var specialAttributes = [
'id',
'pre',
'text',
'repeat',
'partial',
'component',
'component-id',
'transition'
]

View File

@ -214,10 +214,12 @@ function ensurePath (obj, key) {
if (!obj[sec]) obj[sec] = {}
obj = obj[sec]
}
if (typeOf(obj) === 'Object') {
var type = typeOf(obj)
if (type === 'Object' || type === 'Array') {
sec = path[i]
if (!(sec in obj)) obj[sec] = undefined
}
return obj[sec]
}
module.exports = {

View File

@ -22,7 +22,6 @@ var utils = module.exports = {
components : makeHash(),
partials : makeHash(),
transitions : makeHash(),
elements : makeHash(),
/**
* get an attribute and remove it.
@ -141,11 +140,6 @@ var utils = module.exports = {
: null
},
isConstructor: function (obj) {
ViewModel = ViewModel || require('./viewmodel')
return obj.prototype instanceof ViewModel || obj === ViewModel
},
/**
* convert certain option values to the desired format.
*/
@ -153,18 +147,12 @@ var utils = module.exports = {
var components = options.components,
partials = options.partials,
template = options.template,
elements = options.elements,
key
if (components) {
for (key in components) {
components[key] = utils.toConstructor(components[key])
}
}
if (elements) {
for (key in elements) {
elements[key] = utils.toConstructor(elements[key])
}
}
if (partials) {
for (key in partials) {
partials[key] = utils.toFragment(partials[key])
@ -185,11 +173,11 @@ var utils = module.exports = {
},
/**
* warnings, thrown in all cases
* warnings, traces by default
* can be suppressed by `silent` option.
*/
warn: function() {
if (!config.silent && console) {
console.trace()
console.warn(join.call(arguments, ' '))
}
}

View File

@ -1,40 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<title>Custom Elements</title>
<meta charset="utf-8">
<script src="../../../dist/vue.js"></script>
</head>
<body>
<my-element>afsefsefse</my-element>
<cool>hmm</cool>
<wow></wow>
<script>
// global custom element with option object + replace
Vue.element('my-element', {
replace: true,
className: 'test',
template: '<div>{{msg}}</div>'
})
new Vue({
el:'body',
scope: {
msg: 'hihi',
},
elements: {
// private custom element with simple function
cool: function (el) {
el.className = 'cool'
el.innerHTML = 'This is cool'
},
// private custom element with constructor
wow: Vue.extend({
ready: function () {
this.$el.textContent = 'this is wow'
}
})
}
})
</script>
</body>
</html>

View File

@ -11,15 +11,21 @@
<div class="filter">{{filterMsg | nodigits}}</div>
<div class="partial" v-partial="partial-test"></div>
<div class="vm" v-component="vm-test">{{vmMsg}}</div>
<div class="vm-w-model" v-component="vm-w-model:vmData">{{msg + model.msg}}</div>
</div>
<script>
var T = Vue.extend({
components: {
'vm-test': Vue.extend({
'vm-test': {
scope: {
vmMsg: 'component works'
}
})
},
'vm-w-model': {
scope : {
msg: 'component with model '
}
}
},
partials: {
'partial-test': '{{partialMsg}}'
@ -40,7 +46,10 @@
scope: {
dirMsg: 'directive',
filterMsg: 'fi43l132ter5 w12345orks',
partialMsg: 'partial works'
partialMsg: 'partial works',
vmData: {
msg: 'works'
}
}
})
</script>

View File

@ -24,7 +24,7 @@
var data = { c: 555 }
var Demo = Vue.extend({
lazy: true,
ready: function () {
created: function () {
this.msg = 'Yoyoyo'
this.a = data
},

View File

@ -11,7 +11,7 @@
<li v-repeat="item : items" v-class="'list-' + $index">
<ul>
<li v-repeat="subitem : item.items" v-class="'list-' + $index">
{{$parent.$index + '.' + $index + ' : ' + item.title + '<-' + subitem.title}}
{{$parent.$index + '.' + $index + ' : ' + item.title + '&lt;-' + subitem.title}}
</li>
</ul>
</li>
@ -24,6 +24,7 @@
<button id="b1-1" v-on="click: items[1].items[1].title = 'hi'">2.2</button>
</div>
<script>
Vue.config({debug:true})
var items = [
{ title: 0, items: [{title:0}, {title:1}] },
{ title: 1, items: [{title:0}, {title:1}] }

View File

@ -1,13 +0,0 @@
casper.test.begin('Custom Elements', 3, function (test) {
casper
.start('./fixtures/custom-element.html', function () {
test.assertSelectorHasText('div.test', 'hihi')
test.assertSelectorHasText('cool.cool', 'This is cool')
test.assertSelectorHasText('wow', 'this is wow')
})
.run(function () {
test.done()
})
})

View File

@ -1,4 +1,4 @@
casper.test.begin('Component Encapsulation', 4, function (test) {
casper.test.begin('Component Encapsulation', 5, function (test) {
casper
.start('./fixtures/encapsulation.html', function () {
@ -6,6 +6,7 @@ casper.test.begin('Component Encapsulation', 4, function (test) {
test.assertSelectorHasText('.filter', 'filter works')
test.assertSelectorHasText('.partial', 'partial works')
test.assertSelectorHasText('.vm', 'component works')
test.assertSelectorHasText('.vm-w-model', 'component with model works')
})
.run(function () {
test.done()

View File

@ -71,8 +71,8 @@ describe('UNIT: API', function () {
var testId = 'directive-2',
msg = 'wowaaaa?'
dirTest = {
bind: function (value) {
this.el.setAttribute(testId + 'bind', value + 'bind')
bind: function () {
this.el.setAttribute(testId + 'bind', 'bind')
},
update: function (value) {
this.el.setAttribute(testId + 'update', value + 'update')
@ -88,7 +88,7 @@ describe('UNIT: API', function () {
scope: { test: msg }
}),
el = document.querySelector('#' + testId + ' span')
assert.strictEqual(el.getAttribute(testId + 'bind'), msg + 'bind', 'should have called bind()')
assert.strictEqual(el.getAttribute(testId + 'bind'), 'bind', 'should have called bind()')
assert.strictEqual(el.getAttribute(testId + 'update'), msg + 'update', 'should have called update()')
vm.$destroy() // assuming this works
assert.notOk(el.getAttribute(testId + 'bind'), 'should have called unbind()')
@ -143,64 +143,6 @@ describe('UNIT: API', function () {
})
describe('element()', function () {
var testId = 'api-element-test',
testId2 = testId + '2',
testId3 = testId + '3',
opts = {
className: 'hihi',
scope: { hi: 'ok' }
},
Test = Vue.extend(opts),
utils = require('vue/src/utils')
it('should register a Custom Element constructor', function () {
Vue.element(testId, Test)
assert.strictEqual(utils.elements[testId], Test)
})
it('should also work with option objects', function () {
Vue.element(testId2, opts)
assert.ok(utils.elements[testId2].prototype instanceof Vue)
})
it('should accept simple function as-is', function () {
var fn = function (el) {
el.className = 'hihi3'
el.textContent = 'ok3'
}
Vue.element(testId3, fn)
assert.strictEqual(utils.elements[testId3], fn)
})
it('should retrieve the VM if has only one arg', function () {
assert.strictEqual(Vue.element(testId), Test)
})
it('should work with custom tag names', function () {
mock(testId, '<' + testId + '>{{hi}}</' + testId + '>')
var t = new Vue({ el: '#' + testId }),
child = t.$el.querySelector(testId)
assert.strictEqual(child.className, 'hihi')
assert.strictEqual(child.textContent, 'ok')
mock(testId2, '<' + testId2 + '>{{hi}}</' + testId2 + '>')
var t2 = new Vue({ el: '#' + testId2 }),
child2 = t2.$el.querySelector(testId2)
assert.strictEqual(child2.className, 'hihi')
assert.strictEqual(child2.textContent, 'ok')
mock(testId3, '<' + testId3 + '></' + testId3 + '>')
var t3 = new Vue({ el: '#' + testId3 }),
child3 = t3.$el.querySelector(testId3)
assert.strictEqual(child3.className, 'hihi3')
assert.strictEqual(child3.textContent, 'ok3')
})
})
describe('partial()', function () {
var testId = 'api-partial-test',
@ -633,66 +575,6 @@ describe('UNIT: API', function () {
})
})
describe('elements', function () {
it('should allow the VM to use private custom elements', function () {
var Child = Vue.extend({
scope: {
name: 'child'
}
})
var Parent = Vue.extend({
template: '<p>{{name}}</p><child>{{name}}</child>',
scope: {
name: 'dad'
},
elements: {
child: Child
}
})
var p = new Parent()
assert.strictEqual(p.$el.querySelector('p').textContent, 'dad')
assert.strictEqual(p.$el.querySelector('child').textContent, 'child')
})
it('should work with plain option object', function () {
var Parent = Vue.extend({
template: '<p>{{name}}</p><child>{{name}}</child>',
scope: {
name: 'dad'
},
elements: {
child: {
scope: {
name: 'child'
}
}
}
})
var p = new Parent()
assert.strictEqual(p.$el.querySelector('p').textContent, 'dad')
assert.strictEqual(p.$el.querySelector('child').textContent, 'child')
})
it('should work with a simple function', function () {
var Parent = Vue.extend({
template: '<p>{{name}}</p><child></child>',
scope: {
name: 'dad'
},
elements: {
child: function (el) {
el.textContent = 'child'
}
}
})
var p = new Parent()
assert.strictEqual(p.$el.querySelector('p').textContent, 'dad')
assert.strictEqual(p.$el.querySelector('child').textContent, 'child')
})
})
describe('partials', function () {

View File

@ -569,7 +569,11 @@ describe('UNIT: Directives', function () {
})
describe('id', function () {
describe('component', function () {
// body...
})
describe('component-id', function () {
it('should register a VM isntance on its parent\'s $', function () {
var called = false
@ -581,7 +585,7 @@ describe('UNIT: Directives', function () {
}
})
var t = new Vue({
template: '<div v-component="child" v-id="hihi"></div>',
template: '<div v-component="child" v-component-id="hihi"></div>',
components: {
child: Child
}

View File

@ -209,9 +209,6 @@ describe('UNIT: Utils', function () {
a: { scope: { data: 1 } },
b: { scope: { data: 2 } }
},
elements: {
c: { scope: { data: 3 }}
},
template: '<a>{{hi}}</a>'
}
@ -240,9 +237,6 @@ describe('UNIT: Utils', function () {
assert.strictEqual(components.a.options.scope.data, 1)
assert.ok(components.b.prototype instanceof Vue)
assert.strictEqual(components.b.options.scope.data, 2)
var elements = options.elements
assert.ok(elements.c.prototype instanceof Vue)
assert.strictEqual(elements.c.options.scope.data, 3)
})
})