From 5f19affa66b5e79f4aeaf657092a78be2a65cb3a Mon Sep 17 00:00:00 2001 From: Evan You Date: Tue, 8 Jul 2014 12:17:13 -0400 Subject: [PATCH 0001/1534] init --- .gitignore | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .gitignore diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000000..98a73020a19 --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +dist/vue.min.js.gz +test/vue.test.js +explorations +node_modules +.DS_Store \ No newline at end of file From b41cf2272088372958d1eeeb5bfb754a37a59fb7 Mon Sep 17 00:00:00 2001 From: Evan You Date: Tue, 8 Jul 2014 13:45:53 -0400 Subject: [PATCH 0002/1534] setup unit testing --- gruntfile.js | 29 +++++++++++++++++++++++++++++ package.json | 36 ++++++++++++++++++++++++++++++++++++ src/test.js | 1 + src/vue.js | 7 +++++++ test/unit/specs/main.js | 7 +++++++ 5 files changed, 80 insertions(+) create mode 100644 gruntfile.js create mode 100644 package.json create mode 100644 src/test.js create mode 100644 src/vue.js create mode 100644 test/unit/specs/main.js diff --git a/gruntfile.js b/gruntfile.js new file mode 100644 index 00000000000..d005408972f --- /dev/null +++ b/gruntfile.js @@ -0,0 +1,29 @@ +module.exports = function (grunt) { + + grunt.initConfig({ + karma: { + options: { + frameworks: ['jasmine', 'commonjs'], + preprocessors: { + 'src/*.js': ['commonjs'], + 'test/unit/specs/*': ['commonjs'] + }, + files: [ + 'src/*.js', + 'test/unit/specs/*.js' + ], + singleRun: true + }, + browsers: { + options: { + browsers: ['Chrome', 'Firefox'], + reporters: ['progress'] + } + } + } + }) + + grunt.loadNpmTasks('grunt-karma') + grunt.registerTask('unit', ['karma:browsers']) + +} \ No newline at end of file diff --git a/package.json b/package.json new file mode 100644 index 00000000000..fe7b50bba54 --- /dev/null +++ b/package.json @@ -0,0 +1,36 @@ +{ + "name": "vue", + "version": "0.10.5", + "author": "Evan You ", + "license": "MIT", + "description": "Simple, Fast & Composable MVVM for building interative interfaces", + "keywords": [ + "mvvm", + "browser", + "framework" + ], + "main": "src/vue.js", + "repository": { + "type": "git", + "url": "https://github.com/yyx990803/vue.git" + }, + "bugs": "https://github.com/yyx990803/vue/issues", + "homepage": "http://vuejs.org", + "scripts": { + "test": "grunt travis" + }, + "devDependencies": { + "browserify": "^4.2.0", + "grunt": "^0.4.5", + "grunt-browserify": "^2.1.3", + "grunt-karma": "^0.8.3", + "karma": "^0.12.16", + "karma-browserify": "^0.2.1", + "karma-chrome-launcher": "^0.1.4", + "karma-commonjs": "0.0.10", + "karma-firefox-launcher": "^0.1.3", + "karma-jasmine": "^0.1.5", + "karma-phantomjs-launcher": "^0.1.4", + "karma-safari-launcher": "^0.1.1" + } +} diff --git a/src/test.js b/src/test.js new file mode 100644 index 00000000000..3dfd42fa2cb --- /dev/null +++ b/src/test.js @@ -0,0 +1 @@ +module.exports = 123 \ No newline at end of file diff --git a/src/vue.js b/src/vue.js new file mode 100644 index 00000000000..3215401c8ff --- /dev/null +++ b/src/vue.js @@ -0,0 +1,7 @@ +var test = require('./test') + +module.exports = { + test: function () { + return test + } +} \ No newline at end of file diff --git a/test/unit/specs/main.js b/test/unit/specs/main.js new file mode 100644 index 00000000000..007c5d3e75f --- /dev/null +++ b/test/unit/specs/main.js @@ -0,0 +1,7 @@ +var vue = require('../../../src/vue.js') + +describe('test', function () { + it('should work', function () { + expect(vue.test()).toEqual(123) + }) +}) \ No newline at end of file From ecb125d33a013efb3f41855387293690891eac12 Mon Sep 17 00:00:00 2001 From: Evan You Date: Tue, 8 Jul 2014 15:57:47 -0400 Subject: [PATCH 0003/1534] scaffolding --- src/api/asset-register.js | 0 src/api/config.js | 0 src/api/extend.js | 0 src/api/require.js | 0 src/api/use.js | 0 src/batcher.js | 0 src/binding.js | 5 +++++ src/compiler/compiler.js | 5 +++++ src/config.js | 1 + src/directive.js | 5 +++++ src/emitter.js | 0 src/instance/data.js | 15 +++++++++++++++ src/instance/dom.js | 19 +++++++++++++++++++ src/instance/events.js | 13 +++++++++++++ src/instance/lifecycle.js | 7 +++++++ src/observer/observer.js | 0 src/observer/watch-array.js | 0 src/observer/watch-object.js | 0 src/parsers/directive.js | 0 src/parsers/expression.js | 0 src/parsers/path.js | 0 src/parsers/template.js | 0 src/parsers/text.js | 0 src/test.js | 1 - src/transition/css.js | 0 src/transition/js.js | 0 src/transition/transition.js | 0 src/util.js | 9 +++++++++ src/vue.js | 33 +++++++++++++++++++++++++++------ 29 files changed, 106 insertions(+), 7 deletions(-) create mode 100644 src/api/asset-register.js create mode 100644 src/api/config.js create mode 100644 src/api/extend.js create mode 100644 src/api/require.js create mode 100644 src/api/use.js create mode 100644 src/batcher.js create mode 100644 src/binding.js create mode 100644 src/compiler/compiler.js create mode 100644 src/config.js create mode 100644 src/directive.js create mode 100644 src/emitter.js create mode 100644 src/instance/data.js create mode 100644 src/instance/dom.js create mode 100644 src/instance/events.js create mode 100644 src/instance/lifecycle.js create mode 100644 src/observer/observer.js create mode 100644 src/observer/watch-array.js create mode 100644 src/observer/watch-object.js create mode 100644 src/parsers/directive.js create mode 100644 src/parsers/expression.js create mode 100644 src/parsers/path.js create mode 100644 src/parsers/template.js create mode 100644 src/parsers/text.js delete mode 100644 src/test.js create mode 100644 src/transition/css.js create mode 100644 src/transition/js.js create mode 100644 src/transition/transition.js create mode 100644 src/util.js diff --git a/src/api/asset-register.js b/src/api/asset-register.js new file mode 100644 index 00000000000..e69de29bb2d diff --git a/src/api/config.js b/src/api/config.js new file mode 100644 index 00000000000..e69de29bb2d diff --git a/src/api/extend.js b/src/api/extend.js new file mode 100644 index 00000000000..e69de29bb2d diff --git a/src/api/require.js b/src/api/require.js new file mode 100644 index 00000000000..e69de29bb2d diff --git a/src/api/use.js b/src/api/use.js new file mode 100644 index 00000000000..e69de29bb2d diff --git a/src/batcher.js b/src/batcher.js new file mode 100644 index 00000000000..e69de29bb2d diff --git a/src/binding.js b/src/binding.js new file mode 100644 index 00000000000..85e117218a5 --- /dev/null +++ b/src/binding.js @@ -0,0 +1,5 @@ +function Binding () { + +} + +module.exports = Binding \ No newline at end of file diff --git a/src/compiler/compiler.js b/src/compiler/compiler.js new file mode 100644 index 00000000000..567ab828e24 --- /dev/null +++ b/src/compiler/compiler.js @@ -0,0 +1,5 @@ +function Compiler () { + +} + +module.exports = Compiler \ No newline at end of file diff --git a/src/config.js b/src/config.js new file mode 100644 index 00000000000..7c6d6c73d3d --- /dev/null +++ b/src/config.js @@ -0,0 +1 @@ +module.exports = {} \ No newline at end of file diff --git a/src/directive.js b/src/directive.js new file mode 100644 index 00000000000..3e172ca0e07 --- /dev/null +++ b/src/directive.js @@ -0,0 +1,5 @@ +function Directive () { + +} + +module.exports = Directive \ No newline at end of file diff --git a/src/emitter.js b/src/emitter.js new file mode 100644 index 00000000000..e69de29bb2d diff --git a/src/instance/data.js b/src/instance/data.js new file mode 100644 index 00000000000..b58af4ce589 --- /dev/null +++ b/src/instance/data.js @@ -0,0 +1,15 @@ +exports.$get = function (path) { + +} + +exports.$set = function (path, val) { + +} + +exports.$watch = function (key, cb) { + +} + +exports.$unwatch = function (id) { + +} \ No newline at end of file diff --git a/src/instance/dom.js b/src/instance/dom.js new file mode 100644 index 00000000000..74d243b2c0d --- /dev/null +++ b/src/instance/dom.js @@ -0,0 +1,19 @@ +exports.$appendTo = function () { + +} + +exports.$prependTo = function () { + +} + +exports.$before = function () { + +} + +exports.$after = function () { + +} + +exports.$remove = function () { + +} \ No newline at end of file diff --git a/src/instance/events.js b/src/instance/events.js new file mode 100644 index 00000000000..ddd750e44bb --- /dev/null +++ b/src/instance/events.js @@ -0,0 +1,13 @@ +;['emit', 'on', 'off', 'once'].forEach(function (method) { + exports[method] = function () { + + } +}) + +exports.$broadcast = function () { + +} + +exports.$dispatch = function () { + +} \ No newline at end of file diff --git a/src/instance/lifecycle.js b/src/instance/lifecycle.js new file mode 100644 index 00000000000..4f139219df2 --- /dev/null +++ b/src/instance/lifecycle.js @@ -0,0 +1,7 @@ +exports.$mount = function (el) { + +} + +exports.$destroy = function () { + +} \ No newline at end of file diff --git a/src/observer/observer.js b/src/observer/observer.js new file mode 100644 index 00000000000..e69de29bb2d diff --git a/src/observer/watch-array.js b/src/observer/watch-array.js new file mode 100644 index 00000000000..e69de29bb2d diff --git a/src/observer/watch-object.js b/src/observer/watch-object.js new file mode 100644 index 00000000000..e69de29bb2d diff --git a/src/parsers/directive.js b/src/parsers/directive.js new file mode 100644 index 00000000000..e69de29bb2d diff --git a/src/parsers/expression.js b/src/parsers/expression.js new file mode 100644 index 00000000000..e69de29bb2d diff --git a/src/parsers/path.js b/src/parsers/path.js new file mode 100644 index 00000000000..e69de29bb2d diff --git a/src/parsers/template.js b/src/parsers/template.js new file mode 100644 index 00000000000..e69de29bb2d diff --git a/src/parsers/text.js b/src/parsers/text.js new file mode 100644 index 00000000000..e69de29bb2d diff --git a/src/test.js b/src/test.js deleted file mode 100644 index 3dfd42fa2cb..00000000000 --- a/src/test.js +++ /dev/null @@ -1 +0,0 @@ -module.exports = 123 \ No newline at end of file diff --git a/src/transition/css.js b/src/transition/css.js new file mode 100644 index 00000000000..e69de29bb2d diff --git a/src/transition/js.js b/src/transition/js.js new file mode 100644 index 00000000000..e69de29bb2d diff --git a/src/transition/transition.js b/src/transition/transition.js new file mode 100644 index 00000000000..e69de29bb2d diff --git a/src/util.js b/src/util.js new file mode 100644 index 00000000000..f600091865f --- /dev/null +++ b/src/util.js @@ -0,0 +1,9 @@ +// common utils + +exports.mixin = function (target, mixin) { + for (var key in mixin) { + if (target[key] !== mixin[key]) { + target[key] = mixin[key] + } + } +} \ No newline at end of file diff --git a/src/vue.js b/src/vue.js index 3215401c8ff..a6902995423 100644 --- a/src/vue.js +++ b/src/vue.js @@ -1,7 +1,28 @@ -var test = require('./test') +var _ = require('./util'), + Compiler = require('./compiler/compiler') -module.exports = { - test: function () { - return test - } -} \ No newline at end of file +/** + * The exposed Vue constructor. + */ +function Vue (options) { + this._compiler = new Compiler(this, options) +} + +// mixin instance methods +var p = Vue.prototype +_.mixin(p, require('./instance/lifecycle')) +_.mixin(p, require('./instance/data')) +_.mixin(p, require('./instance/dom')) +_.mixin(p, require('./instance/events')) + +// mixin asset registers +_.mixin(Vue, require('./api/asset-register')) + +// static methods +Vue.config = require('./api/config') +Vue.use = require('./api/use') +Vue.require = require('./api/require') +Vue.extend = require('./api/extend') +Vue.nextTick = require('./util').nextTick + +module.exports = Vue \ No newline at end of file From 63b7397be2c1695988de9e0abb32863e9372a450 Mon Sep 17 00:00:00 2001 From: Evan You Date: Tue, 8 Jul 2014 18:14:04 -0400 Subject: [PATCH 0004/1534] setup build --- .jshintrc | 17 ++++++++ gruntfile.js | 89 +++++++++++++++++++++++++++++---------- package.json | 7 +-- src/batcher.js | 1 + src/instance/data.js | 8 ++-- src/instance/lifecycle.js | 2 +- src/util.js | 8 ++-- src/vue.js | 16 +++---- test/.jshintrc | 28 ++++++++++++ test/unit/specs/main.js | 8 ++-- 10 files changed, 137 insertions(+), 47 deletions(-) create mode 100644 .jshintrc create mode 100644 test/.jshintrc diff --git a/.jshintrc b/.jshintrc new file mode 100644 index 00000000000..4a4b1305a3f --- /dev/null +++ b/.jshintrc @@ -0,0 +1,17 @@ +{ + "eqeqeq": true, + "browser": true, + "asi": true, + "multistr": true, + "undef": true, + "unused": true, + "trailing": true, + "sub": true, + "node": true, + "laxbreak": true, + "evil": true, + "eqnull": true, + "globals": { + "console": true + } +} \ No newline at end of file diff --git a/gruntfile.js b/gruntfile.js index d005408972f..29091f3d7c4 100644 --- a/gruntfile.js +++ b/gruntfile.js @@ -1,29 +1,72 @@ module.exports = function (grunt) { - grunt.initConfig({ - karma: { - options: { - frameworks: ['jasmine', 'commonjs'], - preprocessors: { - 'src/*.js': ['commonjs'], - 'test/unit/specs/*': ['commonjs'] - }, - files: [ - 'src/*.js', - 'test/unit/specs/*.js' - ], - singleRun: true - }, - browsers: { - options: { - browsers: ['Chrome', 'Firefox'], - reporters: ['progress'] - } - } + grunt.initConfig({ + + jshint: { + options: { + reporter: require('jshint-stylish'), + jshintrc: true + }, + build: { + src: ['gruntfile.js', 'tasks/*.js'] + }, + src: { + src: 'src/**/*.js' + }, + test: { + src: 'test/*/specs/*.js' + } + }, + + karma: { + options: { + frameworks: ['jasmine', 'commonjs'], + files: [ + 'src/**/*.js', + 'test/unit/specs/*.js' + ], + preprocessors: { + 'src/**/*.js': ['commonjs'], + 'test/unit/specs/*.js': ['commonjs'] + }, + singleRun: true + }, + browsers: { + options: { + browsers: ['Chrome', 'Firefox'], + reporters: ['progress'] + } + } + }, + + browserify: { + options: { + bundleOptions: { + standalone: 'Vue' + } + }, + build: { + src: ['src/vue.js'], + dest: 'dist/vue.js' + }, + watch: { + src: ['src/vue.js'], + dest: 'dist/vue.js', + options: { + watch: true, + keepAlive: true } - }) + } + } + + }) + + grunt.loadNpmTasks('grunt-contrib-jshint') + grunt.loadNpmTasks('grunt-karma') + grunt.loadNpmTasks('grunt-browserify') - grunt.loadNpmTasks('grunt-karma') - grunt.registerTask('unit', ['karma:browsers']) + grunt.registerTask('unit', ['karma:browsers']) + grunt.registerTask('watch', ['browserify:watch']) + grunt.registerTask('build', ['browserify:build']) } \ No newline at end of file diff --git a/package.json b/package.json index fe7b50bba54..cef8c691014 100644 --- a/package.json +++ b/package.json @@ -17,20 +17,21 @@ "bugs": "https://github.com/yyx990803/vue/issues", "homepage": "http://vuejs.org", "scripts": { - "test": "grunt travis" + "test": "grunt ci" }, "devDependencies": { "browserify": "^4.2.0", "grunt": "^0.4.5", "grunt-browserify": "^2.1.3", + "grunt-contrib-jshint": "^0.10.0", "grunt-karma": "^0.8.3", + "jshint-stylish": "^0.3.0", "karma": "^0.12.16", "karma-browserify": "^0.2.1", "karma-chrome-launcher": "^0.1.4", "karma-commonjs": "0.0.10", "karma-firefox-launcher": "^0.1.3", "karma-jasmine": "^0.1.5", - "karma-phantomjs-launcher": "^0.1.4", - "karma-safari-launcher": "^0.1.1" + "karma-phantomjs-launcher": "^0.1.4" } } diff --git a/src/batcher.js b/src/batcher.js index e69de29bb2d..3dfd42fa2cb 100644 --- a/src/batcher.js +++ b/src/batcher.js @@ -0,0 +1 @@ +module.exports = 123 \ No newline at end of file diff --git a/src/instance/data.js b/src/instance/data.js index b58af4ce589..9a3d64edcfe 100644 --- a/src/instance/data.js +++ b/src/instance/data.js @@ -1,15 +1,15 @@ -exports.$get = function (path) { +exports.$get = function () { } -exports.$set = function (path, val) { +exports.$set = function () { } -exports.$watch = function (key, cb) { +exports.$watch = function () { } -exports.$unwatch = function (id) { +exports.$unwatch = function () { } \ No newline at end of file diff --git a/src/instance/lifecycle.js b/src/instance/lifecycle.js index 4f139219df2..140f1554684 100644 --- a/src/instance/lifecycle.js +++ b/src/instance/lifecycle.js @@ -1,4 +1,4 @@ -exports.$mount = function (el) { +exports.$mount = function () { } diff --git a/src/util.js b/src/util.js index f600091865f..b033c8000bd 100644 --- a/src/util.js +++ b/src/util.js @@ -1,9 +1,9 @@ // common utils exports.mixin = function (target, mixin) { - for (var key in mixin) { - if (target[key] !== mixin[key]) { - target[key] = mixin[key] - } + for (var key in mixin) { + if (target[key] !== mixin[key]) { + target[key] = mixin[key] } + } } \ No newline at end of file diff --git a/src/vue.js b/src/vue.js index a6902995423..4016767c26e 100644 --- a/src/vue.js +++ b/src/vue.js @@ -1,11 +1,11 @@ -var _ = require('./util'), - Compiler = require('./compiler/compiler') +var _ = require('./util') +var Compiler = require('./compiler/compiler') /** * The exposed Vue constructor. */ function Vue (options) { - this._compiler = new Compiler(this, options) + this._compiler = new Compiler(this, options) } // mixin instance methods @@ -19,10 +19,10 @@ _.mixin(p, require('./instance/events')) _.mixin(Vue, require('./api/asset-register')) // static methods -Vue.config = require('./api/config') -Vue.use = require('./api/use') -Vue.require = require('./api/require') -Vue.extend = require('./api/extend') -Vue.nextTick = require('./util').nextTick +Vue.config = require('./api/config') +Vue.use = require('./api/use') +Vue.require = require('./api/require') +Vue.extend = require('./api/extend') +Vue.nextTick = require('./util').nextTick module.exports = Vue \ No newline at end of file diff --git a/test/.jshintrc b/test/.jshintrc new file mode 100644 index 00000000000..b438e2c635e --- /dev/null +++ b/test/.jshintrc @@ -0,0 +1,28 @@ +{ + "eqeqeq": true, + "browser": true, + "asi": true, + "multistr": true, + "undef": true, + "unused": true, + "trailing": true, + "sub": true, + "node": true, + "laxbreak": true, + "evil": true, + "globals": { + "console": true, + "it": true, + "describe": true, + "beforeEach": true, + "afterEach": true, + "expect": true, + "mock": true, + "Vue": true, + "$": true, + "mockHTMLEvent": true, + "mockMouseEvent": true, + "mockKeyEvent": true, + "casper": true + } +} \ No newline at end of file diff --git a/test/unit/specs/main.js b/test/unit/specs/main.js index 007c5d3e75f..ab39913ae08 100644 --- a/test/unit/specs/main.js +++ b/test/unit/specs/main.js @@ -1,7 +1,7 @@ -var vue = require('../../../src/vue.js') +var Vue = require('../../../src/vue.js') describe('test', function () { - it('should work', function () { - expect(vue.test()).toEqual(123) - }) + it('should work', function () { + expect(Vue).toBeDefined() + }) }) \ No newline at end of file From c225f8c3b83a2734bcaa1f7085f9bb1b2e73ce9d Mon Sep 17 00:00:00 2001 From: Evan You Date: Tue, 8 Jul 2014 18:35:39 -0400 Subject: [PATCH 0005/1534] more files --- .npmignore | 15 +++++++++ .travis.yml | 12 +++++++ CONTRIBUTING.md | 82 ++++++++++++++++++++++++++++++++++++++++++++++ LICENSE | 21 ++++++++++++ README.md | 61 ++++++++++++++++++++++++++++++++++ bower.json | 17 ++++++++++ component.json | 43 ++++++++++++++++++++++++ gruntfile.js | 6 ++++ package.json | 2 +- tasks/casper.js | 3 ++ tasks/component.js | 16 +++++++++ tasks/release.js | 3 ++ 12 files changed, 280 insertions(+), 1 deletion(-) create mode 100644 .npmignore create mode 100644 .travis.yml create mode 100644 CONTRIBUTING.md create mode 100644 LICENSE create mode 100644 README.md create mode 100644 bower.json create mode 100644 component.json create mode 100644 tasks/casper.js create mode 100644 tasks/component.js create mode 100644 tasks/release.js diff --git a/.npmignore b/.npmignore new file mode 100644 index 00000000000..d5ff25078b4 --- /dev/null +++ b/.npmignore @@ -0,0 +1,15 @@ +test +tasks +examples +explorations +components +.jshintrc +.gitignore +.travis.yml +.npmignore +bower.json +component.json +gruntfile.js +TODO.md +sauce_connect.log +coverage \ No newline at end of file diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 00000000000..596019e079b --- /dev/null +++ b/.travis.yml @@ -0,0 +1,12 @@ +language: node_js +node_js: +- '0.10' +branches: + only: + - master +before_install: +- npm install -g grunt-cli phantomjs casperjs +env: + global: + - secure: Ce9jxsESszOnGyj3A6wILO5W412El9iD/HCHiFgbr8/cSXa4Yt0ZOEZysZeyaBX6IFUCjHtQPLasVgCxijDHrhi7/drmyCE+ksruk/6LJWn9C46PZK6nI+N04iYA2TRnocFllhGbyttpbpxY04smCmGWqXwLppu9nb+VIDkKGmE= + - secure: cZQTby8mGxb4QHi9net2/kK7N2VMOZKPepa+8ob2+jxICSukPgTqGP1iVQWR+tVlU60lFAHpos2o8vQLB4e5Rt5IFEajCr+RppE9xUWxMUulbrXaIrzz1OYA5DvTi/8ZeE6/x0+MpZJT1b/GIqhlrU4QwjjpeJWLwAkv8ysZaEs= diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 00000000000..49a49a36113 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,82 @@ +# Vue.js Contributing Guide + +Hi! I’m really excited that you are interested in contributing to Vue.js. Before submitting your contribution though, please make sure to take a moment and read through the following guidelines. + +## Issue Reporting Checklist + +- Make sure that you are using the latest version of Vue. +- Try to search for your issue, it may have already been answered or even fixed in the development branch. +- It is recommended that you make a JSFiddle to reproduce your issue. You could start with [this template](http://jsfiddle.net/5sH6A/) that already includes the latest version of Vue. +- If your issue is resolved but still open, don’t hesitate to close it. In case you found a solution by yourself, it could be helpful to explain how you fixed it. + +## Pull Request Checklist + +- Checkout a topic branch from `dev` and merge back against `dev`. +- Work in the `src` folder and **DO NOT** checkin `dist` in the commits. +- Squash the commit if there are too many small ones. +- Follow the [code style](#code-style). +- Make sure the default grunt task passes. (see [development setup](#development-setup)) +- If adding new feature: + - Add accompanying test case. + - Provide convincing reason to add this feature. Ideally you should open a suggestion issue first and have it greenlighted before working on it. +- If fixing a bug: + - Provide detailed description of the bug in the PR. Live demo preferred. + - Add appropriate test coverage if applicable. + +## Code Style + +- [No semicolons unless necessary](http://inimino.org/~inimino/blog/javascript_semicolons). +- 2 spaces indentation. +- multiple var declarations. +- align equal signs where possible. +- Return early in one line if possible. +- When in doubt, read the source code. +- Break long ternary conditionals like this: + +``` js +var a = superLongConditionalStatement + ? 'yep' + : 'nope' +``` + +## Development Setup + +You will need [Node](http://nodejs.org), [Grunt](http://gruntjs.com), [PhantomJS](http://phantomjs.org) and [CasperJS](http://casperjs.org). + +``` bash +# in case you don’t already these: +# npm install -g grunt-cli phantomjs casperjs +$ npm install +``` + +To watch and auto-build `dist/vue.js` during development: + +``` bash +$ grunt watch +``` + +To lint: + +``` bash +grunt jshint +``` + +To build: + +``` bash +$ grunt build +``` + +To test: + +``` bash +# if you don’t have these yet: +# npm install -g phantomjs casperjs +$ grunt test +``` + +The unit tests are written with Jasmine and run with Karma. The functional tests are written for and run with CasperJS. + +**If you are not using a Mac** + +You can modify the Gruntfile to only run Karma tests in browsers that are available on your system. Just make sure don’t check in the Gruntfile for the commit. diff --git a/LICENSE b/LICENSE new file mode 100644 index 00000000000..17a9b2f1fe3 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2013 Yuxi Evan You + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 00000000000..c43e786cec5 --- /dev/null +++ b/README.md @@ -0,0 +1,61 @@ +

+ +# Vue.js [![Build Status](https://travis-ci.org/yyx990803/vue.svg?branch=master)](https://travis-ci.org/yyx990803/vue) [![Selenium Test Status](https://saucelabs.com/buildstatus/vuejs)](https://saucelabs.com/u/vuejs) [![Coverage Status](https://img.shields.io/coveralls/yyx990803/vue.svg)](https://coveralls.io/r/yyx990803/vue?branch=master) + +> MVVM made simple. + +## Introduction + +Vue.js is a library for building interactive web interfaces. It provides the benefits of MVVM data binding and a composable component system with a simple and flexible API. You should try it out if you like: + +- Intuitive API that simply makes sense +- Extendable Data bindings +- Plain JavaScript objects as models +- Building interface by composing reusable components +- Flexibility to mix & match the view layer with other libraries + +It's really really easy to get started. Seriously, it's so easy: + +``` html +
+ {{message}} + +
+``` + +``` js +var demo = new Vue({ + data: { + message: 'Hello Vue.js!' + } +}).$mount('#demo') +``` + +To check out the live demo, guides and API reference, visit [vuejs.org](http://vuejs.org). + +## Browser Support + +Vue.js supports [most ECMAScript 5 compliant browsers](https://saucelabs.com/u/vuejs), essentially IE9+. IE8 and below are not supported. + +## Contribution + +Read the [contributing guide](https://github.com/yyx990803/vue/blob/master/CONTRIBUTING.md). + +## Get in Touch + +- General, non source-code related questions: check the [FAQ](https://github.com/yyx990803/vue/wiki/FAQ) first, if it's not addressed in there, ask [here](https://github.com/vuejs/Discussion/issues). +- If you have a Vue-related project/component/tool, add it to [this list](https://github.com/yyx990803/vue/wiki/User-Contributed-Components-&-Tools)! +- Bugs, suggestions & feature requests: [open an issue](https://github.com/yyx990803/vue/issues) +- Twitter: [@vuejs](https://twitter.com/vuejs) +- [Google+ Community](https://plus.google.com/communities/112229843610661683911) +- freenode IRC Channel: #vuejs + +## Changelog + +See details changes for each version in the [release notes](https://github.com/yyx990803/vue/releases). + +## License + +[MIT](http://opensource.org/licenses/MIT) + +Copyright (c) 2014 Evan You \ No newline at end of file diff --git a/bower.json b/bower.json new file mode 100644 index 00000000000..12a85859296 --- /dev/null +++ b/bower.json @@ -0,0 +1,17 @@ +{ + "name": "vue", + "version": "0.11.0", + "main": "dist/vue.js", + "description": "Simple, Fast & Composable MVVM for building interative interfaces", + "authors": ["Evan You "], + "license": "MIT", + "ignore": [ + ".*", + "examples", + "test", + "tasks", + "gruntfile.js", + "*.json", + "*.md" + ] +} \ No newline at end of file diff --git a/component.json b/component.json new file mode 100644 index 00000000000..281ec708b77 --- /dev/null +++ b/component.json @@ -0,0 +1,43 @@ +{ + "name": "vue", + "version": "0.11.0", + "main": "src/main.js", + "author": "Evan You ", + "description": "Simple, Fast & Composable MVVM for building interative interfaces", + "keywords": [ + "mvvm", + "framework", + "data binding" + ], + "license": "MIT", + "scripts": [ + "src/api/asset-register.js", + "src/api/config.js", + "src/api/extend.js", + "src/api/require.js", + "src/api/use.js", + "src/batcher.js", + "src/binding.js", + "src/compiler/compiler.js", + "src/config.js", + "src/directive.js", + "src/emitter.js", + "src/instance/data.js", + "src/instance/dom.js", + "src/instance/events.js", + "src/instance/lifecycle.js", + "src/observer/observer.js", + "src/observer/watch-array.js", + "src/observer/watch-object.js", + "src/parsers/directive.js", + "src/parsers/expression.js", + "src/parsers/path.js", + "src/parsers/template.js", + "src/parsers/text.js", + "src/transition/css.js", + "src/transition/js.js", + "src/transition/transition.js", + "src/util.js", + "src/vue.js" + ] +} \ No newline at end of file diff --git a/gruntfile.js b/gruntfile.js index 29091f3d7c4..8aabb4a920d 100644 --- a/gruntfile.js +++ b/gruntfile.js @@ -61,10 +61,16 @@ module.exports = function (grunt) { }) + // load npm tasks grunt.loadNpmTasks('grunt-contrib-jshint') grunt.loadNpmTasks('grunt-karma') grunt.loadNpmTasks('grunt-browserify') + // load custom tasks + grunt.file.recurse('tasks', function (path) { + require('./' + path)(grunt) + }) + grunt.registerTask('unit', ['karma:browsers']) grunt.registerTask('watch', ['browserify:watch']) grunt.registerTask('build', ['browserify:build']) diff --git a/package.json b/package.json index cef8c691014..ef5cd7f5cd6 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "vue", - "version": "0.10.5", + "version": "0.11.0", "author": "Evan You ", "license": "MIT", "description": "Simple, Fast & Composable MVVM for building interative interfaces", diff --git a/tasks/casper.js b/tasks/casper.js new file mode 100644 index 00000000000..07d9b546047 --- /dev/null +++ b/tasks/casper.js @@ -0,0 +1,3 @@ +module.exports = function () { + +} \ No newline at end of file diff --git a/tasks/component.js b/tasks/component.js new file mode 100644 index 00000000000..cef54a4cd51 --- /dev/null +++ b/tasks/component.js @@ -0,0 +1,16 @@ +// automatically fill in component.json's script field + +module.exports = function (grunt) { + grunt.registerTask('component', function () { + + var component = grunt.file.readJSON('component.json') + component.scripts = [] + + grunt.file.recurse('src', function (file) { + component.scripts.push(file) + }) + + grunt.file.write('component.json', JSON.stringify(component, null, 2)) + + }) +} \ No newline at end of file diff --git a/tasks/release.js b/tasks/release.js new file mode 100644 index 00000000000..07d9b546047 --- /dev/null +++ b/tasks/release.js @@ -0,0 +1,3 @@ +module.exports = function () { + +} \ No newline at end of file From b5bfc59a704f6d8e01b93352319b356b710ad850 Mon Sep 17 00:00:00 2001 From: Evan You Date: Wed, 9 Jul 2014 01:35:20 -0400 Subject: [PATCH 0006/1534] working on new observer --- CONTRIBUTING.md | 7 +- component.json | 4 +- src/batcher.js | 1 - src/emitter.js | 137 ++++++++++++++++++++ src/observer/{watch-array.js => array.js} | 0 src/observer/{watch-object.js => object.js} | 0 src/observer/observer.js | 75 +++++++++++ src/util.js | 30 ++++- src/vue.js | 9 +- 9 files changed, 256 insertions(+), 7 deletions(-) rename src/observer/{watch-array.js => array.js} (100%) rename src/observer/{watch-object.js => object.js} (100%) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 49a49a36113..c1ddda975ec 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -26,10 +26,13 @@ Hi! I’m really excited that you are interested in contributing to Vue.js. Befo ## Code Style - [No semicolons unless necessary](http://inimino.org/~inimino/blog/javascript_semicolons). +- Follow JSDoc. - 2 spaces indentation. - multiple var declarations. -- align equal signs where possible. -- Return early in one line if possible. +- align equal signs where appropriate. +- Return early. +- 1 space after `function` +- 1 space between arguments, but not between parens. - When in doubt, read the source code. - Break long ternary conditionals like this: diff --git a/component.json b/component.json index 281ec708b77..7e16fd53264 100644 --- a/component.json +++ b/component.json @@ -26,9 +26,9 @@ "src/instance/dom.js", "src/instance/events.js", "src/instance/lifecycle.js", + "src/observer/array.js", + "src/observer/object.js", "src/observer/observer.js", - "src/observer/watch-array.js", - "src/observer/watch-object.js", "src/parsers/directive.js", "src/parsers/expression.js", "src/parsers/path.js", diff --git a/src/batcher.js b/src/batcher.js index 3dfd42fa2cb..e69de29bb2d 100644 --- a/src/batcher.js +++ b/src/batcher.js @@ -1 +0,0 @@ -module.exports = 123 \ No newline at end of file diff --git a/src/emitter.js b/src/emitter.js index e69de29bb2d..296e07646a9 100644 --- a/src/emitter.js +++ b/src/emitter.js @@ -0,0 +1,137 @@ +/** + * Simple event emitter based on component/emitter. + * + * @constructor + * @param {Object} ctx - the context to call listners with. + */ + +function Emitter (ctx) { + this._ctx = ctx || this +} + +var p = Emitter.prototype + +/** + * Listen on the given `event` with `fn`. + * + * @param {String} event + * @param {Function} fn + * @return {Emitter} + */ + +p.on = function (event, fn) { + this._cbs = this._cbs || {} + ;(this._cbs[event] = this._cbs[event] || []) + .push(fn) + return this +} + +/** + * Adds an `event` listener that will be invoked a single + * time then automatically removed. + * + * @param {String} event + * @param {Function} fn + * @return {Emitter} + */ + +p.once = function (event, fn) { + var self = this + this._cbs = this._cbs || {} + + function on () { + self.off(event, on) + fn.apply(this, arguments) + } + + on.fn = fn + this.on(event, on) + return this +} + +/** + * Remove the given callback for `event` or all + * registered callbacks. + * + * @param {String} event + * @param {Function} fn + * @return {Emitter} + */ + +p.off = function (event, fn) { + this._cbs = this._cbs || {} + + // all + if (!arguments.length) { + this._cbs = {} + return this + } + + // specific event + var callbacks = this._cbs[event] + if (!callbacks) return this + + // remove all handlers + if (arguments.length === 1) { + delete this._cbs[event] + return this + } + + // remove specific handler + var cb + for (var i = 0; i < callbacks.length; i++) { + cb = callbacks[i] + if (cb === fn || cb.fn === fn) { + callbacks.splice(i, 1) + break + } + } + return this +} + +/** + * The internal, faster emit with fixed amount of arguments + * using Function.call. + * + * @param {Object} event + * @return {Emitter} + */ + +p.emit = function (event, a, b, c) { + this._cbs = this._cbs || {} + var callbacks = this._cbs[event] + + if (callbacks) { + callbacks = callbacks.slice(0) + for (var i = 0, len = callbacks.length; i < len; i++) { + callbacks[i].call(this._ctx, a, b, c) + } + } + + return this +} + +/** + * The external emit using Function.apply, used + * by Vue instance event methods. + * + * @param {Object} event + * @return {Emitter} + */ + +p.applyEmit = function (event) { + this._cbs = this._cbs || {} + var callbacks = this._cbs[event], args + + if (callbacks) { + callbacks = callbacks.slice(0) + args = callbacks.slice.call(arguments, 1) + for (var i = 0, len = callbacks.length; i < len; i++) { + callbacks[i].apply(this._ctx, args) + } + } + + return this +} + +module.exports = Emitter \ No newline at end of file diff --git a/src/observer/watch-array.js b/src/observer/array.js similarity index 100% rename from src/observer/watch-array.js rename to src/observer/array.js diff --git a/src/observer/watch-object.js b/src/observer/object.js similarity index 100% rename from src/observer/watch-object.js rename to src/observer/object.js diff --git a/src/observer/observer.js b/src/observer/observer.js index e69de29bb2d..bdda5c2fadb 100644 --- a/src/observer/observer.js +++ b/src/observer/observer.js @@ -0,0 +1,75 @@ +var _ = require('../util') +var Emitter = require('../emitter') + +/** + * Observer class that are attached to each observed + * object. They are essentially event emitters, but can + * connect to each other and relay the events up the nested + * object chain. + * + * @constructor + * @extends Emitter + * @private + */ + +function Observer () { + Emitter.call(this) + this.connections = Object.create(null) +} + +var p = Observer.prototype = Object.create(Emitter.prototype) + +/** + * Observe an object of unkown type. + * + * @param {*} obj + * @return {Boolean} - returns true if successfully observed. + */ + +p.observe = function (obj) { + if (obj && obj.$observer) { + // already observed + return + } + if (_.isArray(obj)) { + this.observeArray(obj) + return true + } + if (_.isObject(obj)) { + this.observeObject(obj) + return true + } +} + +/** + * Connect to another Observer instance, + * capture its get/set/mutate events and relay the events + * while prepending a key segment to the path. + * + * @param {Observer} target + * @param {String} key + */ + +p.connect = function (target, key) { + +} + +/** + * Disconnect from a connected target Observer. + * + * @param {Observer} target + * @param {String} key + */ + +p.disconnect = function (target, key) { + +} + +/** + * Mixin Array and Object observe methods + */ + +_.mixin(p, require('./array')) +_.mixin(p, require('./object')) + +module.exports = Observer \ No newline at end of file diff --git a/src/util.js b/src/util.js index b033c8000bd..49c923cf6d1 100644 --- a/src/util.js +++ b/src/util.js @@ -1,4 +1,9 @@ -// common utils +/** + * Mix properties into target object. + * + * @param {Object} target + * @param {Object} mixin + */ exports.mixin = function (target, mixin) { for (var key in mixin) { @@ -6,4 +11,27 @@ exports.mixin = function (target, mixin) { target[key] = mixin[key] } } +} + +/** + * Object type check. Only returns true + * for plain JavaScript objects. + * + * @param {*} obj + * @return {Boolean} + */ + +exports.isObject = function (obj) { + return Object.prototype.toString.call(obj) === '[object Object]' +} + +/** + * Array type check. + * + * @param {*} obj + * @return {Boolean} + */ + +exports.isArray = function (obj) { + return Array.isArray(obj) } \ No newline at end of file diff --git a/src/vue.js b/src/vue.js index 4016767c26e..9164a37a8ba 100644 --- a/src/vue.js +++ b/src/vue.js @@ -2,13 +2,18 @@ var _ = require('./util') var Compiler = require('./compiler/compiler') /** - * The exposed Vue constructor. + * The exposed Vue constructor. + * + * @constructor + * @public */ + function Vue (options) { this._compiler = new Compiler(this, options) } // mixin instance methods + var p = Vue.prototype _.mixin(p, require('./instance/lifecycle')) _.mixin(p, require('./instance/data')) @@ -16,9 +21,11 @@ _.mixin(p, require('./instance/dom')) _.mixin(p, require('./instance/events')) // mixin asset registers + _.mixin(Vue, require('./api/asset-register')) // static methods + Vue.config = require('./api/config') Vue.use = require('./api/use') Vue.require = require('./api/require') From 55bfa2f2e78537621e66654ef44b6fd705f03e3f Mon Sep 17 00:00:00 2001 From: Evan You Date: Wed, 9 Jul 2014 10:58:53 -0400 Subject: [PATCH 0007/1534] _.define --- src/util.js | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/src/util.js b/src/util.js index 49c923cf6d1..12ac9b2e3c5 100644 --- a/src/util.js +++ b/src/util.js @@ -34,4 +34,21 @@ exports.isObject = function (obj) { exports.isArray = function (obj) { return Array.isArray(obj) +} + +/** + * Define a readonly, in-enumerable property + * + * @param {Object} obj + * @param {String} key + * @param {*} val + */ + +exports.define = function (obj, key, val) { + Object.defineProperty(obj, key, { + value: val, + enumerable: false, + writable: false, + configurable: true + }) } \ No newline at end of file From 0ad7f54602b41d2dfa82e2368fef1ee2639fecd4 Mon Sep 17 00:00:00 2001 From: Evan You Date: Wed, 9 Jul 2014 19:08:01 -0400 Subject: [PATCH 0008/1534] working on observer --- .jshintrc | 1 + gruntfile.js | 15 ++- src/observer/array-augmentations.js | 83 ++++++++++++++ src/observer/array.js | 0 src/observer/object-augmentations.js | 36 ++++++ src/observer/object.js | 0 src/observer/observer.js | 157 ++++++++++++++++++++++----- src/util.js | 32 +++++- test/unit/observer.js | 12 ++ test/unit/specs/main.js | 7 -- 10 files changed, 298 insertions(+), 45 deletions(-) create mode 100644 src/observer/array-augmentations.js delete mode 100644 src/observer/array.js create mode 100644 src/observer/object-augmentations.js delete mode 100644 src/observer/object.js create mode 100644 test/unit/observer.js delete mode 100644 test/unit/specs/main.js diff --git a/.jshintrc b/.jshintrc index 4a4b1305a3f..ee94095c3cb 100644 --- a/.jshintrc +++ b/.jshintrc @@ -11,6 +11,7 @@ "laxbreak": true, "evil": true, "eqnull": true, + "proto": true, "globals": { "console": true } diff --git a/gruntfile.js b/gruntfile.js index 8aabb4a920d..4b8bc4ae41d 100644 --- a/gruntfile.js +++ b/gruntfile.js @@ -23,18 +23,24 @@ module.exports = function (grunt) { frameworks: ['jasmine', 'commonjs'], files: [ 'src/**/*.js', - 'test/unit/specs/*.js' + 'test/unit/**/*.js' ], preprocessors: { 'src/**/*.js': ['commonjs'], - 'test/unit/specs/*.js': ['commonjs'] + 'test/unit/**/*.js': ['commonjs'] }, singleRun: true }, browsers: { options: { - browsers: ['Chrome', 'Firefox'], - reporters: ['progress'] + browsers: ['Chrome', 'Firefox'], + reporters: ['progress'] + } + }, + phantom: { + options: { + browsers: ['PhantomJS'], + reporters: ['progress'] } } }, @@ -72,6 +78,7 @@ module.exports = function (grunt) { }) grunt.registerTask('unit', ['karma:browsers']) + grunt.registerTask('phantom', ['karma:phantom']) grunt.registerTask('watch', ['browserify:watch']) grunt.registerTask('build', ['browserify:build']) diff --git a/src/observer/array-augmentations.js b/src/observer/array-augmentations.js new file mode 100644 index 00000000000..5307307fbb3 --- /dev/null +++ b/src/observer/array-augmentations.js @@ -0,0 +1,83 @@ +var _ = require('../util') +var slice = [].slice +var arrayAugmentations = Object.create(Array.prototype) + +/** + * Intercept mutating methods and emit events + */ + +;[ + 'push', + 'pop', + 'shift', + 'unshift', + 'splice', + 'sort', + 'reverse' +] +.forEach(function (method) { + // cache original method + var original = Array.prototype[method] + // define wrapped method + _.define(arrayAugmentations, method, function () { + var args = slice.call(arguments) + var result = original.apply(this, args) + var ob = this.$observer + var inserted, removed + + switch (method) { + case 'push': + case 'unshift': + inserted = args + break + case 'pop': + case 'shift': + removed = [result] + break + case 'splice': + inserted = args.slice(2) + removed = result + break + } + + ob.link(inserted) + ob.unlink(removed) + // empty key, value is self + ob.emit('mutate', '', this, { + method : method, + args : args, + result : result, + inserted : inserted, + removed : removed + }) + }) +}) + +/** + * Swap the element at the given index with a new value + * and emits corresponding event. + * + * @param {Number} index + * @param {*} val + * @return {*} - replaced element + */ + +_.define(arrayAugmentations, '$set', function (index, val) { + if (index >= this.length) { + this.length = index + 1 + } + return this.splice(index, 1, val)[0] +}) + +/** + * Convenience method to remove the element at given index. + * + * @param {Number} index + * @param {*} val + */ + +_.define(arrayAugmentations, '$remove', function (index) { + if (index > -1) { + return this.splice(index, 1)[0] + } +}) \ No newline at end of file diff --git a/src/observer/array.js b/src/observer/array.js deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/src/observer/object-augmentations.js b/src/observer/object-augmentations.js new file mode 100644 index 00000000000..afd93b3fa07 --- /dev/null +++ b/src/observer/object-augmentations.js @@ -0,0 +1,36 @@ +var _ = require('../util') +var objectAgumentations = Object.create(Object.prototype) + +/** + * Add a new property to an observed object + * and emits corresponding event + * + * @param {String} key + * @param {*} val + * @public + */ + +_.define(objectAgumentations, '$add', function (key, val) { + if (this.hasOwnProperty(key)) return + this[key] = val + this.$observer.convert(key, val) + this.$observer.emit('add', key, val) +}) + +/** + * Deletes a property from an observed object + * and emits corresponding event + * + * @param {String} key + * @public + */ + +_.define(objectAgumentations, '$delete', function (key) { + if (!this.hasOwnProperty(key)) return + // trigger set events + this[key] = undefined + delete this[key] + this.$observer.emit('delete', key) +}) + +module.exports = objectAgumentations \ No newline at end of file diff --git a/src/observer/object.js b/src/observer/object.js deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/src/observer/observer.js b/src/observer/observer.js index bdda5c2fadb..7734121fb49 100644 --- a/src/observer/observer.js +++ b/src/observer/observer.js @@ -1,75 +1,174 @@ var _ = require('../util') var Emitter = require('../emitter') +var arrayAugmentations = require('./array-augmentations') +var objectAugmentations = require('./object-augmentations') + +// Type enums + +var ARRAY = 0 +var OBJECT = 1 /** * Observer class that are attached to each observed * object. They are essentially event emitters, but can - * connect to each other and relay the events up the nested - * object chain. + * connect to each other like nodes to map the hierarchy + * of data objects. Once connected, detected change events + * can propagate up the nested object chain. + * + * The constructor can be invoked without arguments to + * create a value-less observer that simply listens to + * other observers. * * @constructor * @extends Emitter - * @private + * @param {Array|Object} [value] + * @param {Number} [type] */ -function Observer () { +function Observer (value, type) { Emitter.call(this) - this.connections = Object.create(null) + this.value = value + this.type = type + this.initiated = false + this.children = Object.create(null) + if (value) { + _.define(value, '$observer', this) + } } var p = Observer.prototype = Object.create(Emitter.prototype) /** - * Observe an object of unkown type. - * - * @param {*} obj - * @return {Boolean} - returns true if successfully observed. + * Initialize the observation based on value type. + * Should only be called once. */ -p.observe = function (obj) { - if (obj && obj.$observer) { - // already observed - return - } - if (_.isArray(obj)) { - this.observeArray(obj) - return true +p.init = function () { + var value = this.value + if (this.type === ARRAY) { + _.augment(value, arrayAugmentations) + this.link(value) + } else if (this.type === OBJECT) { + _.augment(value, objectAugmentations) + this.walk(value) } - if (_.isObject(obj)) { - this.observeObject(obj) - return true + this.initiated = true +} + +/** + * Walk through each property, converting them and adding them as child. + * This method should only be called when value type is Object. + * + * @param {Object} obj + */ + +p.walk = function (obj) { + var key, val, ob + for (key in obj) { + val = obj[key] + ob = Observer.create(val) + if (ob) { + this.add(key, ob) + if (ob.initiated) { + this.deliver(key, val) + } else { + ob.init() + } + } else { + this.convert(key, val) + } } } /** - * Connect to another Observer instance, + * Link a list of items to the observer's value Array. + * When any of these items emit change event, the Array will be notified. + * + * @param {Array} items + */ + +p.link = function (items) { + +} + +/** + * Unlink the items from the observer's value Array. + * + * @param {Array} items + */ + +p.unlink = function (items) { + +} + +/** + * Convert a tip value into getter/setter so we can emit the events + * when the property is accessed/changed. + * + * @param {String} key + * @param {*} val + */ + +p.convert = function (key, val) { + +} + +/** + * Walk through an already observed object and emit its tip values. + * This is necessary because newly observed objects emit their values + * during init; for already observed ones we can skip the initialization, + * but still need to emit the values. + * + * @param {String} key + * @param {*} val + */ + +p.deliver = function (key, val) { + +} + +/** + * Add a child observer for a property key, * capture its get/set/mutate events and relay the events * while prepending a key segment to the path. * - * @param {Observer} target * @param {String} key + * @param {Observer} ob */ -p.connect = function (target, key) { +p.add = function (key, ob) { } /** - * Disconnect from a connected target Observer. + * Remove a child observer. * - * @param {Observer} target * @param {String} key + * @param {Observer} ob */ -p.disconnect = function (target, key) { +p.remove = function (key, ob) { } /** - * Mixin Array and Object observe methods + * Attempt to create an observer instance for a value, + * returns the new observer if successfully observed, + * or the existing observer if the value already has one. + * + * @param {*} value + * @return {Observer} + * @static */ -_.mixin(p, require('./array')) -_.mixin(p, require('./object')) +Observer.create = function (value) { + if (value && value.$observer) { + return value.$observer + } if (_.isArray(value)) { + return new Observer(value, ARRAY) + } else if (_.isObject(value)) { + return new Observer(value, OBJECT) + } +} module.exports = Observer \ No newline at end of file diff --git a/src/util.js b/src/util.js index 12ac9b2e3c5..cc87511fe8f 100644 --- a/src/util.js +++ b/src/util.js @@ -37,7 +37,7 @@ exports.isArray = function (obj) { } /** - * Define a readonly, in-enumerable property + * Define a non-enumerable property * * @param {Object} obj * @param {String} key @@ -46,9 +46,31 @@ exports.isArray = function (obj) { exports.define = function (obj, key, val) { Object.defineProperty(obj, key, { - value: val, - enumerable: false, - writable: false, - configurable: true + value : val, + enumerable : false, + writable : true, + configurable : true }) +} + +/** + * Augment an target Object or Array by either + * intercepting the prototype chain using __proto__, + * or copy over property descriptors + * + * @param {Object|Array} target + * @param {Object} proto + */ + +if ('__proto__' in {}) { + exports.augment = function (target, proto) { + target.__proto__ = proto + } +} else { + exports.augment = function (target, proto) { + Object.getOwnPropertyNames(proto).forEach(function (key) { + var descriptor = Object.getOwnPropertyDescriptor(proto, key) + Object.defineProperty(target, key, descriptor) + }) + } } \ No newline at end of file diff --git a/test/unit/observer.js b/test/unit/observer.js new file mode 100644 index 00000000000..953e89a41cf --- /dev/null +++ b/test/unit/observer.js @@ -0,0 +1,12 @@ +var Observer = require('../../src/observer/observer') + +describe('Observer', function () { + + it('should work', function () { + var obj = {} + var ob = Observer.create(obj) + ob.init() + expect(obj.$add).toBeDefined() + }) + +}) \ No newline at end of file diff --git a/test/unit/specs/main.js b/test/unit/specs/main.js deleted file mode 100644 index ab39913ae08..00000000000 --- a/test/unit/specs/main.js +++ /dev/null @@ -1,7 +0,0 @@ -var Vue = require('../../../src/vue.js') - -describe('test', function () { - it('should work', function () { - expect(Vue).toBeDefined() - }) -}) \ No newline at end of file From 30f67ab14090900a28c18db01cf631c037d36e81 Mon Sep 17 00:00:00 2001 From: Evan You Date: Wed, 9 Jul 2014 22:33:39 -0400 Subject: [PATCH 0009/1534] observer object --- src/observer/observer.js | 133 +++++++++++++++++++++++++++++++-------- test/unit/observer.js | 23 +++++-- 2 files changed, 127 insertions(+), 29 deletions(-) diff --git a/src/observer/observer.js b/src/observer/observer.js index 7734121fb49..548ec9c268c 100644 --- a/src/observer/observer.js +++ b/src/observer/observer.js @@ -30,7 +30,7 @@ function Observer (value, type) { this.value = value this.type = type this.initiated = false - this.children = Object.create(null) + this.adaptors = Object.create(null) if (value) { _.define(value, '$observer', this) } @@ -63,53 +63,105 @@ p.init = function () { */ p.walk = function (obj) { - var key, val, ob + var key, val for (key in obj) { val = obj[key] - ob = Observer.create(val) - if (ob) { - this.add(key, ob) - if (ob.initiated) { - this.deliver(key, val) - } else { - ob.init() - } + this.observe(key, val) + this.convert(key, val) + } +} + +/** + * If a property is observable, + * create an Observer for it and add it as a child. + * This method is called only on properties observed + * for the first time. + * + * @param {String} key + * @param {*} val + */ + +p.observe = function (key, val) { + var ob = Observer.create(val) + if (ob) { + this.add(key, ob) + if (ob.initiated) { + this.deliver(key, val) } else { - this.convert(key, val) + ob.init() } } + // emit an initial set event + this.emit('set', key, val) + if (_.isArray(val)) { + this.emit('set', key + '.length', val.length) + } } /** - * Link a list of items to the observer's value Array. - * When any of these items emit change event, the Array will be notified. + * Unobserve a property. + * If it has an observer, remove it from children. * - * @param {Array} items + * @param {String} key + * @param {*} val */ -p.link = function (items) { - +p.unobserve = function (key, val) { + if (val && val.$observer) { + this.remove(key, val.$observer) + } } /** - * Unlink the items from the observer's value Array. + * Convert a tip value into getter/setter so we can emit + * the events when the property is accessed/changed. + * Properties prefixed with `$` or `_` are ignored. + * + * @param {String} key + * @param {*} val + */ + +p.convert = function (key, val) { + var prefix = key.charAt(0) + if (prefix === '$' || prefix === '_') { + return + } + var ob = this + Object.defineProperty(this.value, key, { + enumerable: true, + configurable: true, + get: function () { + ob.emit('get', key) + return val + }, + set: function (newVal) { + if (newVal === val) return + ob.unobserve(key, val) + ob.observe(key, newVal) + val = newVal + } + }) +} + +/** + * Link a list of items to the observer's value Array. + * When any of these items emit change event, the Array will be notified. + * This method should only be called when value type is Array. * * @param {Array} items */ -p.unlink = function (items) { +p.link = function (items) { } /** - * Convert a tip value into getter/setter so we can emit the events - * when the property is accessed/changed. + * Unlink the items from the observer's value Array. * - * @param {String} key - * @param {*} val + * @param {Array} items */ -p.convert = function (key, val) { +p.unlink = function (items) { } @@ -137,7 +189,34 @@ p.deliver = function (key, val) { */ p.add = function (key, ob) { + var self = this + var base = key + '.' + var adaptors = this.adaptors[key] = {} + + adaptors.get = function (path) { + path = base + path + self.emit('get', path) + } + adaptors.set = function (path, val) { + path = base + path + self.emit('set', path, val) + } + + adaptors.mutate = function (path, val, mutation) { + // if path is empty string, the mutation + // comes directly from an Array + path = path + ? base + path + : key + self.emit('mutate', path, val, mutation) + // also emit for length + self.emit('set', path + '.length', val.length) + } + + ob.on('get', adaptors.get) + .on('set', adaptors.set) + .on('mutate', adaptors.mutate) } /** @@ -148,7 +227,11 @@ p.add = function (key, ob) { */ p.remove = function (key, ob) { - + var adaptors = this.adaptors[key] + this.adaptors[key] = null + ob.off('get', adaptors.get) + .off('set', adaptors.set) + .off('mutate', adaptors.mutate) } /** @@ -157,7 +240,7 @@ p.remove = function (key, ob) { * or the existing observer if the value already has one. * * @param {*} value - * @return {Observer} + * @return {Observer|undefined} * @static */ diff --git a/test/unit/observer.js b/test/unit/observer.js index 953e89a41cf..2678d716d6e 100644 --- a/test/unit/observer.js +++ b/test/unit/observer.js @@ -2,11 +2,26 @@ var Observer = require('../../src/observer/observer') describe('Observer', function () { - it('should work', function () { - var obj = {} - var ob = Observer.create(obj) + var obj, ob, spy + + beforeEach(function () { + obj = { + a: 1, + b: { + c: 2 + } + } + ob = Observer.create(obj) ob.init() - expect(obj.$add).toBeDefined() + spy = jasmine.createSpy() + }) + + it('should emit set events', function () { + ob.on('set', spy) + obj.a = 3 + expect(spy).toHaveBeenCalledWith('a', 3, undefined) + obj.b.c = 4 + expect(spy).toHaveBeenCalledWith('b.c', 4, undefined) }) }) \ No newline at end of file From d48906d537f1196909d95f82950e3645304e3731 Mon Sep 17 00:00:00 2001 From: Evan You Date: Wed, 9 Jul 2014 23:42:30 -0400 Subject: [PATCH 0010/1534] use \\b as delimiter --- src/observer/array-augmentations.js | 4 +- src/observer/observer.js | 65 ++++++++++++++++------------- src/vue.js | 13 ++++-- test/unit/observer.js | 3 +- 4 files changed, 51 insertions(+), 34 deletions(-) diff --git a/src/observer/array-augmentations.js b/src/observer/array-augmentations.js index 5307307fbb3..166ea667b05 100644 --- a/src/observer/array-augmentations.js +++ b/src/observer/array-augmentations.js @@ -42,8 +42,8 @@ var arrayAugmentations = Object.create(Array.prototype) ob.link(inserted) ob.unlink(removed) - // empty key, value is self - ob.emit('mutate', '', this, { + // empty path, value is the Array itself + ob.emit('mutate', [], this, { method : method, args : args, result : result, diff --git a/src/observer/observer.js b/src/observer/observer.js index 548ec9c268c..ae9dd7ed1d6 100644 --- a/src/observer/observer.js +++ b/src/observer/observer.js @@ -3,17 +3,18 @@ var Emitter = require('../emitter') var arrayAugmentations = require('./array-augmentations') var objectAugmentations = require('./object-augmentations') -// Type enums +/** + * Type enums + */ var ARRAY = 0 var OBJECT = 1 /** * Observer class that are attached to each observed - * object. They are essentially event emitters, but can - * connect to each other like nodes to map the hierarchy - * of data objects. Once connected, detected change events - * can propagate up the nested object chain. + * object. Observers can connect to each other like nodes + * to map the hierarchy of data objects. Once connected, + * detected change events can propagate up the nested chain. * * The constructor can be invoked without arguments to * create a value-less observer that simply listens to @@ -38,6 +39,34 @@ function Observer (value, type) { var p = Observer.prototype = Object.create(Emitter.prototype) +/** + * Instead of the dot, we use the backspace character + * which is much less likely to appear as property keys + * in JavaScript. + */ + +var delimiter = Observer.pathDelimiter = '\b' + +/** + * Attempt to create an observer instance for a value, + * returns the new observer if successfully observed, + * or the existing observer if the value already has one. + * + * @param {*} value + * @return {Observer|undefined} + * @static + */ + +Observer.create = function (value) { + if (value && value.$observer) { + return value.$observer + } if (_.isArray(value)) { + return new Observer(value, ARRAY) + } else if (_.isObject(value)) { + return new Observer(value, OBJECT) + } +} + /** * Initialize the observation based on value type. * Should only be called once. @@ -94,7 +123,7 @@ p.observe = function (key, val) { // emit an initial set event this.emit('set', key, val) if (_.isArray(val)) { - this.emit('set', key + '.length', val.length) + this.emit('set', key + delimiter + 'length', val.length) } } @@ -190,7 +219,7 @@ p.deliver = function (key, val) { p.add = function (key, ob) { var self = this - var base = key + '.' + var base = key + delimiter var adaptors = this.adaptors[key] = {} adaptors.get = function (path) { @@ -211,7 +240,7 @@ p.add = function (key, ob) { : key self.emit('mutate', path, val, mutation) // also emit for length - self.emit('set', path + '.length', val.length) + self.emit('set', path + delimiter + 'length', val.length) } ob.on('get', adaptors.get) @@ -234,24 +263,4 @@ p.remove = function (key, ob) { .off('mutate', adaptors.mutate) } -/** - * Attempt to create an observer instance for a value, - * returns the new observer if successfully observed, - * or the existing observer if the value already has one. - * - * @param {*} value - * @return {Observer|undefined} - * @static - */ - -Observer.create = function (value) { - if (value && value.$observer) { - return value.$observer - } if (_.isArray(value)) { - return new Observer(value, ARRAY) - } else if (_.isObject(value)) { - return new Observer(value, OBJECT) - } -} - module.exports = Observer \ No newline at end of file diff --git a/src/vue.js b/src/vue.js index 9164a37a8ba..3b139cb0443 100644 --- a/src/vue.js +++ b/src/vue.js @@ -5,6 +5,7 @@ var Compiler = require('./compiler/compiler') * The exposed Vue constructor. * * @constructor + * @param {Object} [options] * @public */ @@ -12,7 +13,9 @@ function Vue (options) { this._compiler = new Compiler(this, options) } -// mixin instance methods +/** + * Mixin instance methods + */ var p = Vue.prototype _.mixin(p, require('./instance/lifecycle')) @@ -20,11 +23,15 @@ _.mixin(p, require('./instance/data')) _.mixin(p, require('./instance/dom')) _.mixin(p, require('./instance/events')) -// mixin asset registers +/** + * Mixin asset registers + */ _.mixin(Vue, require('./api/asset-register')) -// static methods +/** + * Static methods + */ Vue.config = require('./api/config') Vue.use = require('./api/use') diff --git a/test/unit/observer.js b/test/unit/observer.js index 2678d716d6e..bd8e6c55be8 100644 --- a/test/unit/observer.js +++ b/test/unit/observer.js @@ -1,4 +1,5 @@ var Observer = require('../../src/observer/observer') +var delimiter = Observer.pathDelimiter describe('Observer', function () { @@ -21,7 +22,7 @@ describe('Observer', function () { obj.a = 3 expect(spy).toHaveBeenCalledWith('a', 3, undefined) obj.b.c = 4 - expect(spy).toHaveBeenCalledWith('b.c', 4, undefined) + expect(spy).toHaveBeenCalledWith('b' + delimiter + 'c', 4, undefined) }) }) \ No newline at end of file From f097ca23103c0b764ad0a83826ae44a5cf02c00a Mon Sep 17 00:00:00 2001 From: Evan You Date: Wed, 9 Jul 2014 23:58:03 -0400 Subject: [PATCH 0011/1534] comments --- src/observer/observer.js | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/observer/observer.js b/src/observer/observer.js index ae9dd7ed1d6..bb42baa7da8 100644 --- a/src/observer/observer.js +++ b/src/observer/observer.js @@ -40,9 +40,11 @@ function Observer (value, type) { var p = Observer.prototype = Object.create(Emitter.prototype) /** + * Simply concatenating the path segments with `.` cannot + * deal with keys that happen to contain the dot. + * * Instead of the dot, we use the backspace character - * which is much less likely to appear as property keys - * in JavaScript. + * which is much less likely to appear in property keys. */ var delimiter = Observer.pathDelimiter = '\b' From 42e08352c22a52a627ef29f1bb241f7c6a074310 Mon Sep 17 00:00:00 2001 From: Evan You Date: Thu, 10 Jul 2014 17:24:35 -0400 Subject: [PATCH 0012/1534] save --- src/observer/observer.js | 7 +++++-- src/parsers/template.js | 0 src/template.js | 10 ++++++++++ test/unit/observer.js | 12 ++++++++++++ 4 files changed, 27 insertions(+), 2 deletions(-) delete mode 100644 src/parsers/template.js create mode 100644 src/template.js diff --git a/src/observer/observer.js b/src/observer/observer.js index bb42baa7da8..d89f30ae7ae 100644 --- a/src/observer/observer.js +++ b/src/observer/observer.js @@ -96,6 +96,7 @@ p.init = function () { p.walk = function (obj) { var key, val for (key in obj) { + if (!obj.hasOwnProperty(key)) return val = obj[key] this.observe(key, val) this.convert(key, val) @@ -202,8 +203,10 @@ p.unlink = function (items) { * during init; for already observed ones we can skip the initialization, * but still need to emit the values. * - * @param {String} key - * @param {*} val + * If called with no arguments, it delivers set events for the root value. + * + * @param {String} [key] + * @param {*} [val] */ p.deliver = function (key, val) { diff --git a/src/parsers/template.js b/src/parsers/template.js deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/src/template.js b/src/template.js new file mode 100644 index 00000000000..dbaf56a2d99 --- /dev/null +++ b/src/template.js @@ -0,0 +1,10 @@ +/** + * The Template class encapsulates the logic of template + * caching, instantiation, parsing and transclusion. + */ + +function Template () { + +} + +module.exports = Template \ No newline at end of file diff --git a/test/unit/observer.js b/test/unit/observer.js index bd8e6c55be8..d70f77adfe9 100644 --- a/test/unit/observer.js +++ b/test/unit/observer.js @@ -25,4 +25,16 @@ describe('Observer', function () { expect(spy).toHaveBeenCalledWith('b' + delimiter + 'c', 4, undefined) }) + it('should emit get events', function () { + // body... + }) + + it('should emit mutation events on Array mutation', function () { + // body... + }) + + it('should emit ', function () { + // body... + }) + }) \ No newline at end of file From 3acc354f6473c93e827ccb953e4417d78a40ff3a Mon Sep 17 00:00:00 2001 From: Evan You Date: Thu, 10 Jul 2014 20:57:34 -0400 Subject: [PATCH 0013/1534] more observer work --- src/emitter.js | 2 +- src/observer/array-augmentations.js | 14 +- src/observer/object-augmentations.js | 4 +- src/observer/observer.js | 190 ++++++++++----------------- test/unit/observer.js | 78 +++++++++-- 5 files changed, 151 insertions(+), 137 deletions(-) diff --git a/src/emitter.js b/src/emitter.js index 296e07646a9..04284e9a498 100644 --- a/src/emitter.js +++ b/src/emitter.js @@ -21,7 +21,7 @@ var p = Emitter.prototype p.on = function (event, fn) { this._cbs = this._cbs || {} - ;(this._cbs[event] = this._cbs[event] || []) + ;(this._cbs[event] || (this._cbs[event] = [])) .push(fn) return this } diff --git a/src/observer/array-augmentations.js b/src/observer/array-augmentations.js index 166ea667b05..9f300f93e1c 100644 --- a/src/observer/array-augmentations.js +++ b/src/observer/array-augmentations.js @@ -20,6 +20,7 @@ var arrayAugmentations = Object.create(Array.prototype) var original = Array.prototype[method] // define wrapped method _.define(arrayAugmentations, method, function () { + var args = slice.call(arguments) var result = original.apply(this, args) var ob = this.$observer @@ -40,10 +41,17 @@ var arrayAugmentations = Object.create(Array.prototype) break } - ob.link(inserted) - ob.unlink(removed) + // link/unlink added/removed elements + if (inserted) ob.link(inserted) + if (removed) ob.unlink(removed) + + // emit length change + if (inserted || removed) { + ob.notify('set', 'length', this.length) + } + // empty path, value is the Array itself - ob.emit('mutate', [], this, { + ob.notify('mutate', '', this, { method : method, args : args, result : result, diff --git a/src/observer/object-augmentations.js b/src/observer/object-augmentations.js index afd93b3fa07..d4a9eda34b1 100644 --- a/src/observer/object-augmentations.js +++ b/src/observer/object-augmentations.js @@ -14,7 +14,7 @@ _.define(objectAgumentations, '$add', function (key, val) { if (this.hasOwnProperty(key)) return this[key] = val this.$observer.convert(key, val) - this.$observer.emit('add', key, val) + this.$observer.notify('added', key, val) }) /** @@ -30,7 +30,7 @@ _.define(objectAgumentations, '$delete', function (key) { // trigger set events this[key] = undefined delete this[key] - this.$observer.emit('delete', key) + this.$observer.notify('deleted', key) }) module.exports = objectAgumentations \ No newline at end of file diff --git a/src/observer/observer.js b/src/observer/observer.js index d89f30ae7ae..e8009b84181 100644 --- a/src/observer/observer.js +++ b/src/observer/observer.js @@ -30,10 +30,16 @@ function Observer (value, type) { Emitter.call(this) this.value = value this.type = type - this.initiated = false - this.adaptors = Object.create(null) + this.parents = null if (value) { _.define(value, '$observer', this) + if (type === ARRAY) { + _.augment(value, arrayAugmentations) + this.link(value) + } else if (type === OBJECT) { + _.augment(value, objectAugmentations) + this.walk(value) + } } } @@ -69,23 +75,6 @@ Observer.create = function (value) { } } -/** - * Initialize the observation based on value type. - * Should only be called once. - */ - -p.init = function () { - var value = this.value - if (this.type === ARRAY) { - _.augment(value, arrayAugmentations) - this.link(value) - } else if (this.type === OBJECT) { - _.augment(value, objectAugmentations) - this.walk(value) - } - this.initiated = true -} - /** * Walk through each property, converting them and adding them as child. * This method should only be called when value type is Object. @@ -103,6 +92,30 @@ p.walk = function (obj) { } } +/** + * Link a list of Array items to the observer. + * + * @param {Array} items + */ + +p.link = function (items) { + for (var i = 0, l = items.length; i < l; i++) { + this.observe(i, items[i]) + } +} + +/** + * Unlink a list of Array items from the observer. + * + * @param {Array} items + */ + +p.unlink = function (items) { + for (var i = 0, l = items.length; i < l; i++) { + this.unobserve(items[i], i) + } +} + /** * If a property is observable, * create an Observer for it and add it as a child. @@ -116,17 +129,11 @@ p.walk = function (obj) { p.observe = function (key, val) { var ob = Observer.create(val) if (ob) { - this.add(key, ob) - if (ob.initiated) { - this.deliver(key, val) - } else { - ob.init() - } - } - // emit an initial set event - this.emit('set', key, val) - if (_.isArray(val)) { - this.emit('set', key + delimiter + 'length', val.length) + // register self as a parent of the child observer. + (ob.parents || (ob.parents = [])).push({ + ob: this, + key: key + }) } } @@ -134,13 +141,22 @@ p.observe = function (key, val) { * Unobserve a property. * If it has an observer, remove it from children. * - * @param {String} key * @param {*} val */ -p.unobserve = function (key, val) { +p.unobserve = function (val) { if (val && val.$observer) { - this.remove(key, val.$observer) + var parents = val.$observer.parents + var i = parents.length + while (i--) { + if (parents[i].ob === this) { + parents.splice(i, 1) + break + } + } + if (!parents.length) { + val.$observer.parents = null + } } } @@ -163,109 +179,43 @@ p.convert = function (key, val) { enumerable: true, configurable: true, get: function () { - ob.emit('get', key) + ob.notify('get', key) return val }, set: function (newVal) { if (newVal === val) return - ob.unobserve(key, val) + ob.unobserve(val) ob.observe(key, newVal) + ob.notify('set', key, newVal) + if (_.isArray(newVal)) { + ob.notify('set', key + delimiter + 'length', newVal.length) + } val = newVal } }) } /** - * Link a list of items to the observer's value Array. - * When any of these items emit change event, the Array will be notified. - * This method should only be called when value type is Array. - * - * @param {Array} items - */ - -p.link = function (items) { - -} - -/** - * Unlink the items from the observer's value Array. - * - * @param {Array} items - */ - -p.unlink = function (items) { - -} - -/** - * Walk through an already observed object and emit its tip values. - * This is necessary because newly observed objects emit their values - * during init; for already observed ones we can skip the initialization, - * but still need to emit the values. - * - * If called with no arguments, it delivers set events for the root value. - * - * @param {String} [key] - * @param {*} [val] - */ - -p.deliver = function (key, val) { - -} - -/** - * Add a child observer for a property key, - * capture its get/set/mutate events and relay the events - * while prepending a key segment to the path. + * Emit event on self and recursively notify all parents. * - * @param {String} key - * @param {Observer} ob + * @param {String} event + * @param {String} path + * @param {*} val + * @param {Object|undefined} mutation */ -p.add = function (key, ob) { - var self = this - var base = key + delimiter - var adaptors = this.adaptors[key] = {} - - adaptors.get = function (path) { - path = base + path - self.emit('get', path) - } - - adaptors.set = function (path, val) { - path = base + path - self.emit('set', path, val) - } - - adaptors.mutate = function (path, val, mutation) { - // if path is empty string, the mutation - // comes directly from an Array - path = path - ? base + path +p.notify = function (event, path, val, mutation) { + this.emit(event, path, val, mutation) + if (!this.parents) return + for (var i = 0, l = this.parents.length; i < l; i++) { + var parent = this.parents[i] + var ob = parent.ob + var key = parent.key + var parentPath = path + ? key + delimiter + path : key - self.emit('mutate', path, val, mutation) - // also emit for length - self.emit('set', path + delimiter + 'length', val.length) + ob.notify(event, parentPath, val, mutation) } - - ob.on('get', adaptors.get) - .on('set', adaptors.set) - .on('mutate', adaptors.mutate) -} - -/** - * Remove a child observer. - * - * @param {String} key - * @param {Observer} ob - */ - -p.remove = function (key, ob) { - var adaptors = this.adaptors[key] - this.adaptors[key] = null - ob.off('get', adaptors.get) - .off('set', adaptors.set) - .off('mutate', adaptors.mutate) } module.exports = Observer \ No newline at end of file diff --git a/test/unit/observer.js b/test/unit/observer.js index d70f77adfe9..e94ebf4725e 100644 --- a/test/unit/observer.js +++ b/test/unit/observer.js @@ -1,39 +1,95 @@ var Observer = require('../../src/observer/observer') var delimiter = Observer.pathDelimiter -describe('Observer', function () { +function path (p) { + return p.replace(/\./g, '\b') +} - var obj, ob, spy +describe('Observer', function () { + var spy beforeEach(function () { - obj = { + spy = jasmine.createSpy() + }) + + it('get', function () { + var obj = { a: 1, b: { c: 2 } } - ob = Observer.create(obj) - ob.init() - spy = jasmine.createSpy() + var ob = Observer.create(obj) + ob.on('get', spy) + + var t = obj.a + expect(spy).toHaveBeenCalledWith('a', undefined, undefined) + expect(spy.callCount).toEqual(1) + + t = obj.b.c + expect(spy).toHaveBeenCalledWith('b', undefined, undefined) + expect(spy).toHaveBeenCalledWith(path('b.c'), undefined, undefined) + expect(spy.callCount).toEqual(3) }) - it('should emit set events', function () { + it('set', function () { + var obj = { + a: 1, + b: { + c: 2 + } + } + var ob = Observer.create(obj) ob.on('set', spy) + obj.a = 3 expect(spy).toHaveBeenCalledWith('a', 3, undefined) + expect(spy.callCount).toEqual(1) + obj.b.c = 4 - expect(spy).toHaveBeenCalledWith('b' + delimiter + 'c', 4, undefined) + expect(spy).toHaveBeenCalledWith(path('b.c'), 4, undefined) + expect(spy.callCount).toEqual(2) + + var newB = { c: 5 } + obj.b = newB + expect(spy).toHaveBeenCalledWith('b', newB, undefined) + expect(spy.callCount).toEqual(3) + }) + + it('array get', function () { + var obj = { + arr: [{a:1}, {a:2}] + } + var ob = Observer.create(obj) + ob.on('get', spy) + + var t = obj.arr[0].a + expect(spy).toHaveBeenCalledWith(path('arr'), undefined, undefined) + expect(spy).toHaveBeenCalledWith(path('arr.0.a'), undefined, undefined) + expect(spy.callCount).toEqual(2) + }) + + it('array set', function () { + // body... + }) + + it('array mutate', function () { + // body... + }) + + it('object.$add', function () { + // body... }) - it('should emit get events', function () { + it('object.$delete', function () { // body... }) - it('should emit mutation events on Array mutation', function () { + it('array.$set', function () { // body... }) - it('should emit ', function () { + it('array.$remove', function () { // body... }) From d1a4f852160a00be468318d6451831462fa94efc Mon Sep 17 00:00:00 2001 From: Evan You Date: Thu, 10 Jul 2014 22:26:16 -0400 Subject: [PATCH 0014/1534] comments --- src/observer/observer.js | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/src/observer/observer.js b/src/observer/observer.js index e8009b84181..9d736e70460 100644 --- a/src/observer/observer.js +++ b/src/observer/observer.js @@ -85,10 +85,11 @@ Observer.create = function (value) { p.walk = function (obj) { var key, val for (key in obj) { - if (!obj.hasOwnProperty(key)) return - val = obj[key] - this.observe(key, val) - this.convert(key, val) + if (obj.hasOwnProperty(key)) { + val = obj[key] + this.observe(key, val) + this.convert(key, val) + } } } @@ -112,15 +113,14 @@ p.link = function (items) { p.unlink = function (items) { for (var i = 0, l = items.length; i < l; i++) { - this.unobserve(items[i], i) + this.unobserve(items[i]) } } /** * If a property is observable, - * create an Observer for it and add it as a child. - * This method is called only on properties observed - * for the first time. + * create an Observer for it, and register self as + * one of its parents with the associated property key. * * @param {String} key * @param {*} val @@ -138,8 +138,8 @@ p.observe = function (key, val) { } /** - * Unobserve a property. - * If it has an observer, remove it from children. + * Unobserve a property, removing self from + * its observer's parent list. * * @param {*} val */ @@ -161,7 +161,7 @@ p.unobserve = function (val) { } /** - * Convert a tip value into getter/setter so we can emit + * Convert a property into getter/setter so we can emit * the events when the property is accessed/changed. * Properties prefixed with `$` or `_` are ignored. * From 8bcc3834a94c2b556442b0e82d7b73aa1a206c36 Mon Sep 17 00:00:00 2001 From: Evan You Date: Fri, 11 Jul 2014 01:59:28 -0400 Subject: [PATCH 0015/1534] observer array index tracking --- src/observer/array-augmentations.js | 23 ++++++-- src/observer/observer.js | 82 +++++++++++++++++++++++------ 2 files changed, 85 insertions(+), 20 deletions(-) diff --git a/src/observer/array-augmentations.js b/src/observer/array-augmentations.js index 9f300f93e1c..2e64a8d9e7d 100644 --- a/src/observer/array-augmentations.js +++ b/src/observer/array-augmentations.js @@ -24,27 +24,41 @@ var arrayAugmentations = Object.create(Array.prototype) var args = slice.call(arguments) var result = original.apply(this, args) var ob = this.$observer - var inserted, removed + var inserted, removed, index switch (method) { case 'push': + inserted = args + index = this.length - args.length + break case 'unshift': inserted = args + index = 0 break case 'pop': + removed = [result] + index = this.length + break case 'shift': removed = [result] + index = 0 break case 'splice': inserted = args.slice(2) removed = result + index = args[0] break } // link/unlink added/removed elements - if (inserted) ob.link(inserted) + if (inserted) ob.link(inserted, index) if (removed) ob.unlink(removed) + // update indices + if (method !== 'push' && method !== 'pop') { + ob.updateIndices() + } + // emit length change if (inserted || removed) { ob.notify('set', 'length', this.length) @@ -55,6 +69,7 @@ var arrayAugmentations = Object.create(Array.prototype) method : method, args : args, result : result, + index : index, inserted : inserted, removed : removed }) @@ -88,4 +103,6 @@ _.define(arrayAugmentations, '$remove', function (index) { if (index > -1) { return this.splice(index, 1)[0] } -}) \ No newline at end of file +}) + +module.exports = arrayAugmentations \ No newline at end of file diff --git a/src/observer/observer.js b/src/observer/observer.js index 9d736e70460..16228a9816d 100644 --- a/src/observer/observer.js +++ b/src/observer/observer.js @@ -53,7 +53,14 @@ var p = Observer.prototype = Object.create(Emitter.prototype) * which is much less likely to appear in property keys. */ -var delimiter = Observer.pathDelimiter = '\b' +Observer.pathDelimiter = '\b' + +/** + * Switch to globally control whether to emit get events. + * Only enabled during dependency collections. + */ + +Observer.emitGet = false /** * Attempt to create an observer instance for a value, @@ -99,9 +106,10 @@ p.walk = function (obj) { * @param {Array} items */ -p.link = function (items) { +p.link = function (items, index) { + index = index || 0 for (var i = 0, l = items.length; i < l; i++) { - this.observe(i, items[i]) + this.observe(i + index, items[i]) } } @@ -130,6 +138,7 @@ p.observe = function (key, val) { var ob = Observer.create(val) if (ob) { // register self as a parent of the child observer. + if (ob.findParent(this) > -1) return (ob.parents || (ob.parents = [])).push({ ob: this, key: key @@ -146,17 +155,7 @@ p.observe = function (key, val) { p.unobserve = function (val) { if (val && val.$observer) { - var parents = val.$observer.parents - var i = parents.length - while (i--) { - if (parents[i].ob === this) { - parents.splice(i, 1) - break - } - } - if (!parents.length) { - val.$observer.parents = null - } + val.$observer.findParent(this, true) } } @@ -179,7 +178,9 @@ p.convert = function (key, val) { enumerable: true, configurable: true, get: function () { - ob.notify('get', key) + if (Observer.emitGet) { + ob.notify('get', key) + } return val }, set: function (newVal) { @@ -188,7 +189,9 @@ p.convert = function (key, val) { ob.observe(key, newVal) ob.notify('set', key, newVal) if (_.isArray(newVal)) { - ob.notify('set', key + delimiter + 'length', newVal.length) + ob.notify('set', + key + Observer.pathDelimiter + 'length', + newVal.length) } val = newVal } @@ -212,10 +215,55 @@ p.notify = function (event, path, val, mutation) { var ob = parent.ob var key = parent.key var parentPath = path - ? key + delimiter + path + ? key + Observer.pathDelimiter + path : key ob.notify(event, parentPath, val, mutation) } } +/** + * Update child elements' parent key, + * should only be called when value type is Array. + */ + +p.updateIndices = function () { + var arr = this.value + var i = arr.length + var ob + while (i--) { + ob = arr[i] && arr[i].$observer + if (ob) { + var j = ob.findParent(this) + ob.parents[j].key = i + } + } +} + +/** + * Find a parent option object + * + * @param {Observer} parent + * @param {Boolean} [remove] - whether to remove the parent + * @return {Number} - index of parent + */ + +p.findParent = function (parent, remove) { + var parents = this.parents + if (!parents) return -1 + var i = parents.length + while (i--) { + var p = parents[i] + if (p.ob === parent) { + if (remove) { + parents.splice(i, 1) + if (!parents.length) { + this.parents = null + } + } + return i + } + } + return -1 +} + module.exports = Observer \ No newline at end of file From b9f2c1ac39b6ef5df3897c70dbce1756a07b86df Mon Sep 17 00:00:00 2001 From: Evan You Date: Fri, 11 Jul 2014 01:59:40 -0400 Subject: [PATCH 0016/1534] observer benchmarks and tests --- benchmarks/observer.js | 306 +++++++++++++++++++++++++++++++++++++++++ test/unit/observer.js | 76 +++++++--- 2 files changed, 366 insertions(+), 16 deletions(-) create mode 100644 benchmarks/observer.js diff --git a/benchmarks/observer.js b/benchmarks/observer.js new file mode 100644 index 00000000000..5fd2c6f6fff --- /dev/null +++ b/benchmarks/observer.js @@ -0,0 +1,306 @@ +var Observer = require('../src/observer/observer') +var Emitter = require('../src/emitter') +var OldObserver = require('../../vue/src/observer') +var sideEffect = 0 +var runs = 1000 +function cb () { + sideEffect++ +} + +var loadTime = getNano() + +function getNano () { + var hr = process.hrtime() + return hr[0] * 1e9 + hr[1] +} + +function now () { + return (getNano() - loadTime) / 1e6 +} + +function bench (desc, fac, run) { + var objs = [] + for (var i = 0; i < runs; i++) { + objs.push(fac(i)) + } + var s = now() + for (var i = 0; i < runs; i++) { + run(objs[i]) + } + var passed = now() - s + console.log(desc + ' - ' + (16 / (passed / runs)).toFixed(2) + ' ops/frame') +} + +bench( + 'observe (simple object) ', + function (i) { + return {a:i} + }, + function (o) { + new Observer().observe('', o) + } +) + +bench( + 'old observe (simple object) ', + function (i) { + return {a:i} + }, + function (o) { + OldObserver.observe(o, '', new Emitter()) + } +) + +bench( + 'observe (3 nested objects) ', + function (i) { + return {a:{b:{c:i}}} + }, + function (o) { + new Observer().observe('', o) + } +) + +bench( + 'old observe (3 nested objects) ', + function (i) { + return {a:{b:{c:i}}} + }, + function (o) { + OldObserver.observe(o, '', new Emitter()) + } +) + +bench( + 'observe (array, 3 objects) ', + function (i) { + return [{a:i}, {a:i+1}, {a:i+2}] + }, + function (o) { + new Observer().observe('', o) + } +) + +bench( + 'old observe (array, 3 objects) ', + function (i) { + return [{a:i}, {a:i+1}, {a:i+2}] + }, + function (o) { + OldObserver.observe(o, '', new Emitter()) + } +) + +bench( + 'observe (array, 30 objects) ', + function () { + var a = [], i = 30 + while (i--) { + a.push({a:i}) + } + return a + }, + function (o) { + new Observer().observe('', o) + } +) + +bench( + 'old observe (array, 30 objects)', + function () { + var a = [], i = 30 + while (i--) { + a.push({a:i}) + } + return a + }, + function (o) { + OldObserver.observe(o, '', new Emitter()) + } +) + +Observer.emitGet = true +OldObserver.shouldGet = true + +bench( + 'simple get ', + function () { + var a = {a:1} + var ob = new Observer() + ob.observe('', a) + ob.on('get', cb) + return a + }, + function (o) { + var v = o.a + } +) + +bench( + 'old simple get', + function () { + var a = {a:1} + var ob = new Emitter() + OldObserver.observe(a, '', ob) + ob.on('get', cb) + return a + }, + function (o) { + var v = o.a + } +) + +bench( + 'nested get ', + function () { + var a = {a:{b:{c:1}}} + var ob = new Observer() + ob.observe('', a) + ob.on('get', cb) + return a + }, + function (o) { + var v = o.a.b.c + } +) + +bench( + 'old nested get', + function () { + var a = {a:{b:{c:1}}} + var ob = new Emitter() + OldObserver.observe(a, '', ob) + ob.on('get', cb) + return a + }, + function (o) { + var v = o.a.b.c + } +) + +Observer.emitGet = false +OldObserver.shouldGet = false + +bench( + 'simple set ', + function () { + var a = {a:1} + var ob = new Observer() + ob.observe('', a) + ob.on('set', cb) + return a + }, + function (o) { + o.a = 12345 + } +) + +bench( + 'old simple set', + function () { + var a = {a:1} + var ob = new Emitter() + OldObserver.observe(a, '', ob) + ob.on('set', cb) + return a + }, + function (o) { + o.a = 12345 + } +) + +bench( + 'nested set ', + function () { + var a = {a:{b:{c:1}}} + var ob = new Observer() + ob.observe('', a) + ob.on('set', cb) + return a + }, + function (o) { + o.a.b.c = 2 + } +) + +bench( + 'old nested set', + function () { + var a = {a:{b:{c:1}}} + var ob = new Emitter() + OldObserver.observe(a, '', ob) + ob.on('set', cb) + return a + }, + function (o) { + o.a.b.c = 2 + } +) + +bench( + 'array mutation (5 objects) ', + function () { + var a = [], i = 5 + while (i--) { + a.push({a:i}) + } + var ob = new Observer() + ob.observe('', a) + ob.on('mutation', cb) + return a + }, + function (o) { + o.reverse() + } +) + +bench( + 'old array mutation (5 objects) ', + function () { + var a = [], i = 5 + while (i--) { + a.push({a:i}) + } + var ob = new Emitter() + OldObserver.observe(a, '', ob) + ob.on('mutation', cb) + return a + }, + function (o) { + o.reverse() + } +) + +bench( + 'array mutation (50 objects) ', + function () { + var a = [], i = 50 + while (i--) { + a.push({a:i}) + } + var ob = new Observer() + ob.observe('', a) + ob.on('mutation', cb) + return a + }, + function (o) { + o.reverse() + } +) + +bench( + 'old array mutation (50 objects)', + function () { + var a = [], i = 50 + while (i--) { + a.push({a:i}) + } + var ob = new Emitter() + OldObserver.observe(a, '', ob) + ob.on('mutation', cb) + return a + }, + function (o) { + o.reverse() + } +) \ No newline at end of file diff --git a/test/unit/observer.js b/test/unit/observer.js index e94ebf4725e..f753e5e011e 100644 --- a/test/unit/observer.js +++ b/test/unit/observer.js @@ -1,18 +1,18 @@ var Observer = require('../../src/observer/observer') -var delimiter = Observer.pathDelimiter - -function path (p) { - return p.replace(/\./g, '\b') -} +var u = undefined +Observer.pathDelimiter = '.' describe('Observer', function () { var spy beforeEach(function () { - spy = jasmine.createSpy() + spy = jasmine.createSpy('observer') }) it('get', function () { + + Observer.emitGet = true + var obj = { a: 1, b: { @@ -23,13 +23,15 @@ describe('Observer', function () { ob.on('get', spy) var t = obj.a - expect(spy).toHaveBeenCalledWith('a', undefined, undefined) + expect(spy).toHaveBeenCalledWith('a', u, u) expect(spy.callCount).toEqual(1) t = obj.b.c - expect(spy).toHaveBeenCalledWith('b', undefined, undefined) - expect(spy).toHaveBeenCalledWith(path('b.c'), undefined, undefined) + expect(spy).toHaveBeenCalledWith('b', u, u) + expect(spy).toHaveBeenCalledWith('b.c', u, u) expect(spy.callCount).toEqual(3) + + Observer.emitGet = false }) it('set', function () { @@ -43,20 +45,27 @@ describe('Observer', function () { ob.on('set', spy) obj.a = 3 - expect(spy).toHaveBeenCalledWith('a', 3, undefined) + expect(spy).toHaveBeenCalledWith('a', 3, u) expect(spy.callCount).toEqual(1) obj.b.c = 4 - expect(spy).toHaveBeenCalledWith(path('b.c'), 4, undefined) + expect(spy).toHaveBeenCalledWith('b.c', 4, u) expect(spy.callCount).toEqual(2) var newB = { c: 5 } obj.b = newB - expect(spy).toHaveBeenCalledWith('b', newB, undefined) + expect(spy).toHaveBeenCalledWith('b', newB, u) + expect(spy.callCount).toEqual(3) + + // same value set should not emit events + obj.a = 3 expect(spy.callCount).toEqual(3) }) it('array get', function () { + + Observer.emitGet = true + var obj = { arr: [{a:1}, {a:2}] } @@ -64,17 +73,52 @@ describe('Observer', function () { ob.on('get', spy) var t = obj.arr[0].a - expect(spy).toHaveBeenCalledWith(path('arr'), undefined, undefined) - expect(spy).toHaveBeenCalledWith(path('arr.0.a'), undefined, undefined) + expect(spy).toHaveBeenCalledWith('arr', u, u) + expect(spy).toHaveBeenCalledWith('arr.0.a', u, u) expect(spy.callCount).toEqual(2) + + Observer.emitGet = false }) it('array set', function () { - // body... + var obj = { + arr: [{a:1}, {a:2}] + } + var ob = Observer.create(obj) + ob.on('set', spy) + + obj.arr[0].a = 2 + expect(spy).toHaveBeenCalledWith('arr.0.a', 2, u) + + // set events after mutation + obj.arr.reverse() + obj.arr[0].a = 3 + expect(spy).toHaveBeenCalledWith('arr.0.a', 3, u) }) it('array mutate', function () { - // body... + var arr = [{a:1}, {a:2}] + var ob = Observer.create(arr) + + ob.on('mutate', spy) + arr.push({a:3}) + expect(spy.mostRecentCall.args[0]).toEqual('') + expect(spy.mostRecentCall.args[1]).toEqual(arr) + var mutation = spy.mostRecentCall.args[2] + expect(mutation).toBeDefined() + expect(mutation.method).toEqual('push') + expect(mutation.index).toEqual(2) + expect(mutation.inserted.length).toEqual(1) + expect(mutation.inserted[0]).toEqual(arr[2]) + }) + + it('array set after mutate', function () { + var arr = [{a:1}, {a:2}] + var ob = Observer.create(arr) + ob.on('set', spy) + arr.push({a:3}) + arr[2].a = 4 + expect(spy).toHaveBeenCalledWith('2.a', 4, u) }) it('object.$add', function () { From b6fe815777ddcbd441ab7c1a9963126ece133f93 Mon Sep 17 00:00:00 2001 From: Evan You Date: Fri, 11 Jul 2014 02:12:34 -0400 Subject: [PATCH 0017/1534] polyfill benchmark for old Vue --- benchmarks/observer.js | 45 ++++++++++++++++++++++++++++++++++++++---- 1 file changed, 41 insertions(+), 4 deletions(-) diff --git a/benchmarks/observer.js b/benchmarks/observer.js index 5fd2c6f6fff..8394f731a5a 100644 --- a/benchmarks/observer.js +++ b/benchmarks/observer.js @@ -1,3 +1,12 @@ +// polyfill window/document for old Vue +global.window = { + setTimeout: setTimeout, + console: console +} +global.document = { + documentElement: {} +} + var Observer = require('../src/observer/observer') var Emitter = require('../src/emitter') var OldObserver = require('../../vue/src/observer') @@ -238,7 +247,35 @@ bench( ) bench( - 'array mutation (5 objects) ', + 'array push ', + function () { + var a = [] + var ob = new Observer() + ob.observe('', a) + ob.on('mutation', cb) + return a + }, + function (o) { + o.push({a:1}) + } +) + +bench( + 'old array push', + function () { + var a = [] + var ob = new Emitter() + OldObserver.observe(a, '', ob) + ob.on('mutation', cb) + return a + }, + function (o) { + o.push({a:1}) + } +) + +bench( + 'array reverse (5 objects) ', function () { var a = [], i = 5 while (i--) { @@ -255,7 +292,7 @@ bench( ) bench( - 'old array mutation (5 objects) ', + 'old array reverse (5 objects) ', function () { var a = [], i = 5 while (i--) { @@ -272,7 +309,7 @@ bench( ) bench( - 'array mutation (50 objects) ', + 'array reverse (50 objects) ', function () { var a = [], i = 50 while (i--) { @@ -289,7 +326,7 @@ bench( ) bench( - 'old array mutation (50 objects)', + 'old array reverse (50 objects)', function () { var a = [], i = 50 while (i--) { From cf067ee05fdd062b757583c966dc21fea205d3a4 Mon Sep 17 00:00:00 2001 From: Evan You Date: Fri, 11 Jul 2014 02:48:04 -0400 Subject: [PATCH 0018/1534] bench task --- benchmarks/observer.js | 9 --------- bower.json | 1 + component.json | 6 +++--- src/observer/observer.js | 7 +------ tasks/bench.js | 22 ++++++++++++++++++++++ 5 files changed, 27 insertions(+), 18 deletions(-) create mode 100644 tasks/bench.js diff --git a/benchmarks/observer.js b/benchmarks/observer.js index 8394f731a5a..39bba0570e1 100644 --- a/benchmarks/observer.js +++ b/benchmarks/observer.js @@ -1,12 +1,3 @@ -// polyfill window/document for old Vue -global.window = { - setTimeout: setTimeout, - console: console -} -global.document = { - documentElement: {} -} - var Observer = require('../src/observer/observer') var Emitter = require('../src/emitter') var OldObserver = require('../../vue/src/observer') diff --git a/bower.json b/bower.json index 12a85859296..64d131170da 100644 --- a/bower.json +++ b/bower.json @@ -7,6 +7,7 @@ "license": "MIT", "ignore": [ ".*", + "benchmarks", "examples", "test", "tasks", diff --git a/component.json b/component.json index 7e16fd53264..5c8a3a953ec 100644 --- a/component.json +++ b/component.json @@ -26,14 +26,14 @@ "src/instance/dom.js", "src/instance/events.js", "src/instance/lifecycle.js", - "src/observer/array.js", - "src/observer/object.js", + "src/observer/array-augmentations.js", + "src/observer/object-augmentations.js", "src/observer/observer.js", "src/parsers/directive.js", "src/parsers/expression.js", "src/parsers/path.js", - "src/parsers/template.js", "src/parsers/text.js", + "src/template.js", "src/transition/css.js", "src/transition/js.js", "src/transition/transition.js", diff --git a/src/observer/observer.js b/src/observer/observer.js index 16228a9816d..abc25182b1c 100644 --- a/src/observer/observer.js +++ b/src/observer/observer.js @@ -254,12 +254,7 @@ p.findParent = function (parent, remove) { while (i--) { var p = parents[i] if (p.ob === parent) { - if (remove) { - parents.splice(i, 1) - if (!parents.length) { - this.parents = null - } - } + if (remove) parents.splice(i, 1) return i } } diff --git a/tasks/bench.js b/tasks/bench.js new file mode 100644 index 00000000000..844853906f2 --- /dev/null +++ b/tasks/bench.js @@ -0,0 +1,22 @@ +module.exports = function (grunt) { + grunt.registerTask('bench', function () { + + // polyfill window/document for old Vue + global.window = { + setTimeout: setTimeout, + console: console + } + global.document = { + documentElement: {} + } + + require('fs') + .readdirSync('./benchmarks') + .forEach(function (mod) { + if (mod === 'run.js') return + console.log('\n' + mod.slice(0, -3).toUpperCase() + '\n') + require('../benchmarks/' + mod) + }) + + }) +} \ No newline at end of file From 320b36c63e9652ea2d2bb62e7f1046364c8d2b89 Mon Sep 17 00:00:00 2001 From: Evan You Date: Fri, 11 Jul 2014 12:49:19 -0400 Subject: [PATCH 0019/1534] more observer tests & bench --- benchmarks/observer.js | 32 ++++- breaks.md | 1 + src/observer/array-augmentations.js | 6 +- src/observer/object-augmentations.js | 6 +- test/unit/observer.js | 194 +++++++++++++++++++++++++-- 5 files changed, 223 insertions(+), 16 deletions(-) create mode 100644 breaks.md diff --git a/benchmarks/observer.js b/benchmarks/observer.js index 39bba0570e1..537c9abe9c2 100644 --- a/benchmarks/observer.js +++ b/benchmarks/observer.js @@ -1,10 +1,10 @@ var Observer = require('../src/observer/observer') var Emitter = require('../src/emitter') var OldObserver = require('../../vue/src/observer') -var sideEffect = 0 +var sideEffect = true var runs = 1000 function cb () { - sideEffect++ + sideEffect = !sideEffect } var loadTime = getNano() @@ -237,6 +237,34 @@ bench( } ) +bench( + 'swap set ', + function () { + var a = {a:{b:{c:1}}} + var ob = new Observer() + ob.observe('', a) + ob.on('set', cb) + return a + }, + function (o) { + o.a = {b:{c:2}} + } +) + +bench( + 'old swap set ', + function () { + var a = {a:{b:{c:1}}} + var ob = new Emitter() + OldObserver.observe(a, '', ob) + ob.on('set', cb) + return a + }, + function (o) { + o.a = {b:{c:2}} + } +) + bench( 'array push ', function () { diff --git a/breaks.md b/breaks.md new file mode 100644 index 00000000000..a7366a60f70 --- /dev/null +++ b/breaks.md @@ -0,0 +1 @@ +- array.$remove now only accepts number index as argument. \ No newline at end of file diff --git a/src/observer/array-augmentations.js b/src/observer/array-augmentations.js index 2e64a8d9e7d..cd3b54b61d6 100644 --- a/src/observer/array-augmentations.js +++ b/src/observer/array-augmentations.js @@ -70,9 +70,11 @@ var arrayAugmentations = Object.create(Array.prototype) args : args, result : result, index : index, - inserted : inserted, - removed : removed + inserted : inserted || [], + removed : removed || [] }) + + return result }) }) diff --git a/src/observer/object-augmentations.js b/src/observer/object-augmentations.js index d4a9eda34b1..beae6b67143 100644 --- a/src/observer/object-augmentations.js +++ b/src/observer/object-augmentations.js @@ -13,8 +13,10 @@ var objectAgumentations = Object.create(Object.prototype) _.define(objectAgumentations, '$add', function (key, val) { if (this.hasOwnProperty(key)) return this[key] = val - this.$observer.convert(key, val) - this.$observer.notify('added', key, val) + var ob = this.$observer + ob.observe(key, val) + ob.convert(key, val) + ob.notify('added', key, val) }) /** diff --git a/test/unit/observer.js b/test/unit/observer.js index f753e5e011e..1955e1e30c6 100644 --- a/test/unit/observer.js +++ b/test/unit/observer.js @@ -1,4 +1,6 @@ var Observer = require('../../src/observer/observer') +// internal emitter has fixed 3 arguments +// so we need to fill up the assetions with undefined var u = undefined Observer.pathDelimiter = '.' @@ -52,6 +54,7 @@ describe('Observer', function () { expect(spy).toHaveBeenCalledWith('b.c', 4, u) expect(spy.callCount).toEqual(2) + // swap set var newB = { c: 5 } obj.b = newB expect(spy).toHaveBeenCalledWith('b', newB, u) @@ -96,10 +99,9 @@ describe('Observer', function () { expect(spy).toHaveBeenCalledWith('arr.0.a', 3, u) }) - it('array mutate', function () { + it('array push', function () { var arr = [{a:1}, {a:2}] var ob = Observer.create(arr) - ob.on('mutate', spy) arr.push({a:3}) expect(spy.mostRecentCall.args[0]).toEqual('') @@ -108,33 +110,205 @@ describe('Observer', function () { expect(mutation).toBeDefined() expect(mutation.method).toEqual('push') expect(mutation.index).toEqual(2) + expect(mutation.removed.length).toEqual(0) expect(mutation.inserted.length).toEqual(1) expect(mutation.inserted[0]).toEqual(arr[2]) + // test index update after mutation + ob.on('set', spy) + arr[2].a = 4 + expect(spy).toHaveBeenCalledWith('2.a', 4, u) }) - it('array set after mutate', function () { + it('array pop', function () { var arr = [{a:1}, {a:2}] + var popped = arr[1] var ob = Observer.create(arr) + ob.on('mutate', spy) + arr.pop() + expect(spy.mostRecentCall.args[0]).toEqual('') + expect(spy.mostRecentCall.args[1]).toEqual(arr) + var mutation = spy.mostRecentCall.args[2] + expect(mutation).toBeDefined() + expect(mutation.method).toEqual('pop') + expect(mutation.index).toEqual(1) + expect(mutation.inserted.length).toEqual(0) + expect(mutation.removed.length).toEqual(1) + expect(mutation.removed[0]).toEqual(popped) + }) + + it('array shift', function () { + var arr = [{a:1}, {a:2}] + var shifted = arr[0] + var ob = Observer.create(arr) + ob.on('mutate', spy) + arr.shift() + expect(spy.mostRecentCall.args[0]).toEqual('') + expect(spy.mostRecentCall.args[1]).toEqual(arr) + var mutation = spy.mostRecentCall.args[2] + expect(mutation).toBeDefined() + expect(mutation.method).toEqual('shift') + expect(mutation.index).toEqual(0) + expect(mutation.inserted.length).toEqual(0) + expect(mutation.removed.length).toEqual(1) + expect(mutation.removed[0]).toEqual(shifted) + // test index update after mutation ob.on('set', spy) - arr.push({a:3}) - arr[2].a = 4 - expect(spy).toHaveBeenCalledWith('2.a', 4, u) + arr[0].a = 4 + expect(spy).toHaveBeenCalledWith('0.a', 4, u) + }) + + it('array unshift', function () { + var arr = [{a:1}, {a:2}] + var unshifted = {a:3} + var ob = Observer.create(arr) + ob.on('mutate', spy) + arr.unshift(unshifted) + expect(spy.mostRecentCall.args[0]).toEqual('') + expect(spy.mostRecentCall.args[1]).toEqual(arr) + var mutation = spy.mostRecentCall.args[2] + expect(mutation).toBeDefined() + expect(mutation.method).toEqual('unshift') + expect(mutation.index).toEqual(0) + expect(mutation.removed.length).toEqual(0) + expect(mutation.inserted.length).toEqual(1) + expect(mutation.inserted[0]).toEqual(unshifted) + // test index update after mutation + ob.on('set', spy) + arr[1].a = 4 + expect(spy).toHaveBeenCalledWith('1.a', 4, u) + }) + + it('array splice', function () { + var arr = [{a:1}, {a:2}] + var inserted = {a:3} + var removed = arr[1] + var ob = Observer.create(arr) + ob.on('mutate', spy) + arr.splice(1, 1, inserted) + expect(spy.mostRecentCall.args[0]).toEqual('') + expect(spy.mostRecentCall.args[1]).toEqual(arr) + var mutation = spy.mostRecentCall.args[2] + expect(mutation).toBeDefined() + expect(mutation.method).toEqual('splice') + expect(mutation.index).toEqual(1) + expect(mutation.removed.length).toEqual(1) + expect(mutation.inserted.length).toEqual(1) + expect(mutation.removed[0]).toEqual(removed) + expect(mutation.inserted[0]).toEqual(inserted) + // test index update after mutation + ob.on('set', spy) + arr[1].a = 4 + expect(spy).toHaveBeenCalledWith('1.a', 4, u) + }) + + it('array sort', function () { + var arr = [{a:1}, {a:2}] + var ob = Observer.create(arr) + ob.on('mutate', spy) + arr.sort(function (a, b) { + return a.a < b.a ? 1 : -1 + }) + expect(spy.mostRecentCall.args[0]).toEqual('') + expect(spy.mostRecentCall.args[1]).toEqual(arr) + var mutation = spy.mostRecentCall.args[2] + expect(mutation).toBeDefined() + expect(mutation.method).toEqual('sort') + expect(mutation.index).toBeUndefined() + expect(mutation.removed.length).toEqual(0) + expect(mutation.inserted.length).toEqual(0) + // test index update after mutation + ob.on('set', spy) + arr[1].a = 4 + expect(spy).toHaveBeenCalledWith('1.a', 4, u) + }) + + it('array reverse', function () { + var arr = [{a:1}, {a:2}] + var ob = Observer.create(arr) + ob.on('mutate', spy) + arr.reverse() + expect(spy.mostRecentCall.args[0]).toEqual('') + expect(spy.mostRecentCall.args[1]).toEqual(arr) + var mutation = spy.mostRecentCall.args[2] + expect(mutation).toBeDefined() + expect(mutation.method).toEqual('reverse') + expect(mutation.index).toBeUndefined() + expect(mutation.removed.length).toEqual(0) + expect(mutation.inserted.length).toEqual(0) + // test index update after mutation + ob.on('set', spy) + arr[1].a = 4 + expect(spy).toHaveBeenCalledWith('1.a', 4, u) }) it('object.$add', function () { - // body... + var obj = {a:{b:1}} + var ob = Observer.create(obj) + ob.on('added', spy) + + // add event + var added = {d:2} + obj.a.$add('c', added) + expect(spy).toHaveBeenCalledWith('a.c', added, u) + + // check if added object is properly observed + ob.on('set', spy) + obj.a.c.d = 3 + expect(spy).toHaveBeenCalledWith('a.c.d', 3, u) }) it('object.$delete', function () { - // body... + var obj = {a:{b:1}} + var ob = Observer.create(obj) + ob.on('deleted', spy) + + obj.a.$delete('b') + expect(spy).toHaveBeenCalledWith('a.b', u, u) }) it('array.$set', function () { - // body... + var arr = [{a:1}, {a:2}] + var ob = Observer.create(arr) + ob.on('mutate', spy) + var inserted = {a:3} + var removed = arr[1] + arr.$set(1, inserted) + + expect(spy.mostRecentCall.args[0]).toEqual('') + expect(spy.mostRecentCall.args[1]).toEqual(arr) + var mutation = spy.mostRecentCall.args[2] + expect(mutation).toBeDefined() + expect(mutation.method).toEqual('splice') + expect(mutation.index).toEqual(1) + expect(mutation.removed.length).toEqual(1) + expect(mutation.inserted.length).toEqual(1) + expect(mutation.removed[0]).toEqual(removed) + expect(mutation.inserted[0]).toEqual(inserted) + + ob.on('set', spy) + arr[1].a = 4 + expect(spy).toHaveBeenCalledWith('1.a', 4, u) }) it('array.$remove', function () { - // body... + var arr = [{a:1}, {a:2}] + var ob = Observer.create(arr) + ob.on('mutate', spy) + var removed = arr.$remove(0) + + expect(spy.mostRecentCall.args[0]).toEqual('') + expect(spy.mostRecentCall.args[1]).toEqual(arr) + var mutation = spy.mostRecentCall.args[2] + expect(mutation).toBeDefined() + expect(mutation.method).toEqual('splice') + expect(mutation.index).toEqual(0) + expect(mutation.removed.length).toEqual(1) + expect(mutation.inserted.length).toEqual(0) + expect(mutation.removed[0]).toEqual(removed) + + ob.on('set', spy) + arr[0].a = 3 + expect(spy).toHaveBeenCalledWith('0.a', 3, u) }) }) \ No newline at end of file From ff1329b83813b8374a6cf1008561a2348742236a Mon Sep 17 00:00:00 2001 From: Evan You Date: Fri, 11 Jul 2014 14:00:53 -0400 Subject: [PATCH 0020/1534] bench tasks --- .gitignore | 3 ++- benchmarks/observer.js | 10 +++++++--- benchmarks/runner.html | 1 + gruntfile.js | 21 ++++++++++++++------- tasks/bench.js | 7 +++++-- 5 files changed, 29 insertions(+), 13 deletions(-) create mode 100644 benchmarks/runner.html diff --git a/.gitignore b/.gitignore index 98a73020a19..eb4535c1720 100644 --- a/.gitignore +++ b/.gitignore @@ -2,4 +2,5 @@ dist/vue.min.js.gz test/vue.test.js explorations node_modules -.DS_Store \ No newline at end of file +.DS_Store +benchmarks/browser.js \ No newline at end of file diff --git a/benchmarks/observer.js b/benchmarks/observer.js index 537c9abe9c2..216ac530a37 100644 --- a/benchmarks/observer.js +++ b/benchmarks/observer.js @@ -1,3 +1,5 @@ +console.log('\nObserver\n') + var Observer = require('../src/observer/observer') var Emitter = require('../src/emitter') var OldObserver = require('../../vue/src/observer') @@ -7,15 +9,17 @@ function cb () { sideEffect = !sideEffect } -var loadTime = getNano() - function getNano () { var hr = process.hrtime() return hr[0] * 1e9 + hr[1] } function now () { - return (getNano() - loadTime) / 1e6 + return process.hrtime + ? getNano() / 1e6 + : window.performence + ? window.performence.now() + : Date.now() } function bench (desc, fac, run) { diff --git a/benchmarks/runner.html b/benchmarks/runner.html new file mode 100644 index 00000000000..b5d4bf1f791 --- /dev/null +++ b/benchmarks/runner.html @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/gruntfile.js b/gruntfile.js index 4b8bc4ae41d..2cc6e229b76 100644 --- a/gruntfile.js +++ b/gruntfile.js @@ -46,22 +46,29 @@ module.exports = function (grunt) { }, browserify: { - options: { - bundleOptions: { - standalone: 'Vue' - } - }, build: { src: ['src/vue.js'], - dest: 'dist/vue.js' + dest: 'dist/vue.js', + options: { + bundleOptions: { + standalone: 'Vue' + } + } }, watch: { src: ['src/vue.js'], dest: 'dist/vue.js', options: { watch: true, - keepAlive: true + keepAlive: true, + bundleOptions: { + standalone: 'Vue' + } } + }, + bench: { + src: ['benchmarks/*.js', '!benchmarks/browser.js'], + dest: 'benchmarks/browser.js' } } diff --git a/tasks/bench.js b/tasks/bench.js index 844853906f2..38b36cda7ad 100644 --- a/tasks/bench.js +++ b/tasks/bench.js @@ -1,3 +1,7 @@ +/** + * Run benchmarks in Node + */ + module.exports = function (grunt) { grunt.registerTask('bench', function () { @@ -13,8 +17,7 @@ module.exports = function (grunt) { require('fs') .readdirSync('./benchmarks') .forEach(function (mod) { - if (mod === 'run.js') return - console.log('\n' + mod.slice(0, -3).toUpperCase() + '\n') + if (mod === 'browser.js' || mod === 'runner.html') return require('../benchmarks/' + mod) }) From bb773eba2b069024f3d76430b0f51b01dab7847e Mon Sep 17 00:00:00 2001 From: Evan You Date: Fri, 11 Jul 2014 14:06:40 -0400 Subject: [PATCH 0021/1534] bench readability --- benchmarks/observer.js | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/benchmarks/observer.js b/benchmarks/observer.js index 216ac530a37..85c37955734 100644 --- a/benchmarks/observer.js +++ b/benchmarks/observer.js @@ -46,7 +46,7 @@ bench( ) bench( - 'old observe (simple object) ', + 'observe (simple object) old ', function (i) { return {a:i} }, @@ -66,7 +66,7 @@ bench( ) bench( - 'old observe (3 nested objects) ', + 'observe (3 nested objects) old ', function (i) { return {a:{b:{c:i}}} }, @@ -86,7 +86,7 @@ bench( ) bench( - 'old observe (array, 3 objects) ', + 'observe (array, 3 objects) old ', function (i) { return [{a:i}, {a:i+1}, {a:i+2}] }, @@ -110,7 +110,7 @@ bench( ) bench( - 'old observe (array, 30 objects)', + 'observe (array, 30 objects) old', function () { var a = [], i = 30 while (i--) { @@ -141,7 +141,7 @@ bench( ) bench( - 'old simple get', + 'simple get old', function () { var a = {a:1} var ob = new Emitter() @@ -169,7 +169,7 @@ bench( ) bench( - 'old nested get', + 'nested get old', function () { var a = {a:{b:{c:1}}} var ob = new Emitter() @@ -200,7 +200,7 @@ bench( ) bench( - 'old simple set', + 'simple set old', function () { var a = {a:1} var ob = new Emitter() @@ -228,7 +228,7 @@ bench( ) bench( - 'old nested set', + 'nested set old', function () { var a = {a:{b:{c:1}}} var ob = new Emitter() @@ -256,7 +256,7 @@ bench( ) bench( - 'old swap set ', + 'swap set old ', function () { var a = {a:{b:{c:1}}} var ob = new Emitter() @@ -284,7 +284,7 @@ bench( ) bench( - 'old array push', + 'array push old', function () { var a = [] var ob = new Emitter() @@ -315,7 +315,7 @@ bench( ) bench( - 'old array reverse (5 objects) ', + 'array reverse (5 objects) old ', function () { var a = [], i = 5 while (i--) { @@ -349,7 +349,7 @@ bench( ) bench( - 'old array reverse (50 objects)', + 'array reverse (50 objects) old', function () { var a = [], i = 50 while (i--) { From dde0bc8d419b5fd947224c810254de07985af8da Mon Sep 17 00:00:00 2001 From: Evan You Date: Sat, 12 Jul 2014 18:38:08 -0400 Subject: [PATCH 0022/1534] $remove --- src/observer/array-augmentations.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/observer/array-augmentations.js b/src/observer/array-augmentations.js index cd3b54b61d6..86b9380299d 100644 --- a/src/observer/array-augmentations.js +++ b/src/observer/array-augmentations.js @@ -102,6 +102,9 @@ _.define(arrayAugmentations, '$set', function (index, val) { */ _.define(arrayAugmentations, '$remove', function (index) { + if (typeof index !== 'number') { + index = this.indexOf(index) + } if (index > -1) { return this.splice(index, 1)[0] } From d8315de3f96e92b53d84bd6c447872c55a7f25ae Mon Sep 17 00:00:00 2001 From: Evan You Date: Sat, 12 Jul 2014 18:38:35 -0400 Subject: [PATCH 0023/1534] record planned changes --- breaks.md | 1 - changes.md | 51 +++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 51 insertions(+), 1 deletion(-) delete mode 100644 breaks.md create mode 100644 changes.md diff --git a/breaks.md b/breaks.md deleted file mode 100644 index a7366a60f70..00000000000 --- a/breaks.md +++ /dev/null @@ -1 +0,0 @@ -- array.$remove now only accepts number index as argument. \ No newline at end of file diff --git a/changes.md b/changes.md new file mode 100644 index 00000000000..a879dc2b7f5 --- /dev/null +++ b/changes.md @@ -0,0 +1,51 @@ +# More flexible directive syntax + + - v-on + + + - v-style + + + - custom directive + fsef + + - v-repeat +
    +
  • +
+ +# Two Way filters + +``` html + +``` + +``` js +Vue.filter('format', { + read: function (val) { + return val + '!' + }, + write: function (val, oldVal) { + return val.match(/ok/) ? val : oldVal + } +}) +``` + +# (Experimental) Validators + +``` html + +``` + +``` js + Vue.validator('email', function (val) { + return val.match(...) + }) + // this.$validation.abc // false + // this.$valid // false +``` \ No newline at end of file From e5d12fa101a315f2946dfa022231b1ff3e4bce1e Mon Sep 17 00:00:00 2001 From: Evan You Date: Sat, 12 Jul 2014 18:48:10 -0400 Subject: [PATCH 0024/1534] ideas --- changes.md | 29 ++++++++++++++++++++++++++++- 1 file changed, 28 insertions(+), 1 deletion(-) diff --git a/changes.md b/changes.md index a879dc2b7f5..38e4d2dcb64 100644 --- a/changes.md +++ b/changes.md @@ -1,15 +1,35 @@ +# Instantiation + +Instances are no longer compiled at instantiation. Data will be observed, but no DOM compilation will happen until the new instance method `$mount` has been called. Also, when a new instance is created without `el` option, it no longers auto creates one. + +``` js +var vm = new Vue({ data: {a:1} }) // only observes the data +vm.$mount('#app') // actually compile the DOM +``` + # More flexible directive syntax - v-on + + ``` html + ``` - v-style + + ``` html + ``` - custom directive - fsef + + ``` html + fsef + ``` - v-repeat + + ``` html
+ ``` # Two Way filters @@ -48,4 +69,10 @@ Vue.filter('format', { }) // this.$validation.abc // false // this.$valid // false +``` + +# (Experimental) One time interpolations + +``` html +{{* hello }} ``` \ No newline at end of file From 2c63abe4364ccb7b4759b5d02af0c2a360475a0e Mon Sep 17 00:00:00 2001 From: Evan You Date: Sat, 12 Jul 2014 18:54:00 -0400 Subject: [PATCH 0025/1534] more ideas --- changes.md | 34 ++++++++++++++++++++++++++-------- 1 file changed, 26 insertions(+), 8 deletions(-) diff --git a/changes.md b/changes.md index 38e4d2dcb64..f62c2d5beda 100644 --- a/changes.md +++ b/changes.md @@ -1,4 +1,6 @@ -# Instantiation +If you happen to see this - note that most of these are just planned but subject to change at any moment. Feedback is welcome though. + +## Instantiation Instances are no longer compiled at instantiation. Data will be observed, but no DOM compilation will happen until the new instance method `$mount` has been called. Also, when a new instance is created without `el` option, it no longers auto creates one. @@ -7,24 +9,24 @@ var vm = new Vue({ data: {a:1} }) // only observes the data vm.$mount('#app') // actually compile the DOM ``` -# More flexible directive syntax +## More flexible directive syntax - v-on ``` html - + ``` - v-style ``` html - + ``` - custom directive ``` html - fsef + fsef ``` - v-repeat @@ -40,7 +42,7 @@ vm.$mount('#app') // actually compile the DOM ``` -# Two Way filters +## Two Way filters ``` html @@ -57,7 +59,23 @@ Vue.filter('format', { }) ``` -# (Experimental) Validators +## Block logic control + +``` html + +

{{title}}

+

{{content}}

+ +``` + +``` html + + + + +``` + +## (Experimental) Validators ``` html @@ -71,7 +89,7 @@ Vue.filter('format', { // this.$valid // false ``` -# (Experimental) One time interpolations +## (Experimental) One time interpolations ``` html {{* hello }} From 0b70e524b79a17e6a10859657decbcaa5b367b45 Mon Sep 17 00:00:00 2001 From: Evan You Date: Sun, 13 Jul 2014 01:36:41 -0400 Subject: [PATCH 0026/1534] exploring scope inehritance --- changes.md | 6 ++ explorations/inheritance.js | 108 +++++++++++++++++++++++++++ explorations/test.html | 1 + src/observer/object-augmentations.js | 5 +- src/observer/observer.js | 16 ++-- src/util.js | 59 +++++++++++---- 6 files changed, 173 insertions(+), 22 deletions(-) create mode 100644 explorations/inheritance.js create mode 100644 explorations/test.html diff --git a/changes.md b/changes.md index f62c2d5beda..740d0c8b020 100644 --- a/changes.md +++ b/changes.md @@ -75,6 +75,12 @@ Vue.filter('format', { ``` +## (Experimental) New Scope Inheritance Model + +In the previous version, nested Vue instances do not have prototypal inheritance of their data scope. Although you can access parent data properties in templates, you need to explicitly travel up the scope chain with `this.$parent` in JavaScript code or use `this.$get()` to get a property on the scope chain. The expression parser also needs to do a lot of dirty work to determine the correct scope the variables belong to. + +In the new model, we provide a scope inehritance system similar to Angular, in which you can directly access properties that exist on parent scopes. The major difference is that setting a primitive value property on a child scope WILL affect that on the parent scope! This is one of the major gotchas in Angular and it's no longer an issue in Vue, although if you are somewhat familiar with how prototype inehritance works, you might be surprised how this is possible. Well, the reason is that all data properties in Vue are getter/setters, and invoking a setter will not cause the child scope shadowing parent scopes. + ## (Experimental) Validators ``` html diff --git a/explorations/inheritance.js b/explorations/inheritance.js new file mode 100644 index 00000000000..2acdf9465f1 --- /dev/null +++ b/explorations/inheritance.js @@ -0,0 +1,108 @@ +var Observer = require('../src/observer/observer') +var _ = require('../src/util') + +function Vue (options) { + + // scope prototypal inehritance + var scope = this._scope = options.parent + ? Object.create(options.parent._scope) + : {} + + // copy instantiation data into scope + for (var key in options.data) { + if (key in scope) { + // key exists on the scope prototype chain + // cannot use direct set here, because in the parent + // scope everything is already getter/setter and we + // need to overwrite them with Object.defineProperty. + _.define(scope, key, options.data[key], true) + } else { + scope[key] = options.data[key] + } + } + + // create observer + // pass in noProto:true to avoid mutating the __proto__ + var ob = this._observer = Observer.create(this._scope, true) + + // relay change events from parent scope. + // this ensures the current Vue instance is aware of + // stuff going on up in the scope chain. + if (options.parent) { + var po = options.parent._observer + ;['set', 'mutate', 'added', 'deleted'].forEach(function (event) { + po.on(event, function (key, a, b) { + if (!scope.hasOwnProperty(key)) { + ob.emit(event, key, a, b) + } + }) + }) + } + + // proxy everything on self + for (var key in this._scope) { + _.proxy(this, this._scope, key) + } + + // also proxy newly added keys. + var self = this + ob.on('added', function (key) { + if (!self.hasOwnProperty(key)) { + _.proxy(self, scope, key) + } + }) + +} + +Vue.prototype.$add = function (key, val) { + this._scope.$add.call(this._scope, key, val) +} + +Vue.prototype.$delete = function (key) { + this._scope.$delete.call(this._scope, key) +} + +window.vm = new Vue({ + data: { + a: 'go!', + b: 2, + c: { + d: 3 + }, + arr: [{a:1}, {a:2}, {a:3}], + get hello () { + return 'hello!' + this.a + }, + go: function () { + console.log(this.a) + } + } +}) + +window.child = new Vue({ + parent: vm, + data: { + a: 1, + change: function () { + this.c.d = 4 + this.b = 456 // Unlike Angular, setting primitive values in Vue WILL affect outer scope, + // unless you overwrite it in the instantiation data! + } + } +}) + +vm._observer.on('set', function (key, val) { + console.log('vm set:' + key.replace(/[\b]/g, '.'), val) +}) + +child._observer.on('set', function (key, val) { + console.log('child set:' + key.replace(/[\b]/g, '.'), val) +}) + +vm._observer.on('mutate', function (key, val) { + console.log('vm mutate:' + key.replace(/[\b]/g, '.'), val) +}) + +child._observer.on('mutate', function (key, val) { + console.log('child mutate:' + key.replace(/[\b]/g, '.'), val) +}) \ No newline at end of file diff --git a/explorations/test.html b/explorations/test.html new file mode 100644 index 00000000000..16f50060a09 --- /dev/null +++ b/explorations/test.html @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/observer/object-augmentations.js b/src/observer/object-augmentations.js index beae6b67143..a8ee721dea5 100644 --- a/src/observer/object-augmentations.js +++ b/src/observer/object-augmentations.js @@ -12,7 +12,8 @@ var objectAgumentations = Object.create(Object.prototype) _.define(objectAgumentations, '$add', function (key, val) { if (this.hasOwnProperty(key)) return - this[key] = val + // make sure it's defined on itself. + _.define(this, key, val, true) var ob = this.$observer ob.observe(key, val) ob.convert(key, val) @@ -29,8 +30,6 @@ _.define(objectAgumentations, '$add', function (key, val) { _.define(objectAgumentations, '$delete', function (key) { if (!this.hasOwnProperty(key)) return - // trigger set events - this[key] = undefined delete this[key] this.$observer.notify('deleted', key) }) diff --git a/src/observer/observer.js b/src/observer/observer.js index abc25182b1c..2ec4d3334b4 100644 --- a/src/observer/observer.js +++ b/src/observer/observer.js @@ -26,7 +26,7 @@ var OBJECT = 1 * @param {Number} [type] */ -function Observer (value, type) { +function Observer (value, type, noProto) { Emitter.call(this) this.value = value this.type = type @@ -37,7 +37,11 @@ function Observer (value, type) { _.augment(value, arrayAugmentations) this.link(value) } else if (type === OBJECT) { - _.augment(value, objectAugmentations) + if (noProto) { + _.deepMixin(value, objectAugmentations) + } else { + _.augment(value, objectAugmentations) + } this.walk(value) } } @@ -72,13 +76,15 @@ Observer.emitGet = false * @static */ -Observer.create = function (value) { - if (value && value.$observer) { +Observer.create = function (value, noProto) { + if (value && + value.hasOwnProperty('$observer') && + value.$observer instanceof Observer) { return value.$observer } if (_.isArray(value)) { return new Observer(value, ARRAY) } else if (_.isObject(value)) { - return new Observer(value, OBJECT) + return new Observer(value, OBJECT, noProto) } } diff --git a/src/util.js b/src/util.js index cc87511fe8f..d74bb5acc4e 100644 --- a/src/util.js +++ b/src/util.js @@ -1,18 +1,53 @@ /** * Mix properties into target object. * - * @param {Object} target - * @param {Object} mixin + * @param {Object} to + * @param {Object} from */ -exports.mixin = function (target, mixin) { - for (var key in mixin) { - if (target[key] !== mixin[key]) { - target[key] = mixin[key] +exports.mixin = function (to, from) { + for (var key in from) { + if (to[key] !== from[key]) { + to[key] = from[key] } } } +/** + * Mixin including non-enumerables, and copy property descriptors. + * + * @param {Object} to + * @param {Object} from + */ + +exports.deepMixin = function (to, from) { + Object.getOwnPropertyNames(from).forEach(function (key) { + var descriptor = Object.getOwnPropertyDescriptor(from, key) + Object.defineProperty(to, key, descriptor) + }) +} + +/** + * Proxy a property on one object to another. + * + * @param {Object} to + * @param {Object} from + * @param {String} key + */ + +exports.proxy = function (to, from, key) { + Object.defineProperty(to, key, { + enumerable: true, + configurable: true, + get: function () { + return from[key] + }, + set: function (val) { + from[key] = val + } + }) +} + /** * Object type check. Only returns true * for plain JavaScript objects. @@ -42,12 +77,13 @@ exports.isArray = function (obj) { * @param {Object} obj * @param {String} key * @param {*} val + * @param {Boolean} [enumerable] */ -exports.define = function (obj, key, val) { +exports.define = function (obj, key, val, enumerable) { Object.defineProperty(obj, key, { value : val, - enumerable : false, + enumerable : !!enumerable, writable : true, configurable : true }) @@ -67,10 +103,5 @@ if ('__proto__' in {}) { target.__proto__ = proto } } else { - exports.augment = function (target, proto) { - Object.getOwnPropertyNames(proto).forEach(function (key) { - var descriptor = Object.getOwnPropertyDescriptor(proto, key) - Object.defineProperty(target, key, descriptor) - }) - } + exports.augment = exports.deepMixin } \ No newline at end of file From c4611022479c128f2d12c3ca43c0d443a34164b4 Mon Sep 17 00:00:00 2001 From: Evan You Date: Sun, 13 Jul 2014 03:11:21 -0400 Subject: [PATCH 0027/1534] more hacks... kinda works? --- changes.md | 13 +++++- explorations/inheritance.js | 85 ++++++++++++++++++++++++++++--------- src/observer/observer.js | 16 ++++--- 3 files changed, 85 insertions(+), 29 deletions(-) diff --git a/changes.md b/changes.md index 740d0c8b020..530ee0924a8 100644 --- a/changes.md +++ b/changes.md @@ -29,6 +29,15 @@ vm.$mount('#app') // actually compile the DOM fsef ``` + - v-with + + ``` html +
+ ``` + - v-repeat ``` html @@ -79,7 +88,9 @@ Vue.filter('format', { In the previous version, nested Vue instances do not have prototypal inheritance of their data scope. Although you can access parent data properties in templates, you need to explicitly travel up the scope chain with `this.$parent` in JavaScript code or use `this.$get()` to get a property on the scope chain. The expression parser also needs to do a lot of dirty work to determine the correct scope the variables belong to. -In the new model, we provide a scope inehritance system similar to Angular, in which you can directly access properties that exist on parent scopes. The major difference is that setting a primitive value property on a child scope WILL affect that on the parent scope! This is one of the major gotchas in Angular and it's no longer an issue in Vue, although if you are somewhat familiar with how prototype inehritance works, you might be surprised how this is possible. Well, the reason is that all data properties in Vue are getter/setters, and invoking a setter will not cause the child scope shadowing parent scopes. +In the new model, we provide a scope inehritance system similar to Angular, in which you can directly access properties that exist on parent scopes. The major difference is that setting a primitive value property on a child scope WILL affect that on the parent scope! This is one of the major gotchas in Angular. If you are somewhat familiar with how prototype inehritance works, you might be surprised how this is possible. Well, the reason is that all data properties in Vue are getter/setters, and invoking a setter will not cause the child scope shadowing parent scopes. See the example [here](http://jsfiddle.net/yyx990803/Px2n6/). + +This is very powerful, but probably should only be available in implicit child instances created by `v-repeat` and `v-if`. Explicit components should retain its own root scope and use some sort of two way binding like `v-with` to sync with outer scope. ## (Experimental) Validators diff --git a/explorations/inheritance.js b/explorations/inheritance.js index 2acdf9465f1..b69ce563f8d 100644 --- a/explorations/inheritance.js +++ b/explorations/inheritance.js @@ -3,27 +3,66 @@ var _ = require('../src/util') function Vue (options) { - // scope prototypal inehritance + var data = options.data var scope = this._scope = options.parent ? Object.create(options.parent._scope) : {} // copy instantiation data into scope - for (var key in options.data) { + for (var key in data) { if (key in scope) { // key exists on the scope prototype chain // cannot use direct set here, because in the parent // scope everything is already getter/setter and we // need to overwrite them with Object.defineProperty. - _.define(scope, key, options.data[key], true) + _.define(scope, key, data[key], true) } else { - scope[key] = options.data[key] + scope[key] = data[key] } } // create observer // pass in noProto:true to avoid mutating the __proto__ var ob = this._observer = Observer.create(this._scope, true) + var dob = Observer.create(data) + var locked = false + + // sync scope and original data. + ob + .on('set', guard(function (key, val) { + data[key] = val + })) + .on('added', guard(function (key, val) { + data.$add(key, val) + })) + .on('deleted', guard(function (key) { + data.$delete(key) + })) + + // also need to sync data object changes to scope... + // this would cause cycle updates, so we need to lock + // stuff when one side updates the other + dob + .on('set', guard(function (key, val) { + scope[key] = val + })) + .on('added', guard(function (key, val) { + scope.$add(key, val) + })) + .on('deleted', guard(function (key) { + scope.$delete(key) + })) + + function guard (fn) { + return function (key, val) { + if (locked || key.indexOf(Observer.pathDelimiter) > -1) { + return + } + locked = true + fn(key, val) + locked = false + } + } // relay change events from parent scope. // this ensures the current Vue instance is aware of @@ -40,13 +79,14 @@ function Vue (options) { } // proxy everything on self - for (var key in this._scope) { - _.proxy(this, this._scope, key) + for (var key in scope) { + _.proxy(this, scope, key) } // also proxy newly added keys. var self = this - ob.on('added', function (key) { + ob + .on('added', function (key) { if (!self.hasOwnProperty(key)) { _.proxy(self, scope, key) } @@ -62,21 +102,20 @@ Vue.prototype.$delete = function (key) { this._scope.$delete.call(this._scope, key) } -window.vm = new Vue({ - data: { - a: 'go!', - b: 2, - c: { - d: 3 - }, - arr: [{a:1}, {a:2}, {a:3}], - get hello () { - return 'hello!' + this.a - }, - go: function () { - console.log(this.a) - } +window.model = { + a: 'go!', + b: 2, + c: { + d: 3 + }, + arr: [{a:1}, {a:2}, {a:3}], + go: function () { + console.log(this.a) } +} + +window.vm = new Vue({ + data: model }) window.child = new Vue({ @@ -91,6 +130,10 @@ window.child = new Vue({ } }) +window.v2 = new Vue({ + data: model.arr[0] +}) + vm._observer.on('set', function (key, val) { console.log('vm set:' + key.replace(/[\b]/g, '.'), val) }) diff --git a/src/observer/observer.js b/src/observer/observer.js index 2ec4d3334b4..bbc2f134eb2 100644 --- a/src/observer/observer.js +++ b/src/observer/observer.js @@ -22,11 +22,12 @@ var OBJECT = 1 * * @constructor * @extends Emitter - * @param {Array|Object} [value] - * @param {Number} [type] + * @param {Array|Object} value + * @param {Number} type + * @param {Object} [options] */ -function Observer (value, type, noProto) { +function Observer (value, type, options) { Emitter.call(this) this.value = value this.type = type @@ -37,7 +38,7 @@ function Observer (value, type, noProto) { _.augment(value, arrayAugmentations) this.link(value) } else if (type === OBJECT) { - if (noProto) { + if (options && options.noProto) { _.deepMixin(value, objectAugmentations) } else { _.augment(value, objectAugmentations) @@ -72,19 +73,20 @@ Observer.emitGet = false * or the existing observer if the value already has one. * * @param {*} value + * @param {Object} [options] * @return {Observer|undefined} * @static */ -Observer.create = function (value, noProto) { +Observer.create = function (value, options) { if (value && value.hasOwnProperty('$observer') && value.$observer instanceof Observer) { return value.$observer } if (_.isArray(value)) { - return new Observer(value, ARRAY) + return new Observer(value, ARRAY, options) } else if (_.isObject(value)) { - return new Observer(value, OBJECT, noProto) + return new Observer(value, OBJECT, options) } } From 49ad4ca7d1572a9ea9c8111ee5c9d7b3302550d1 Mon Sep 17 00:00:00 2001 From: Evan You Date: Sun, 13 Jul 2014 11:21:31 -0400 Subject: [PATCH 0028/1534] fix inheritance test --- explorations/inheritance.js | 80 +++++++++++++++++++++++-------------- 1 file changed, 50 insertions(+), 30 deletions(-) diff --git a/explorations/inheritance.js b/explorations/inheritance.js index b69ce563f8d..94a3ccaa5d4 100644 --- a/explorations/inheritance.js +++ b/explorations/inheritance.js @@ -4,8 +4,9 @@ var _ = require('../src/util') function Vue (options) { var data = options.data - var scope = this._scope = options.parent - ? Object.create(options.parent._scope) + var parent = options.parent + var scope = this._scope = parent + ? Object.create(parent._scope) : {} // copy instantiation data into scope @@ -23,35 +24,35 @@ function Vue (options) { // create observer // pass in noProto:true to avoid mutating the __proto__ - var ob = this._observer = Observer.create(this._scope, true) + var ob = this._observer = Observer.create(scope, { noProto: true }) var dob = Observer.create(data) var locked = false // sync scope and original data. ob - .on('set', guard(function (key, val) { - data[key] = val - })) - .on('added', guard(function (key, val) { - data.$add(key, val) - })) - .on('deleted', guard(function (key) { - data.$delete(key) - })) + .on('set', guard(function (key, val) { + data[key] = val + })) + .on('added', guard(function (key, val) { + data.$add(key, val) + })) + .on('deleted', guard(function (key) { + data.$delete(key) + })) // also need to sync data object changes to scope... // this would cause cycle updates, so we need to lock // stuff when one side updates the other dob - .on('set', guard(function (key, val) { - scope[key] = val - })) - .on('added', guard(function (key, val) { - scope.$add(key, val) - })) - .on('deleted', guard(function (key) { - scope.$delete(key) - })) + .on('set', guard(function (key, val) { + scope[key] = val + })) + .on('added', guard(function (key, val) { + scope.$add(key, val) + })) + .on('deleted', guard(function (key) { + scope.$delete(key) + })) function guard (fn) { return function (key, val) { @@ -67,8 +68,8 @@ function Vue (options) { // relay change events from parent scope. // this ensures the current Vue instance is aware of // stuff going on up in the scope chain. - if (options.parent) { - var po = options.parent._observer + if (parent) { + var po = parent._observer ;['set', 'mutate', 'added', 'deleted'].forEach(function (event) { po.on(event, function (key, a, b) { if (!scope.hasOwnProperty(key)) { @@ -85,8 +86,7 @@ function Vue (options) { // also proxy newly added keys. var self = this - ob - .on('added', function (key) { + ob.on('added', function (key) { if (!self.hasOwnProperty(key)) { _.proxy(self, scope, key) } @@ -102,6 +102,17 @@ Vue.prototype.$delete = function (key) { this._scope.$delete.call(this._scope, key) } +Vue.prototype.$toJSON = function () { + return JSON.stringify(this._scope) +} + +Vue.prototype.$log = function (key) { + var data = key + ? this._scope[key] + : this._scope + console.log(JSON.parse(JSON.stringify(data))) +} + window.model = { a: 'go!', b: 2, @@ -130,7 +141,8 @@ window.child = new Vue({ } }) -window.v2 = new Vue({ +window.item = new Vue({ + parent: vm, data: model.arr[0] }) @@ -138,14 +150,22 @@ vm._observer.on('set', function (key, val) { console.log('vm set:' + key.replace(/[\b]/g, '.'), val) }) -child._observer.on('set', function (key, val) { - console.log('child set:' + key.replace(/[\b]/g, '.'), val) -}) - vm._observer.on('mutate', function (key, val) { console.log('vm mutate:' + key.replace(/[\b]/g, '.'), val) }) +child._observer.on('set', function (key, val) { + console.log('child set:' + key.replace(/[\b]/g, '.'), val) +}) + child._observer.on('mutate', function (key, val) { console.log('child mutate:' + key.replace(/[\b]/g, '.'), val) +}) + +item._observer.on('set', function (key, val) { + console.log('item set:' + key.replace(/[\b]/g, '.'), val) +}) + +item._observer.on('mutate', function (key, val) { + console.log('item mutate:' + key.replace(/[\b]/g, '.'), val) }) \ No newline at end of file From 706c67d1d013577fdbfab258bca78557419cba7c Mon Sep 17 00:00:00 2001 From: Evan You Date: Mon, 14 Jul 2014 10:43:48 -0400 Subject: [PATCH 0029/1534] restructure --- benchmarks/observer.js | 2 +- explorations/inheritance.js | 6 +- src/api/asset-register.js | 0 src/api/config.js | 0 src/{instance => api}/data.js | 16 ++++ src/{instance => api}/dom.js | 0 src/{instance => api}/events.js | 0 src/api/extend.js | 0 src/api/global.js | 61 +++++++++++++ src/{instance => api}/lifecycle.js | 0 src/api/require.js | 0 src/api/use.js | 0 src/config.js | 12 ++- .../compiler.js => internal/compile.js} | 0 src/internal/init.js | 90 +++++++++++++++++++ .../array-augmentations.js | 0 .../object-augmentations.js | 0 src/{observer => observe}/observer.js | 2 +- src/{parsers => parse}/directive.js | 0 src/{parsers => parse}/expression.js | 0 src/{parsers => parse}/path.js | 0 src/{parsers => parse}/text.js | 0 src/util.js | 1 + src/vue.js | 31 +++---- 24 files changed, 197 insertions(+), 24 deletions(-) delete mode 100644 src/api/asset-register.js delete mode 100644 src/api/config.js rename src/{instance => api}/data.js (51%) rename src/{instance => api}/dom.js (100%) rename src/{instance => api}/events.js (100%) delete mode 100644 src/api/extend.js create mode 100644 src/api/global.js rename src/{instance => api}/lifecycle.js (100%) delete mode 100644 src/api/require.js delete mode 100644 src/api/use.js rename src/{compiler/compiler.js => internal/compile.js} (100%) create mode 100644 src/internal/init.js rename src/{observer => observe}/array-augmentations.js (100%) rename src/{observer => observe}/object-augmentations.js (100%) rename src/{observer => observe}/observer.js (98%) rename src/{parsers => parse}/directive.js (100%) rename src/{parsers => parse}/expression.js (100%) rename src/{parsers => parse}/path.js (100%) rename src/{parsers => parse}/text.js (100%) diff --git a/benchmarks/observer.js b/benchmarks/observer.js index 85c37955734..bd307765cdf 100644 --- a/benchmarks/observer.js +++ b/benchmarks/observer.js @@ -1,6 +1,6 @@ console.log('\nObserver\n') -var Observer = require('../src/observer/observer') +var Observer = require('../src/observe/observer') var Emitter = require('../src/emitter') var OldObserver = require('../../vue/src/observer') var sideEffect = true diff --git a/explorations/inheritance.js b/explorations/inheritance.js index 94a3ccaa5d4..0c79f75c00e 100644 --- a/explorations/inheritance.js +++ b/explorations/inheritance.js @@ -1,4 +1,4 @@ -var Observer = require('../src/observer/observer') +var Observer = require('../src/observe/observer') var _ = require('../src/util') function Vue (options) { @@ -87,9 +87,7 @@ function Vue (options) { // also proxy newly added keys. var self = this ob.on('added', function (key) { - if (!self.hasOwnProperty(key)) { - _.proxy(self, scope, key) - } + _.proxy(self, scope, key) }) } diff --git a/src/api/asset-register.js b/src/api/asset-register.js deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/src/api/config.js b/src/api/config.js deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/src/instance/data.js b/src/api/data.js similarity index 51% rename from src/instance/data.js rename to src/api/data.js index 9a3d64edcfe..9d4c0376726 100644 --- a/src/instance/data.js +++ b/src/api/data.js @@ -6,10 +6,26 @@ exports.$set = function () { } +exports.$add = function () { + +} + +exports.$delete = function () { + +} + exports.$watch = function () { } exports.$unwatch = function () { +} + +exports.$toJSON = function () { + +} + +exports.$log = function () { + } \ No newline at end of file diff --git a/src/instance/dom.js b/src/api/dom.js similarity index 100% rename from src/instance/dom.js rename to src/api/dom.js diff --git a/src/instance/events.js b/src/api/events.js similarity index 100% rename from src/instance/events.js rename to src/api/events.js diff --git a/src/api/extend.js b/src/api/extend.js deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/src/api/global.js b/src/api/global.js new file mode 100644 index 00000000000..167ef520ca6 --- /dev/null +++ b/src/api/global.js @@ -0,0 +1,61 @@ +var _ = require('../util') +var config = require('../config') + +/** + * Configuration + */ + +exports.config = function () { + +} + +/** + * Class inehritance + */ + +exports.extend = function () { + +} + +/** + * Plugin system + */ + +exports.use = function () { + +} + +/** + * Expose some internal utilities + */ + +exports.require = function () { + +} + +/** + * Define asset registries and registration + * methods on a constructor. + */ + +config.assetTypes.forEach(function (type) { + var registry = '_' + type + 's' + exports[registry] = {} + + /** + * Asset registration method. + * + * @param {String} id + * @param {*} definition + */ + + exports[type] = function (id, definition) { + this[registry][id] = definition + } +}) + +/** + * This is pretty useful so we expose it as a global method. + */ + +exports.nextTick = _.nextTick \ No newline at end of file diff --git a/src/instance/lifecycle.js b/src/api/lifecycle.js similarity index 100% rename from src/instance/lifecycle.js rename to src/api/lifecycle.js diff --git a/src/api/require.js b/src/api/require.js deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/src/api/use.js b/src/api/use.js deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/src/config.js b/src/config.js index 7c6d6c73d3d..4d4591e64d5 100644 --- a/src/config.js +++ b/src/config.js @@ -1 +1,11 @@ -module.exports = {} \ No newline at end of file +module.exports = { + + assetTypes: [ + 'directive', + 'filter', + 'partial', + 'effect', + 'component' + ] + +} \ No newline at end of file diff --git a/src/compiler/compiler.js b/src/internal/compile.js similarity index 100% rename from src/compiler/compiler.js rename to src/internal/compile.js diff --git a/src/internal/init.js b/src/internal/init.js new file mode 100644 index 00000000000..8583f7aaf83 --- /dev/null +++ b/src/internal/init.js @@ -0,0 +1,90 @@ +exports._init = function (options) { + + var data = options.data + var parent = options.parent + var scope = this._scope = parent + ? Object.create(parent._scope) + : {} + + // copy instantiation data into scope + for (var key in data) { + if (key in scope) { + // key exists on the scope prototype chain + // cannot use direct set here, because in the parent + // scope everything is already getter/setter and we + // need to overwrite them with Object.defineProperty. + _.define(scope, key, data[key], true) + } else { + scope[key] = data[key] + } + } + + // create observer + // pass in noProto:true to avoid mutating the __proto__ + var ob = this._observer = Observer.create(scope, { noProto: true }) + var dob = Observer.create(data) + var locked = false + + // sync scope and original data. + ob + .on('set', guard(function (key, val) { + data[key] = val + })) + .on('added', guard(function (key, val) { + data.$add(key, val) + })) + .on('deleted', guard(function (key) { + data.$delete(key) + })) + + // also need to sync data object changes to scope... + // this would cause cycle updates, so we need to lock + // stuff when one side updates the other + dob + .on('set', guard(function (key, val) { + scope[key] = val + })) + .on('added', guard(function (key, val) { + scope.$add(key, val) + })) + .on('deleted', guard(function (key) { + scope.$delete(key) + })) + + function guard (fn) { + return function (key, val) { + if (locked || key.indexOf(Observer.pathDelimiter) > -1) { + return + } + locked = true + fn(key, val) + locked = false + } + } + + // relay change events from parent scope. + // this ensures the current Vue instance is aware of + // stuff going on up in the scope chain. + if (parent) { + var po = parent._observer + ;['set', 'mutate', 'added', 'deleted'].forEach(function (event) { + po.on(event, function (key, a, b) { + if (!scope.hasOwnProperty(key)) { + ob.emit(event, key, a, b) + } + }) + }) + } + + // proxy everything on self + for (var key in scope) { + _.proxy(this, scope, key) + } + + // also proxy newly added keys. + var self = this + ob.on('added', function (key) { + _.proxy(self, scope, key) + }) + +} \ No newline at end of file diff --git a/src/observer/array-augmentations.js b/src/observe/array-augmentations.js similarity index 100% rename from src/observer/array-augmentations.js rename to src/observe/array-augmentations.js diff --git a/src/observer/object-augmentations.js b/src/observe/object-augmentations.js similarity index 100% rename from src/observer/object-augmentations.js rename to src/observe/object-augmentations.js diff --git a/src/observer/observer.js b/src/observe/observer.js similarity index 98% rename from src/observer/observer.js rename to src/observe/observer.js index bbc2f134eb2..d3be10de248 100644 --- a/src/observer/observer.js +++ b/src/observe/observer.js @@ -85,7 +85,7 @@ Observer.create = function (value, options) { return value.$observer } if (_.isArray(value)) { return new Observer(value, ARRAY, options) - } else if (_.isObject(value)) { + } else if (_.isObject(value) && !value._scope) { // avoid Vue instance return new Observer(value, OBJECT, options) } } diff --git a/src/parsers/directive.js b/src/parse/directive.js similarity index 100% rename from src/parsers/directive.js rename to src/parse/directive.js diff --git a/src/parsers/expression.js b/src/parse/expression.js similarity index 100% rename from src/parsers/expression.js rename to src/parse/expression.js diff --git a/src/parsers/path.js b/src/parse/path.js similarity index 100% rename from src/parsers/path.js rename to src/parse/path.js diff --git a/src/parsers/text.js b/src/parse/text.js similarity index 100% rename from src/parsers/text.js rename to src/parse/text.js diff --git a/src/util.js b/src/util.js index d74bb5acc4e..8ce5b4e70ec 100644 --- a/src/util.js +++ b/src/util.js @@ -36,6 +36,7 @@ exports.deepMixin = function (to, from) { */ exports.proxy = function (to, from, key) { + if (to.hasOwnProperty(key)) return Object.defineProperty(to, key, { enumerable: true, configurable: true, diff --git a/src/vue.js b/src/vue.js index 3b139cb0443..d56d4d910fc 100644 --- a/src/vue.js +++ b/src/vue.js @@ -1,5 +1,4 @@ -var _ = require('./util') -var Compiler = require('./compiler/compiler') +var _ = require('./util') /** * The exposed Vue constructor. @@ -10,33 +9,31 @@ var Compiler = require('./compiler/compiler') */ function Vue (options) { - this._compiler = new Compiler(this, options) + this._init(options) } +var p = Vue.prototype + /** - * Mixin instance methods + * Mixin internal instance methods */ -var p = Vue.prototype -_.mixin(p, require('./instance/lifecycle')) -_.mixin(p, require('./instance/data')) -_.mixin(p, require('./instance/dom')) -_.mixin(p, require('./instance/events')) + _.mixin(p, require('./internal/init')) + _.mixin(p, require('./internal/compile')) /** - * Mixin asset registers + * Mixin API instance methods */ -_.mixin(Vue, require('./api/asset-register')) +_.mixin(p, require('./api/data')) +_.mixin(p, require('./api/dom')) +_.mixin(p, require('./api/events')) +_.mixin(p, require('./api/lifecycle')) /** - * Static methods + * Mixin global API */ -Vue.config = require('./api/config') -Vue.use = require('./api/use') -Vue.require = require('./api/require') -Vue.extend = require('./api/extend') -Vue.nextTick = require('./util').nextTick +_.mixin(Vue, require('./api/global')) module.exports = Vue \ No newline at end of file From e9ecdfe1c0f695396ce790f9637787239d180804 Mon Sep 17 00:00:00 2001 From: Evan You Date: Mon, 14 Jul 2014 17:42:55 -0400 Subject: [PATCH 0030/1534] work on instantiation with scope inheritance --- benchmarks/instantiation.js | 81 +++++++++ explorations/inheritance.js | 158 ++++-------------- src/internal/compile.js | 12 +- src/internal/init.js | 250 +++++++++++++++++++++------- src/internal/properties.js | 34 ++++ src/observe/array-augmentations.js | 4 +- src/observe/object-augmentations.js | 7 +- src/observe/observer.js | 25 +-- src/vue.js | 11 ++ tasks/bench.js | 20 ++- 10 files changed, 384 insertions(+), 218 deletions(-) create mode 100644 benchmarks/instantiation.js create mode 100644 src/internal/properties.js diff --git a/benchmarks/instantiation.js b/benchmarks/instantiation.js new file mode 100644 index 00000000000..0c689bcb309 --- /dev/null +++ b/benchmarks/instantiation.js @@ -0,0 +1,81 @@ +console.log('\nInstantiation\n') + +var Vue = require('../src/vue') +var sideEffect = null +var parent = new Vue({ + data: { a: 1 } +}) + +function getNano () { + var hr = process.hrtime() + return hr[0] * 1e9 + hr[1] +} + +function now () { + return process.hrtime + ? getNano() / 1e6 + : window.performence + ? window.performence.now() + : Date.now() +} + +// warm up +for (var i = 0; i < 1000; i++) { + sideEffect = new Vue() +} + +function bench (desc, n, fn) { + var s = now() + for (var i = 0; i < n; i++) { + fn() + } + var time = now() - s + var opf = (16 / (time / n)).toFixed(2) + console.log(desc + ' ' + n + ' times - ' + opf + ' ops/frame') +} + +function simpleInstance () { + sideEffect = new Vue({ + data: {a: 1} + }) +} + +function simpleInstanceWithInheritance () { + sideEffect = new Vue({ + parent: parent, + data: { b:2 } + }) +} + +function complexInstance () { + sideEffect = new Vue({ + data: { + a: { + b: { + c: 1 + } + }, + c: { + b: { + c: { a:1 }, + d: 2, + e: 3, + d: 4 + } + }, + e: [{a:1}, {a:2}, {a:3}] + } + }) +} + +bench('Simple instance', 10, simpleInstance) +bench('Simple instance', 100, simpleInstance) +bench('Simple instance', 1000, simpleInstance) + +bench('Simple instance with inheritance', 10, simpleInstanceWithInheritance) +bench('Simple instance with inheritance', 100, simpleInstanceWithInheritance) +bench('Simple instance with inheritance', 1000, simpleInstanceWithInheritance) + +bench('Complex instance', 10, complexInstance) +bench('Complex instance', 100, complexInstance) +bench('Complex instance', 1000, complexInstance) \ No newline at end of file diff --git a/explorations/inheritance.js b/explorations/inheritance.js index 0c79f75c00e..d6a28a83fba 100644 --- a/explorations/inheritance.js +++ b/explorations/inheritance.js @@ -1,123 +1,12 @@ -var Observer = require('../src/observe/observer') -var _ = require('../src/util') - -function Vue (options) { - - var data = options.data - var parent = options.parent - var scope = this._scope = parent - ? Object.create(parent._scope) - : {} - - // copy instantiation data into scope - for (var key in data) { - if (key in scope) { - // key exists on the scope prototype chain - // cannot use direct set here, because in the parent - // scope everything is already getter/setter and we - // need to overwrite them with Object.defineProperty. - _.define(scope, key, data[key], true) - } else { - scope[key] = data[key] - } - } - - // create observer - // pass in noProto:true to avoid mutating the __proto__ - var ob = this._observer = Observer.create(scope, { noProto: true }) - var dob = Observer.create(data) - var locked = false - - // sync scope and original data. - ob - .on('set', guard(function (key, val) { - data[key] = val - })) - .on('added', guard(function (key, val) { - data.$add(key, val) - })) - .on('deleted', guard(function (key) { - data.$delete(key) - })) - - // also need to sync data object changes to scope... - // this would cause cycle updates, so we need to lock - // stuff when one side updates the other - dob - .on('set', guard(function (key, val) { - scope[key] = val - })) - .on('added', guard(function (key, val) { - scope.$add(key, val) - })) - .on('deleted', guard(function (key) { - scope.$delete(key) - })) - - function guard (fn) { - return function (key, val) { - if (locked || key.indexOf(Observer.pathDelimiter) > -1) { - return - } - locked = true - fn(key, val) - locked = false - } - } - - // relay change events from parent scope. - // this ensures the current Vue instance is aware of - // stuff going on up in the scope chain. - if (parent) { - var po = parent._observer - ;['set', 'mutate', 'added', 'deleted'].forEach(function (event) { - po.on(event, function (key, a, b) { - if (!scope.hasOwnProperty(key)) { - ob.emit(event, key, a, b) - } - }) - }) - } - - // proxy everything on self - for (var key in scope) { - _.proxy(this, scope, key) - } - - // also proxy newly added keys. - var self = this - ob.on('added', function (key) { - _.proxy(self, scope, key) - }) - -} - -Vue.prototype.$add = function (key, val) { - this._scope.$add.call(this._scope, key, val) -} - -Vue.prototype.$delete = function (key) { - this._scope.$delete.call(this._scope, key) -} - -Vue.prototype.$toJSON = function () { - return JSON.stringify(this._scope) -} - -Vue.prototype.$log = function (key) { - var data = key - ? this._scope[key] - : this._scope - console.log(JSON.parse(JSON.stringify(data))) -} +var Vue = require('../src/vue') window.model = { - a: 'go!', - b: 2, + a: 'parent a', + b: 'parent b', c: { d: 3 }, - arr: [{a:1}, {a:2}, {a:3}], + arr: [{a: 'item a'}], go: function () { console.log(this.a) } @@ -130,7 +19,7 @@ window.vm = new Vue({ window.child = new Vue({ parent: vm, data: { - a: 1, + a: 'child a', change: function () { this.c.d = 4 this.b = 456 // Unlike Angular, setting primitive values in Vue WILL affect outer scope, @@ -141,29 +30,42 @@ window.child = new Vue({ window.item = new Vue({ parent: vm, - data: model.arr[0] + data: vm.arr[0] }) vm._observer.on('set', function (key, val) { console.log('vm set:' + key.replace(/[\b]/g, '.'), val) }) -vm._observer.on('mutate', function (key, val) { - console.log('vm mutate:' + key.replace(/[\b]/g, '.'), val) -}) - child._observer.on('set', function (key, val) { console.log('child set:' + key.replace(/[\b]/g, '.'), val) }) -child._observer.on('mutate', function (key, val) { - console.log('child mutate:' + key.replace(/[\b]/g, '.'), val) -}) - item._observer.on('set', function (key, val) { console.log('item set:' + key.replace(/[\b]/g, '.'), val) }) -item._observer.on('mutate', function (key, val) { - console.log('item mutate:' + key.replace(/[\b]/g, '.'), val) -}) \ No newline at end of file +// TODO turn these into tests + +console.log(vm.a) // 'parent a' +console.log(child.a) // 'child a' +console.log(child.b) // 'parent b' +console.log(item.a) // 'item a' +console.log(item.b) // 'parent b' + +// set shadowed parent property +vm.a = 'haha!' // vm set:a haha! + +// set shadowed child property +child.a = 'hmm' // child set:a hmm + +// test parent scope change downward propagation +vm.b = 'hoho!' // child set:b hoho! + // item set:b hoho! + // vm set:b hoho! + +// set child owning an array item +item.a = 'wow' // child set:arr.0.a wow + // item set:arr.0.a wow + // vm set:arr.0.a wow + // item set:a wow \ No newline at end of file diff --git a/src/internal/compile.js b/src/internal/compile.js index 567ab828e24..a30fa1d02b2 100644 --- a/src/internal/compile.js +++ b/src/internal/compile.js @@ -1,5 +1,9 @@ -function Compiler () { - -} +/** + * Start compilation of an instance. + * + * @private + */ -module.exports = Compiler \ No newline at end of file +exports._compile = function () { + +} \ No newline at end of file diff --git a/src/internal/init.js b/src/internal/init.js index 8583f7aaf83..b994542b0ea 100644 --- a/src/internal/init.js +++ b/src/internal/init.js @@ -1,90 +1,212 @@ +var _ = require('../util') +var Observer = require('../observe/observer') +var scopeEvents = ['set', 'mutate', 'added', 'deleted', 'added:self', 'deleted:self'] + +/** + * Kick off the initialization process on instance creation. + * + * @param {Object} options + * @private + */ + exports._init = function (options) { + this.$options = options = options || {} + // create scope + this._initScope(options) + // setup initial data. + this._initData(options.data || {}, true) + // setup property proxying + this._initProxy() +} + +/** + * Setup scope and listen to parent scope changes. + * Only called once during _init(). + */ - var data = options.data - var parent = options.parent - var scope = this._scope = parent +exports._initScope = function (options) { + + var parent = this.$parent = options.parent + var scope = this._scope = parent && options._inheritScope !== false ? Object.create(parent._scope) : {} + // create scope observer + this._observer = Observer.create(scope, { + callbackContext: this, + doNotAlterProto: true + }) + + if (!parent) return + + // relay change events that sent down from + // the scope prototype chain. + var ob = this._observer + var pob = parent._observer + var listeners = this._scopeListeners = {} + scopeEvents.forEach(function (event) { + var cb = listeners[event] = function (key, a, b) { + // since these events come from upstream, + // we only emit them if we don't have the same keys + // shadowing them in current scope. + if (!scope.hasOwnProperty(key)) { + ob.emit(event, key, a, b) + } + } + pob.on(event, cb) + }) +} + +/** + * Teardown scope and remove listeners attached to parent scope. + * Only called once during $destroy(). + */ + +exports._teardownScope = function () { + this._scope = null + if (!this.$parent) return + var pob = this.$parent._observer + var listeners = this._scopeListeners + scopeEvents.forEach(function (event) { + pob.off(event, listeners[event]) + }) +} + +/** + * Set the instances data object. Teasdown previous data + * object if necessary, and setup syncing between the scope + * and the data object. + * + * @param {Object} data + * @param {Boolean} init + */ + +exports._initData = function (data, init) { + var scope = this._scope + + if (!init) { + // teardown old sync listeners + this._unsync() + // delete keys not present in the new data + for (var key in scope) { + if (scope.hasOwnProperty(key) && !(key in data)) { + scope.$delete(key) + } + } + } // copy instantiation data into scope for (var key in data) { - if (key in scope) { - // key exists on the scope prototype chain - // cannot use direct set here, because in the parent - // scope everything is already getter/setter and we - // need to overwrite them with Object.defineProperty. - _.define(scope, key, data[key], true) - } else { + if (scope.hasOwnProperty(key)) { + // existing property, trigger set scope[key] = data[key] + } else { + // new property + scope.$add(key, data[key]) } } - // create observer - // pass in noProto:true to avoid mutating the __proto__ - var ob = this._observer = Observer.create(scope, { noProto: true }) - var dob = Observer.create(data) + // sync scope and new data + this._data = data + this._dataObserver = Observer.create(data) + this._sync() +} + +/** + * Proxy the scope properties on the instance itself. + * So that vm.a === vm._scope.a + */ + +exports._initProxy = function () { + // proxy every scope property on the instance itself + var scope = this._scope + for (var key in scope) { + _.proxy(this, scope, key) + } + // keep proxying up-to-date with added/deleted keys. + this._observer + .on('added:self', function (key) { + _.proxy(this, scope, key) + }) + .on('deleted:self', function (key) { + delete this[key] + }) +} + +/** + * Setup two-way sync between the instance scope and + * the original data. Requires teardown. + */ + +exports._sync = function () { + var data = this._data + var scope = this._scope var locked = false + var listeners = this._syncListeners = { + data: { + set: guard(function (key, val) { + data[key] = val + }), + added: guard(function (key, val) { + data.$add(key, val) + }), + deleted: guard(function (key) { + data.$delete(key) + }) + }, + scope: { + set: guard(function (key, val) { + scope[key] = val + }), + added: guard(function (key, val) { + scope.$add(key, val) + }), + deleted: guard(function (key) { + scope.$delete(key) + }) + } + } + // sync scope and original data. - ob - .on('set', guard(function (key, val) { - data[key] = val - })) - .on('added', guard(function (key, val) { - data.$add(key, val) - })) - .on('deleted', guard(function (key) { - data.$delete(key) - })) - - // also need to sync data object changes to scope... - // this would cause cycle updates, so we need to lock - // stuff when one side updates the other - dob - .on('set', guard(function (key, val) { - scope[key] = val - })) - .on('added', guard(function (key, val) { - scope.$add(key, val) - })) - .on('deleted', guard(function (key) { - scope.$delete(key) - })) + this._observer + .on('set:self', listeners.data.set) + .on('added:self', listeners.data.added) + .on('deleted:self', listeners.data.deleted) + + this._dataObserver + .on('set:self', listeners.scope.set) + .on('added:self', listeners.scope.added) + .on('deleted:self', listeners.scope.delted) + + /** + * The guard function prevents infinite loop + * when syncing between two observers. + */ function guard (fn) { return function (key, val) { - if (locked || key.indexOf(Observer.pathDelimiter) > -1) { - return - } + if (locked) return locked = true fn(key, val) locked = false } } +} - // relay change events from parent scope. - // this ensures the current Vue instance is aware of - // stuff going on up in the scope chain. - if (parent) { - var po = parent._observer - ;['set', 'mutate', 'added', 'deleted'].forEach(function (event) { - po.on(event, function (key, a, b) { - if (!scope.hasOwnProperty(key)) { - ob.emit(event, key, a, b) - } - }) - }) - } +/** + * Teardown the sync between scope and previous data object. + */ - // proxy everything on self - for (var key in scope) { - _.proxy(this, scope, key) - } +exports._unsync = function () { + var listeners = this._syncListeners - // also proxy newly added keys. - var self = this - ob.on('added', function (key) { - _.proxy(self, scope, key) - }) - + this._observer + .off('set:self', listeners.data.set) + .off('added:self', listeners.data.added) + .off('deleted:self', listeners.data.deleted) + + this._dataObserver + .off('set:self', listeners.scope.set) + .off('added:self', listeners.scope.added) + .off('deleted:self', listeners.scope.delted) } \ No newline at end of file diff --git a/src/internal/properties.js b/src/internal/properties.js new file mode 100644 index 00000000000..c302bfd613c --- /dev/null +++ b/src/internal/properties.js @@ -0,0 +1,34 @@ +/** + * Prototype properties on every Vue instance. + */ + +module.exports = function (p) { + + /** + * The $root recursively points to the root instance. + * + * @readonly + */ + + Object.defineProperty(p, '$root', { + get: function () { + return this.$parent + ? this.$parent.$root + : this + } + }) + + /** + * $data has a setter which does a bunch of teardown/setup work + */ + + Object.defineProperty(p, '$data', { + get: function () { + return this._data + }, + set: function (newData) { + this._initData(newData) + } + }) + +} \ No newline at end of file diff --git a/src/observe/array-augmentations.js b/src/observe/array-augmentations.js index 86b9380299d..c4458e6b7c1 100644 --- a/src/observe/array-augmentations.js +++ b/src/observe/array-augmentations.js @@ -61,11 +61,11 @@ var arrayAugmentations = Object.create(Array.prototype) // emit length change if (inserted || removed) { - ob.notify('set', 'length', this.length) + ob.propagate('set', 'length', this.length) } // empty path, value is the Array itself - ob.notify('mutate', '', this, { + ob.propagate('mutate', '', this, { method : method, args : args, result : result, diff --git a/src/observe/object-augmentations.js b/src/observe/object-augmentations.js index a8ee721dea5..d107b7f8d2b 100644 --- a/src/observe/object-augmentations.js +++ b/src/observe/object-augmentations.js @@ -17,7 +17,8 @@ _.define(objectAgumentations, '$add', function (key, val) { var ob = this.$observer ob.observe(key, val) ob.convert(key, val) - ob.notify('added', key, val) + ob.emit('added:self', key, val) + ob.propagate('added', key, val) }) /** @@ -31,7 +32,9 @@ _.define(objectAgumentations, '$add', function (key, val) { _.define(objectAgumentations, '$delete', function (key) { if (!this.hasOwnProperty(key)) return delete this[key] - this.$observer.notify('deleted', key) + var ob = this.$observer + ob.emit('deleted:self', key) + ob.propagate('deleted', key) }) module.exports = objectAgumentations \ No newline at end of file diff --git a/src/observe/observer.js b/src/observe/observer.js index d3be10de248..d94d0a02734 100644 --- a/src/observe/observer.js +++ b/src/observe/observer.js @@ -25,10 +25,12 @@ var OBJECT = 1 * @param {Array|Object} value * @param {Number} type * @param {Object} [options] + * - doNotAlterProto: if true, do not alter object's __proto__ + * - callbackContext: `this` context for callbacks */ function Observer (value, type, options) { - Emitter.call(this) + Emitter.call(this, options && options.callbackContext) this.value = value this.type = type this.parents = null @@ -38,7 +40,7 @@ function Observer (value, type, options) { _.augment(value, arrayAugmentations) this.link(value) } else if (type === OBJECT) { - if (options && options.noProto) { + if (options && options.doNotAlterProto) { _.deepMixin(value, objectAugmentations) } else { _.augment(value, objectAugmentations) @@ -73,7 +75,7 @@ Observer.emitGet = false * or the existing observer if the value already has one. * * @param {*} value - * @param {Object} [options] + * @param {Object} [options] - see the Observer constructor. * @return {Observer|undefined} * @static */ @@ -187,7 +189,7 @@ p.convert = function (key, val) { configurable: true, get: function () { if (Observer.emitGet) { - ob.notify('get', key) + ob.propagate('get', key) } return val }, @@ -195,11 +197,12 @@ p.convert = function (key, val) { if (newVal === val) return ob.unobserve(val) ob.observe(key, newVal) - ob.notify('set', key, newVal) + ob.emit('set:self', key, newVal) + ob.propagate('set', key, newVal) if (_.isArray(newVal)) { - ob.notify('set', - key + Observer.pathDelimiter + 'length', - newVal.length) + ob.propagate('set', + key + Observer.pathDelimiter + 'length', + newVal.length) } val = newVal } @@ -207,7 +210,7 @@ p.convert = function (key, val) { } /** - * Emit event on self and recursively notify all parents. + * Emit event on self and recursively propagate all parents. * * @param {String} event * @param {String} path @@ -215,7 +218,7 @@ p.convert = function (key, val) { * @param {Object|undefined} mutation */ -p.notify = function (event, path, val, mutation) { +p.propagate = function (event, path, val, mutation) { this.emit(event, path, val, mutation) if (!this.parents) return for (var i = 0, l = this.parents.length; i < l; i++) { @@ -225,7 +228,7 @@ p.notify = function (event, path, val, mutation) { var parentPath = path ? key + Observer.pathDelimiter + path : key - ob.notify(event, parentPath, val, mutation) + ob.propagate(event, parentPath, val, mutation) } } diff --git a/src/vue.js b/src/vue.js index d56d4d910fc..c41900c2e55 100644 --- a/src/vue.js +++ b/src/vue.js @@ -3,6 +3,11 @@ var _ = require('./util') /** * The exposed Vue constructor. * + * API conventions: + * - public API methods/properties are prefiexed with `$` + * - internal methods/properties are prefixed with `_` + * - non-prefixed properties are assumed to be proxied user data. + * * @constructor * @param {Object} [options] * @public @@ -14,6 +19,12 @@ function Vue (options) { var p = Vue.prototype +/** + * Define prototype properties + */ + +require('./internal/properties')(p) + /** * Mixin internal instance methods */ diff --git a/tasks/bench.js b/tasks/bench.js index 38b36cda7ad..39bd8ca45b0 100644 --- a/tasks/bench.js +++ b/tasks/bench.js @@ -3,7 +3,7 @@ */ module.exports = function (grunt) { - grunt.registerTask('bench', function () { + grunt.registerTask('bench', function (target) { // polyfill window/document for old Vue global.window = { @@ -14,12 +14,18 @@ module.exports = function (grunt) { documentElement: {} } - require('fs') - .readdirSync('./benchmarks') - .forEach(function (mod) { - if (mod === 'browser.js' || mod === 'runner.html') return - require('../benchmarks/' + mod) - }) + if (target) { + run(target) + } else { + require('fs') + .readdirSync('./benchmarks') + .forEach(run) + } + + function run (mod) { + if (mod === 'browser.js' || mod === 'runner.html') return + require('../benchmarks/' + mod) + } }) } \ No newline at end of file From d6ce9a03daf0f2ad89907a8136c4d84f8d4af6bf Mon Sep 17 00:00:00 2001 From: Evan You Date: Mon, 14 Jul 2014 18:10:54 -0400 Subject: [PATCH 0031/1534] should only fall through properties on $scope. --- explorations/inheritance.js | 10 ++++++++-- src/internal/init.js | 32 +++++++++++++++++++++----------- 2 files changed, 29 insertions(+), 13 deletions(-) diff --git a/explorations/inheritance.js b/explorations/inheritance.js index d6a28a83fba..832c0fbe30c 100644 --- a/explorations/inheritance.js +++ b/explorations/inheritance.js @@ -48,10 +48,16 @@ item._observer.on('set', function (key, val) { // TODO turn these into tests console.log(vm.a) // 'parent a' + console.log(child.a) // 'child a' -console.log(child.b) // 'parent b' +console.log(child.$scope.a) // 'child a' +console.log(child.b) // undefined +console.log(child.$scope.b) // 'parent b' + console.log(item.a) // 'item a' -console.log(item.b) // 'parent b' +console.log(item.$scope.a) // 'item a' +console.log(item.b) // undefined +console.log(item.$scope.b) // 'parent b' // set shadowed parent property vm.a = 'haha!' // vm set:a haha! diff --git a/src/internal/init.js b/src/internal/init.js index b994542b0ea..580d3e61892 100644 --- a/src/internal/init.js +++ b/src/internal/init.js @@ -27,8 +27,8 @@ exports._init = function (options) { exports._initScope = function (options) { var parent = this.$parent = options.parent - var scope = this._scope = parent && options._inheritScope !== false - ? Object.create(parent._scope) + var scope = this.$scope = parent && options._inheritScope !== false + ? Object.create(parent.$scope) : {} // create scope observer this._observer = Observer.create(scope, { @@ -62,7 +62,7 @@ exports._initScope = function (options) { */ exports._teardownScope = function () { - this._scope = null + this.$scope = null if (!this.$parent) return var pob = this.$parent._observer var listeners = this._scopeListeners @@ -81,13 +81,14 @@ exports._teardownScope = function () { */ exports._initData = function (data, init) { - var scope = this._scope + var scope = this.$scope + var key if (!init) { // teardown old sync listeners this._unsync() // delete keys not present in the new data - for (var key in scope) { + for (key in scope) { if (scope.hasOwnProperty(key) && !(key in data)) { scope.$delete(key) } @@ -95,7 +96,7 @@ exports._initData = function (data, init) { } // copy instantiation data into scope - for (var key in data) { + for (key in data) { if (scope.hasOwnProperty(key)) { // existing property, trigger set scope[key] = data[key] @@ -113,14 +114,23 @@ exports._initData = function (data, init) { /** * Proxy the scope properties on the instance itself. - * So that vm.a === vm._scope.a + * So that vm.a === vm.$scope.a. + * + * Note this only proxy *local* scope properties. + * This prevents child instances accidentally modifying properties + * with the same name up in the scope chain because scope perperties + * are all getter/setters. + * + * To access parent properties through prototypal fall through, + * access it on the instance's $scope. */ exports._initProxy = function () { - // proxy every scope property on the instance itself - var scope = this._scope + var scope = this.$scope for (var key in scope) { - _.proxy(this, scope, key) + if (scope.hasOwnProperty(key)) { + _.proxy(this, scope, key) + } } // keep proxying up-to-date with added/deleted keys. this._observer @@ -139,7 +149,7 @@ exports._initProxy = function () { exports._sync = function () { var data = this._data - var scope = this._scope + var scope = this.$scope var locked = false var listeners = this._syncListeners = { From ca253824fd5f28dd105a0e9a17fcd69c936e200e Mon Sep 17 00:00:00 2001 From: Evan You Date: Mon, 14 Jul 2014 21:08:13 -0400 Subject: [PATCH 0032/1534] more restructuring --- src/instance/bindings.js | 7 ++ src/instance/compile.js | 7 ++ src/{internal/init.js => instance/data.js} | 101 --------------------- src/instance/proxy.js | 31 +++++++ src/instance/scope.js | 54 +++++++++++ src/internal/compile.js | 9 -- src/internal/properties.js | 34 ------- src/template.js | 10 -- src/vue.js | 44 +++++++-- 9 files changed, 137 insertions(+), 160 deletions(-) create mode 100644 src/instance/bindings.js create mode 100644 src/instance/compile.js rename src/{internal/init.js => instance/data.js} (50%) create mode 100644 src/instance/proxy.js create mode 100644 src/instance/scope.js delete mode 100644 src/internal/compile.js delete mode 100644 src/internal/properties.js delete mode 100644 src/template.js diff --git a/src/instance/bindings.js b/src/instance/bindings.js new file mode 100644 index 00000000000..5080fae5625 --- /dev/null +++ b/src/instance/bindings.js @@ -0,0 +1,7 @@ +/** + * Setup the bindings graph. + */ + +exports._initBindings = function () { + +} \ No newline at end of file diff --git a/src/instance/compile.js b/src/instance/compile.js new file mode 100644 index 00000000000..751d94434ab --- /dev/null +++ b/src/instance/compile.js @@ -0,0 +1,7 @@ +/** + * Compile the instance's template. + */ + +exports._compile = function () { + +} \ No newline at end of file diff --git a/src/internal/init.js b/src/instance/data.js similarity index 50% rename from src/internal/init.js rename to src/instance/data.js index 580d3e61892..451b15734c6 100644 --- a/src/internal/init.js +++ b/src/instance/data.js @@ -1,75 +1,4 @@ -var _ = require('../util') var Observer = require('../observe/observer') -var scopeEvents = ['set', 'mutate', 'added', 'deleted', 'added:self', 'deleted:self'] - -/** - * Kick off the initialization process on instance creation. - * - * @param {Object} options - * @private - */ - -exports._init = function (options) { - this.$options = options = options || {} - // create scope - this._initScope(options) - // setup initial data. - this._initData(options.data || {}, true) - // setup property proxying - this._initProxy() -} - -/** - * Setup scope and listen to parent scope changes. - * Only called once during _init(). - */ - -exports._initScope = function (options) { - - var parent = this.$parent = options.parent - var scope = this.$scope = parent && options._inheritScope !== false - ? Object.create(parent.$scope) - : {} - // create scope observer - this._observer = Observer.create(scope, { - callbackContext: this, - doNotAlterProto: true - }) - - if (!parent) return - - // relay change events that sent down from - // the scope prototype chain. - var ob = this._observer - var pob = parent._observer - var listeners = this._scopeListeners = {} - scopeEvents.forEach(function (event) { - var cb = listeners[event] = function (key, a, b) { - // since these events come from upstream, - // we only emit them if we don't have the same keys - // shadowing them in current scope. - if (!scope.hasOwnProperty(key)) { - ob.emit(event, key, a, b) - } - } - pob.on(event, cb) - }) -} - -/** - * Teardown scope and remove listeners attached to parent scope. - * Only called once during $destroy(). - */ - -exports._teardownScope = function () { - this.$scope = null - if (!this.$parent) return - var pob = this.$parent._observer - var listeners = this._scopeListeners - scopeEvents.forEach(function (event) { - pob.off(event, listeners[event]) - }) -} /** * Set the instances data object. Teasdown previous data @@ -112,36 +41,6 @@ exports._initData = function (data, init) { this._sync() } -/** - * Proxy the scope properties on the instance itself. - * So that vm.a === vm.$scope.a. - * - * Note this only proxy *local* scope properties. - * This prevents child instances accidentally modifying properties - * with the same name up in the scope chain because scope perperties - * are all getter/setters. - * - * To access parent properties through prototypal fall through, - * access it on the instance's $scope. - */ - -exports._initProxy = function () { - var scope = this.$scope - for (var key in scope) { - if (scope.hasOwnProperty(key)) { - _.proxy(this, scope, key) - } - } - // keep proxying up-to-date with added/deleted keys. - this._observer - .on('added:self', function (key) { - _.proxy(this, scope, key) - }) - .on('deleted:self', function (key) { - delete this[key] - }) -} - /** * Setup two-way sync between the instance scope and * the original data. Requires teardown. diff --git a/src/instance/proxy.js b/src/instance/proxy.js new file mode 100644 index 00000000000..a9136c23fe2 --- /dev/null +++ b/src/instance/proxy.js @@ -0,0 +1,31 @@ +var _ = require('../util') + +/** + * Proxy the scope properties on the instance itself. + * So that vm.a === vm.$scope.a. + * + * Note this only proxy *local* scope properties. + * This prevents child instances accidentally modifying properties + * with the same name up in the scope chain because scope perperties + * are all getter/setters. + * + * To access parent properties through prototypal fall through, + * access it on the instance's $scope. + */ + +exports._initProxy = function () { + var scope = this.$scope + for (var key in scope) { + if (scope.hasOwnProperty(key)) { + _.proxy(this, scope, key) + } + } + // keep proxying up-to-date with added/deleted keys. + this._observer + .on('added:self', function (key) { + _.proxy(this, scope, key) + }) + .on('deleted:self', function (key) { + delete this[key] + }) +} \ No newline at end of file diff --git a/src/instance/scope.js b/src/instance/scope.js new file mode 100644 index 00000000000..c64910b9214 --- /dev/null +++ b/src/instance/scope.js @@ -0,0 +1,54 @@ +var Observer = require('../observe/observer') +var scopeEvents = ['set', 'mutate', 'added', 'deleted'] + +/** + * Setup scope and listen to parent scope changes. + * Only called once during _init(). + */ + +exports._initScope = function (options) { + + var parent = this.$parent = options.parent + var scope = this.$scope = parent && options._inheritScope !== false + ? Object.create(parent.$scope) + : {} + // create scope observer + this._observer = Observer.create(scope, { + callbackContext: this, + doNotAlterProto: true + }) + + if (!parent) return + + // relay change events that sent down from + // the scope prototype chain. + var ob = this._observer + var pob = parent._observer + var listeners = this._scopeListeners = {} + scopeEvents.forEach(function (event) { + var cb = listeners[event] = function (key, a, b) { + // since these events come from upstream, + // we only emit them if we don't have the same keys + // shadowing them in current scope. + if (!scope.hasOwnProperty(key)) { + ob.emit(event, key, a, b) + } + } + pob.on(event, cb) + }) +} + +/** + * Teardown scope and remove listeners attached to parent scope. + * Only called once during $destroy(). + */ + +exports._teardownScope = function () { + this.$scope = null + if (!this.$parent) return + var pob = this.$parent._observer + var listeners = this._scopeListeners + scopeEvents.forEach(function (event) { + pob.off(event, listeners[event]) + }) +} \ No newline at end of file diff --git a/src/internal/compile.js b/src/internal/compile.js deleted file mode 100644 index a30fa1d02b2..00000000000 --- a/src/internal/compile.js +++ /dev/null @@ -1,9 +0,0 @@ -/** - * Start compilation of an instance. - * - * @private - */ - -exports._compile = function () { - -} \ No newline at end of file diff --git a/src/internal/properties.js b/src/internal/properties.js deleted file mode 100644 index c302bfd613c..00000000000 --- a/src/internal/properties.js +++ /dev/null @@ -1,34 +0,0 @@ -/** - * Prototype properties on every Vue instance. - */ - -module.exports = function (p) { - - /** - * The $root recursively points to the root instance. - * - * @readonly - */ - - Object.defineProperty(p, '$root', { - get: function () { - return this.$parent - ? this.$parent.$root - : this - } - }) - - /** - * $data has a setter which does a bunch of teardown/setup work - */ - - Object.defineProperty(p, '$data', { - get: function () { - return this._data - }, - set: function (newData) { - this._initData(newData) - } - }) - -} \ No newline at end of file diff --git a/src/template.js b/src/template.js deleted file mode 100644 index dbaf56a2d99..00000000000 --- a/src/template.js +++ /dev/null @@ -1,10 +0,0 @@ -/** - * The Template class encapsulates the logic of template - * caching, instantiation, parsing and transclusion. - */ - -function Template () { - -} - -module.exports = Template \ No newline at end of file diff --git a/src/vue.js b/src/vue.js index c41900c2e55..85aea1df691 100644 --- a/src/vue.js +++ b/src/vue.js @@ -14,26 +14,58 @@ var _ = require('./util') */ function Vue (options) { - this._init(options) + this.$options = options = options || {} + // create scope + this._initScope(options) + // setup initial data. + this._initData(options.data || {}, true) + // setup property proxying + this._initProxy() + // setup root binding + this._initBindings() } var p = Vue.prototype /** - * Define prototype properties + * The $root recursively points to the root instance. + * + * @readonly + */ + +Object.defineProperty(p, '$root', { + get: function () { + return this.$parent + ? this.$parent.$root + : this + } +}) + +/** + * $data has a setter which does a bunch of teardown/setup work */ -require('./internal/properties')(p) +Object.defineProperty(p, '$data', { + get: function () { + return this._data + }, + set: function (newData) { + this._initData(newData) + } +}) /** * Mixin internal instance methods */ - _.mixin(p, require('./internal/init')) - _.mixin(p, require('./internal/compile')) +_.mixin(p, require('./instance/scope')) +_.mixin(p, require('./instance/data')) +_.mixin(p, require('./instance/proxy')) +_.mixin(p, require('./instance/bindings')) +_.mixin(p, require('./instance/compile')) /** - * Mixin API instance methods + * Mixin public API methods */ _.mixin(p, require('./api/data')) From db3a657a672770d09de12ae19356603b6e273952 Mon Sep 17 00:00:00 2001 From: Evan You Date: Mon, 14 Jul 2014 21:30:56 -0400 Subject: [PATCH 0033/1534] work on bindings --- src/binding.js | 8 +++++++- src/instance/bindings.js | 18 ++++++++++++++++++ 2 files changed, 25 insertions(+), 1 deletion(-) diff --git a/src/binding.js b/src/binding.js index 85e117218a5..fcc4001862b 100644 --- a/src/binding.js +++ b/src/binding.js @@ -1,5 +1,11 @@ function Binding () { - + this.children = Object.create(null) +} + +var p = Binding.prototype + +p.addChild = function (key, child) { + } module.exports = Binding \ No newline at end of file diff --git a/src/instance/bindings.js b/src/instance/bindings.js index 5080fae5625..d3485306e33 100644 --- a/src/instance/bindings.js +++ b/src/instance/bindings.js @@ -1,7 +1,25 @@ +var Binding = require('../binding') + /** * Setup the bindings graph. */ exports._initBindings = function () { + var root = this._rootBinding = new Binding() + // the $data binding points to the root itself! + root.addChild('$data', root) + // point $parent and $root bindings to their + // repective owners. + if (this.$parent) { + root.addChild('$parent', this.$parent._rootBinding) + root.addChild('$root', this.$root._rootBinding) + } +} + +/** + * Create a binding + */ + +exports._createBinding = function (key) { } \ No newline at end of file From 4ca4b2c77e869c091c0cb0306682a55763cf6cd1 Mon Sep 17 00:00:00 2001 From: Evan You Date: Mon, 14 Jul 2014 23:02:08 -0400 Subject: [PATCH 0034/1534] fix typo --- src/instance/data.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/instance/data.js b/src/instance/data.js index 451b15734c6..f48c715cd28 100644 --- a/src/instance/data.js +++ b/src/instance/data.js @@ -85,7 +85,7 @@ exports._sync = function () { this._dataObserver .on('set:self', listeners.scope.set) .on('added:self', listeners.scope.added) - .on('deleted:self', listeners.scope.delted) + .on('deleted:self', listeners.scope.deleted) /** * The guard function prevents infinite loop @@ -117,5 +117,5 @@ exports._unsync = function () { this._dataObserver .off('set:self', listeners.scope.set) .off('added:self', listeners.scope.added) - .off('deleted:self', listeners.scope.delted) + .off('deleted:self', listeners.scope.deleted) } \ No newline at end of file From f89290cb0dd82e48e5a410156170f49b6544abbd Mon Sep 17 00:00:00 2001 From: Evan You Date: Mon, 14 Jul 2014 23:23:59 -0400 Subject: [PATCH 0035/1534] comments --- src/instance/data.js | 13 +++++++------ src/instance/proxy.js | 14 ++++++++------ src/instance/scope.js | 12 ++++++++---- src/vue.js | 2 +- 4 files changed, 24 insertions(+), 17 deletions(-) diff --git a/src/instance/data.js b/src/instance/data.js index f48c715cd28..f31b671194f 100644 --- a/src/instance/data.js +++ b/src/instance/data.js @@ -1,12 +1,13 @@ var Observer = require('../observe/observer') /** - * Set the instances data object. Teasdown previous data - * object if necessary, and setup syncing between the scope - * and the data object. + * Setup the instances data object, copying properties into + * scope and setup the syncing between the data and the scope. + * If swapping data object with the `$data` accessor, teardown + * previous sync listeners and delete keys not present in new data. * * @param {Object} data - * @param {Boolean} init + * @param {Boolean} init - if not ture, indicates its a `$data` swap. */ exports._initData = function (data, init) { @@ -24,7 +25,7 @@ exports._initData = function (data, init) { } } - // copy instantiation data into scope + // copy properties into scope for (key in data) { if (scope.hasOwnProperty(key)) { // existing property, trigger set @@ -35,7 +36,7 @@ exports._initData = function (data, init) { } } - // sync scope and new data + // setup sync between scope and new data this._data = data this._dataObserver = Observer.create(data) this._sync() diff --git a/src/instance/proxy.js b/src/instance/proxy.js index a9136c23fe2..940142ee0e7 100644 --- a/src/instance/proxy.js +++ b/src/instance/proxy.js @@ -1,16 +1,18 @@ var _ = require('../util') /** - * Proxy the scope properties on the instance itself. - * So that vm.a === vm.$scope.a. + * Proxy the scope properties on the instance itself, + * so that vm.a === vm.$scope.a. * - * Note this only proxy *local* scope properties. - * This prevents child instances accidentally modifying properties - * with the same name up in the scope chain because scope perperties - * are all getter/setters. + * Note this only proxies *local* scope properties. We want to + * prevent child instances accidentally modifying properties + * with the same name up in the scope chain because scope + * perperties are all getter/setters. * * To access parent properties through prototypal fall through, * access it on the instance's $scope. + * + * This should only be called once during _init(). */ exports._initProxy = function () { diff --git a/src/instance/scope.js b/src/instance/scope.js index c64910b9214..7ed370103b6 100644 --- a/src/instance/scope.js +++ b/src/instance/scope.js @@ -2,12 +2,16 @@ var Observer = require('../observe/observer') var scopeEvents = ['set', 'mutate', 'added', 'deleted'] /** - * Setup scope and listen to parent scope changes. - * Only called once during _init(). + * Setup instance scope. + * The scope is reponsible for prototypal inheritance of + * parent instance propertiesm abd all binding paths and + * expressions of the current instance are evaluated against its scope. + * + * This should only be called once during _init(). */ -exports._initScope = function (options) { - +exports._initScope = function () { + var options = this.$options var parent = this.$parent = options.parent var scope = this.$scope = parent && options._inheritScope !== false ? Object.create(parent.$scope) diff --git a/src/vue.js b/src/vue.js index 85aea1df691..20b39fc3f87 100644 --- a/src/vue.js +++ b/src/vue.js @@ -16,7 +16,7 @@ var _ = require('./util') function Vue (options) { this.$options = options = options || {} // create scope - this._initScope(options) + this._initScope() // setup initial data. this._initData(options.data || {}, true) // setup property proxying From 67cec80173128ee9820b43ada628e6f431fddf94 Mon Sep 17 00:00:00 2001 From: Evan You Date: Tue, 15 Jul 2014 10:26:53 -0400 Subject: [PATCH 0036/1534] work on bindings --- changes.md | 6 ++++ src/binding.js | 35 ++++++++++++++++++++ src/instance/bindings.js | 39 ++++++++++++++++++++-- src/instance/data.js | 50 ++++++++++++++++++----------- src/instance/proxy.js | 4 +-- src/instance/scope.js | 4 +-- src/observe/object-augmentations.js | 8 ++--- src/parse/template.js | 0 src/vue.js | 2 +- 9 files changed, 118 insertions(+), 30 deletions(-) create mode 100644 src/parse/template.js diff --git a/changes.md b/changes.md index 530ee0924a8..6c9bf5c696b 100644 --- a/changes.md +++ b/changes.md @@ -9,6 +9,12 @@ var vm = new Vue({ data: {a:1} }) // only observes the data vm.$mount('#app') // actually compile the DOM ``` +## Scope & Data + +- new option: `syncData`. + +Each Vue instance now creates an associated `$scope` object which has prototypal inheritance similar to Angular. This makes expression evaluation much cleaner. A side effect of this change is that the `data` object being passed in is no longer mutated by default. You need to now explicitly pass in `syncData: true` in the options for direct property changes on the scope to be synced back to the root data object. In most cases, this is not necessary. + ## More flexible directive syntax - v-on diff --git a/src/binding.js b/src/binding.js index fcc4001862b..52d3a17eaba 100644 --- a/src/binding.js +++ b/src/binding.js @@ -4,8 +4,43 @@ function Binding () { var p = Binding.prototype +/** + * Add a child binding to the tree. + * + * @param {String} key + * @param {Binding} child + */ + p.addChild = function (key, child) { } +/** + * Traverse along a path and trigger updates + * along the way. + * + * @param {String} path + */ + +p.updatePath = function (path) { + +} + +/** + * Trigger updates for the subtree starting at + * self as root. + */ + +p.updateSubTree = function () { + +} + +/** + * Notify all subscribers of it self + */ + +p.notify = function () { + +} + module.exports = Binding \ No newline at end of file diff --git a/src/instance/bindings.js b/src/instance/bindings.js index d3485306e33..c9fd4c0a861 100644 --- a/src/instance/bindings.js +++ b/src/instance/bindings.js @@ -1,7 +1,22 @@ var Binding = require('../binding') +var Observer = require('../observe/observer') +var Path = require('../parse/path') /** - * Setup the bindings graph. + * Setup the binding tree. + * + * Bindings form a tree-like structure that maps the Object structure + * of observed data. However, only paths present in the templates are + * created in the binding tree. When a change event from the data + * observer arrives on the instance, we traverse the binding tree + * along the changed path, triggering binding updates along the way. + * When we reach the path endpoint, if it has any children, we also + * trigger updates on the entire sub-tree. + * + * Each instance has a root binding and it has three special children: + * `$data`, `$parent` & `$root`. `$data` points to the root binding + * itself. `$parent` and `$root` point to the instance's parent and + * root's root bindings, respectively. */ exports._initBindings = function () { @@ -14,12 +29,30 @@ exports._initBindings = function () { root.addChild('$parent', this.$parent._rootBinding) root.addChild('$root', this.$root._rootBinding) } + this._observer + .on('set', this._updateBindings) + .on('add', this._updateBindings) + .on('delete', this._updateBindings) + .on('mutate', this._updateBindings) } /** - * Create a binding + * Create bindings along a path + * + * @param {String|Array} path */ -exports._createBinding = function (key) { +exports._createBindings = function (path) { +} + +/** + * Traverse the binding tree + * + * @param {String} path + */ + +exports._updateBindings = function (path) { + path = path.split(Observer.pathDelimiter) + this._rootBinding.updatePath(path) } \ No newline at end of file diff --git a/src/instance/data.js b/src/instance/data.js index f31b671194f..1e7fb529724 100644 --- a/src/instance/data.js +++ b/src/instance/data.js @@ -1,8 +1,17 @@ var Observer = require('../observe/observer') /** - * Setup the instances data object, copying properties into - * scope and setup the syncing between the data and the scope. + * Setup the instances data object. + * + * Properties are copied into the scope object to take advantage of + * prototypal inheritance. + * + * If the `syncData` option is true, Vue will maintain property + * syncing between the scope and the original data object, so that + * any changes to the scope are synced back to the passed in object. + * This is useful internally when e.g. creating v-repeat instances + * with no alias. + * * If swapping data object with the `$data` accessor, teardown * previous sync listeners and delete keys not present in new data. * @@ -12,11 +21,14 @@ var Observer = require('../observe/observer') exports._initData = function (data, init) { var scope = this.$scope + var options = this.$options var key if (!init) { // teardown old sync listeners - this._unsync() + if (options.syncData) { + this._unsync() + } // delete keys not present in the new data for (key in scope) { if (scope.hasOwnProperty(key) && !(key in data)) { @@ -37,9 +49,11 @@ exports._initData = function (data, init) { } // setup sync between scope and new data - this._data = data - this._dataObserver = Observer.create(data) - this._sync() + if (options.syncData) { + this._data = data + this._dataObserver = Observer.create(data) + this._sync() + } } /** @@ -57,10 +71,10 @@ exports._sync = function () { set: guard(function (key, val) { data[key] = val }), - added: guard(function (key, val) { + add: guard(function (key, val) { data.$add(key, val) }), - deleted: guard(function (key) { + delete: guard(function (key) { data.$delete(key) }) }, @@ -68,10 +82,10 @@ exports._sync = function () { set: guard(function (key, val) { scope[key] = val }), - added: guard(function (key, val) { + add: guard(function (key, val) { scope.$add(key, val) }), - deleted: guard(function (key) { + delete: guard(function (key) { scope.$delete(key) }) } @@ -80,13 +94,13 @@ exports._sync = function () { // sync scope and original data. this._observer .on('set:self', listeners.data.set) - .on('added:self', listeners.data.added) - .on('deleted:self', listeners.data.deleted) + .on('add:self', listeners.data.add) + .on('delete:self', listeners.data.delete) this._dataObserver .on('set:self', listeners.scope.set) - .on('added:self', listeners.scope.added) - .on('deleted:self', listeners.scope.deleted) + .on('add:self', listeners.scope.add) + .on('delete:self', listeners.scope.delete) /** * The guard function prevents infinite loop @@ -112,11 +126,11 @@ exports._unsync = function () { this._observer .off('set:self', listeners.data.set) - .off('added:self', listeners.data.added) - .off('deleted:self', listeners.data.deleted) + .off('add:self', listeners.data.add) + .off('delete:self', listeners.data.delete) this._dataObserver .off('set:self', listeners.scope.set) - .off('added:self', listeners.scope.added) - .off('deleted:self', listeners.scope.deleted) + .off('add:self', listeners.scope.add) + .off('delete:self', listeners.scope.delete) } \ No newline at end of file diff --git a/src/instance/proxy.js b/src/instance/proxy.js index 940142ee0e7..1ce0b7120b0 100644 --- a/src/instance/proxy.js +++ b/src/instance/proxy.js @@ -24,10 +24,10 @@ exports._initProxy = function () { } // keep proxying up-to-date with added/deleted keys. this._observer - .on('added:self', function (key) { + .on('add:self', function (key) { _.proxy(this, scope, key) }) - .on('deleted:self', function (key) { + .on('delete:self', function (key) { delete this[key] }) } \ No newline at end of file diff --git a/src/instance/scope.js b/src/instance/scope.js index 7ed370103b6..296d2e3ddc5 100644 --- a/src/instance/scope.js +++ b/src/instance/scope.js @@ -1,5 +1,5 @@ var Observer = require('../observe/observer') -var scopeEvents = ['set', 'mutate', 'added', 'deleted'] +var scopeEvents = ['set', 'mutate', 'add', 'delete'] /** * Setup instance scope. @@ -13,7 +13,7 @@ var scopeEvents = ['set', 'mutate', 'added', 'deleted'] exports._initScope = function () { var options = this.$options var parent = this.$parent = options.parent - var scope = this.$scope = parent && options._inheritScope !== false + var scope = this.$scope = parent ? Object.create(parent.$scope) : {} // create scope observer diff --git a/src/observe/object-augmentations.js b/src/observe/object-augmentations.js index d107b7f8d2b..9f80eb8bd6f 100644 --- a/src/observe/object-augmentations.js +++ b/src/observe/object-augmentations.js @@ -17,8 +17,8 @@ _.define(objectAgumentations, '$add', function (key, val) { var ob = this.$observer ob.observe(key, val) ob.convert(key, val) - ob.emit('added:self', key, val) - ob.propagate('added', key, val) + ob.emit('add:self', key, val) + ob.propagate('add', key, val) }) /** @@ -33,8 +33,8 @@ _.define(objectAgumentations, '$delete', function (key) { if (!this.hasOwnProperty(key)) return delete this[key] var ob = this.$observer - ob.emit('deleted:self', key) - ob.propagate('deleted', key) + ob.emit('delete:self', key) + ob.propagate('delete', key) }) module.exports = objectAgumentations \ No newline at end of file diff --git a/src/parse/template.js b/src/parse/template.js new file mode 100644 index 00000000000..e69de29bb2d diff --git a/src/vue.js b/src/vue.js index 20b39fc3f87..9666285ddbd 100644 --- a/src/vue.js +++ b/src/vue.js @@ -21,7 +21,7 @@ function Vue (options) { this._initData(options.data || {}, true) // setup property proxying this._initProxy() - // setup root binding + // setup binding tree this._initBindings() } From de089aa176b5ae10a2b27687e77ffff5d9504f2a Mon Sep 17 00:00:00 2001 From: Evan You Date: Wed, 16 Jul 2014 04:18:43 -0400 Subject: [PATCH 0037/1534] add doctype to bench runner so it works in IE9 --- benchmarks/runner.html | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/benchmarks/runner.html b/benchmarks/runner.html index b5d4bf1f791..8f54f2a3f3b 100644 --- a/benchmarks/runner.html +++ b/benchmarks/runner.html @@ -1 +1,10 @@ - \ No newline at end of file + + + + + + + + + + \ No newline at end of file From bbc38d64a1952f43b648055b58530a322878d42a Mon Sep 17 00:00:00 2001 From: Evan You Date: Wed, 16 Jul 2014 12:43:03 -0400 Subject: [PATCH 0038/1534] bindings and directive --- src/binding.js | 33 +++++++++++++++++++++++++++++++-- src/directive.js | 23 +++++++++++++++++++++-- 2 files changed, 52 insertions(+), 4 deletions(-) diff --git a/src/binding.js b/src/binding.js index 52d3a17eaba..9983cb4f16d 100644 --- a/src/binding.js +++ b/src/binding.js @@ -1,5 +1,14 @@ +/** + * A binding is an observable that can have multiple directives + * subscribing to it. It can also have multiple other bindings + * as children to form a trie-like structure. + * + * @constructor + */ + function Binding () { this.children = Object.create(null) + this.subs = [] } var p = Binding.prototype @@ -36,10 +45,30 @@ p.updateSubTree = function () { } /** - * Notify all subscribers of it self + * Add a directive subscriber. + * + * @param {Directive} sub + */ + +p.addSubscriber = function (sub) { + +} + +/** + * Remove a directive subscriber. + * + * @param {Directive} sub + */ + +p.removeSubscriber = function (sub) { + +} + +/** + * Notify all subscribers of a new value. */ -p.notify = function () { +p.publish = function () { } diff --git a/src/directive.js b/src/directive.js index 3e172ca0e07..6fd3a74f84d 100644 --- a/src/directive.js +++ b/src/directive.js @@ -1,5 +1,24 @@ -function Directive () { - +/** + * A directive links a DOM element with a piece of data, which can + * be either simple paths or computed properties. It subscribes to + * a list of dependencies (Bindings) and refreshes the list during + * its getter evaluation. + * + * @param {String} id + * @param {Node} el + * @param {Vue} vm + * @param {String} expression + * @constructor + */ + +function Directive (id, el, vm, expression) { + +} + +var p = Directive.prototype + +p._update = function () { + } module.exports = Directive \ No newline at end of file From 5e9fcb2785771108bae0a4a9574f77174c34df11 Mon Sep 17 00:00:00 2001 From: Evan You Date: Wed, 16 Jul 2014 12:59:38 -0400 Subject: [PATCH 0039/1534] uid --- src/binding.js | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/src/binding.js b/src/binding.js index 9983cb4f16d..7af308387a3 100644 --- a/src/binding.js +++ b/src/binding.js @@ -1,3 +1,11 @@ +/** + * Assign unique id to each binding created so that directives + * can have an easier time avoiding duplicates and refreshing + * dependencies. + */ + +var uid = 0 + /** * A binding is an observable that can have multiple directives * subscribing to it. It can also have multiple other bindings @@ -7,8 +15,9 @@ */ function Binding () { - this.children = Object.create(null) - this.subs = [] + this.uid = uid++ + this.children = Object.create(null) + this.subs = [] } var p = Binding.prototype From aa24b3e4ad19bf11fb57f5b4871ab057fc5b655b Mon Sep 17 00:00:00 2001 From: Evan You Date: Wed, 16 Jul 2014 18:46:39 -0400 Subject: [PATCH 0040/1534] working on global api --- explorations/inheritance.js | 1 + src/api/global.js | 103 ++++++++++++++++++++++++------------ src/config.js | 53 ++++++++++++++++--- src/directives/index.js | 0 src/filters/index.js | 0 src/instance/scope.js | 3 +- src/util.js | 59 +++++++++++++++++++++ src/vue.js | 8 ++- 8 files changed, 182 insertions(+), 45 deletions(-) create mode 100644 src/directives/index.js create mode 100644 src/filters/index.js diff --git a/explorations/inheritance.js b/explorations/inheritance.js index 832c0fbe30c..9c9c937c962 100644 --- a/explorations/inheritance.js +++ b/explorations/inheritance.js @@ -30,6 +30,7 @@ window.child = new Vue({ window.item = new Vue({ parent: vm, + syncData: true, data: vm.arr[0] }) diff --git a/src/api/global.js b/src/api/global.js index 167ef520ca6..fa619a2fa03 100644 --- a/src/api/global.js +++ b/src/api/global.js @@ -2,60 +2,95 @@ var _ = require('../util') var config = require('../config') /** - * Configuration + * Vue and every constructor that extends Vue has an associated + * options object, which can be accessed during compilation steps + * as `this.constructor.options`. */ -exports.config = function () { - +exports.options = { + directives : require('../directives'), + filters : require('../filters'), + partials : {}, + effects : {}, + components : {} } /** - * Class inehritance + * Expose useful internals */ -exports.extend = function () { - -} +exports.util = _ +exports.config = config +exports.nextTick = _.nextTick +exports.transition = require('../transition/transition') /** - * Plugin system + * Class inehritance + * + * @param {Object} extendOptions */ -exports.use = function () { - +exports.extend = function (extendOptions) { + var Super = this + var Sub = function (instanceOptions) { + var mergedOptions = _.mergeOptions(Sub.options, instanceOptions) + Super.call(this, mergedOptions) + } + Sub.prototype = Object.create(Super.prototype) + _.define(Sub.prototype, 'constructor', Sub) + Sub.options = _.mergeOptions(Super.options, extendOptions) + Sub.super = Super + // allow further extension + Sub.extend = Super.extend + // create asset registers, so extended classes + // can have their private assets too. + createAssetRegisters(Sub) } /** - * Expose some internal utilities + * Plugin system + * + * @param {String|Object} plugin */ -exports.require = function () { - +exports.use = function (plugin) { + if (typeof plugin === 'string') { + try { + plugin = require(plugin) + } catch (e) { + _.warn('Cannot load plugin: ' + plugin) + } + } + // additional parameters + var args = [].slice.call(arguments, 1) + args.unshift(this) + if (typeof plugin.install === 'function') { + plugin.install.apply(plugin, args) + } else { + plugin.apply(null, args) + } + return this } /** - * Define asset registries and registration - * methods on a constructor. + * Define asset registration methods on a constructor. + * + * @param {Function} Ctor */ -config.assetTypes.forEach(function (type) { - var registry = '_' + type + 's' - exports[registry] = {} - - /** - * Asset registration method. - * - * @param {String} id - * @param {*} definition - */ - - exports[type] = function (id, definition) { - this[registry][id] = definition - } -}) +createAssetRegisters(exports) +function createAssetRegisters (Ctor) { + config.assetTypes.forEach(function (type) { -/** - * This is pretty useful so we expose it as a global method. - */ + /** + * Asset registration method. + * + * @param {String} id + * @param {*} definition + */ -exports.nextTick = _.nextTick \ No newline at end of file + Ctor[type] = function (id, definition) { + this.options[type + 's'][id] = definition + } + }) +} \ No newline at end of file diff --git a/src/config.js b/src/config.js index 4d4591e64d5..7d8569866a2 100644 --- a/src/config.js +++ b/src/config.js @@ -1,11 +1,50 @@ +var assetTypes = [ + 'directive', + 'filter', + 'partial', + 'effect', + 'component' +] + module.exports = { - assetTypes: [ - 'directive', - 'filter', - 'partial', - 'effect', - 'component' - ] + /** + * The prefix to look for when parsing directives. + * @type {String} + */ + + prefix: 'v', + + /** + * Whether to print debug messages. + * Also enables stack trace for warnings. + * @type {Boolean} + */ + + debug: false, + + /** + * Whether to suppress warnings. + * @type {Boolean} + */ + + silent: false, + + /** + * Whether to parse mustache tags in templates. + * @type {Boolean} + */ + + interpolate: true, + + /** + * Asset types + * @type {Array.} + * @readonly + */ + + get assetTypes () { + return assetTypes + } } \ No newline at end of file diff --git a/src/directives/index.js b/src/directives/index.js new file mode 100644 index 00000000000..e69de29bb2d diff --git a/src/filters/index.js b/src/filters/index.js new file mode 100644 index 00000000000..e69de29bb2d diff --git a/src/instance/scope.js b/src/instance/scope.js index 296d2e3ddc5..6d721db6975 100644 --- a/src/instance/scope.js +++ b/src/instance/scope.js @@ -11,8 +11,7 @@ var scopeEvents = ['set', 'mutate', 'add', 'delete'] */ exports._initScope = function () { - var options = this.$options - var parent = this.$parent = options.parent + var parent = this.$parent = this.$options.parent var scope = this.$scope = parent ? Object.create(parent.$scope) : {} diff --git a/src/util.js b/src/util.js index 8ce5b4e70ec..c029db6f85c 100644 --- a/src/util.js +++ b/src/util.js @@ -1,3 +1,5 @@ +var config = require('./config') + /** * Mix properties into target object. * @@ -105,4 +107,61 @@ if ('__proto__' in {}) { } } else { exports.augment = exports.deepMixin +} + +/** + * Merge two option objects. + * + * @param {Object} parent + * @param {Object} child + * @param {Boolean} noRecurse + */ + +exports.mergeOptions = function (parent, child, noRecurse) { + // TODO + // - merge lifecycle hooks + // - merge asset registries + // - else override + // - use prototypal inheritance where appropriate +} + +/** + * Enable debug utilities. The enableDebug() function and all + * _.log() & _.warn() calls will be dropped in the minified + * production build. + */ + +enableDebug() + +function enableDebug () { + + var hasConsole = typeof console !== 'undefined' + + /** + * Log a message. + * + * @param {String} msg + */ + + exports.log = function (msg) { + if (hasConsole && config.debug) { + console.log(msg) + } + } + + /** + * We've got a problem here. + * + * @param {String} msg + */ + + exports.warn = function (msg) { + if (hasConsole && !config.silent) { + console.warn(msg) + if (config.debug && console.trace) { + console.trace(msg) + } + } + } + } \ No newline at end of file diff --git a/src/vue.js b/src/vue.js index 9666285ddbd..cda083874b4 100644 --- a/src/vue.js +++ b/src/vue.js @@ -14,17 +14,21 @@ var _ = require('./util') */ function Vue (options) { - this.$options = options = options || {} + this.$options = options || {} // create scope this._initScope() // setup initial data. - this._initData(options.data || {}, true) + this._initData(this.$options.data || {}, true) // setup property proxying this._initProxy() // setup binding tree this._initBindings() } +/** + * Build up the prototype + */ + var p = Vue.prototype /** From 96adbae6223028ce45a16c9339bd08bc3558ea73 Mon Sep 17 00:00:00 2001 From: Evan You Date: Thu, 17 Jul 2014 13:36:45 -0400 Subject: [PATCH 0041/1534] fix observer tests --- src/api/global.js | 2 +- src/config.js | 18 +++++++----------- test/unit/observer.js | 14 +++++++------- 3 files changed, 15 insertions(+), 19 deletions(-) diff --git a/src/api/global.js b/src/api/global.js index fa619a2fa03..bb99594ae4d 100644 --- a/src/api/global.js +++ b/src/api/global.js @@ -80,7 +80,7 @@ exports.use = function (plugin) { createAssetRegisters(exports) function createAssetRegisters (Ctor) { - config.assetTypes.forEach(function (type) { + config._assetTypes.forEach(function (type) { /** * Asset registration method. diff --git a/src/config.js b/src/config.js index 7d8569866a2..ab293bebd98 100644 --- a/src/config.js +++ b/src/config.js @@ -1,11 +1,3 @@ -var assetTypes = [ - 'directive', - 'filter', - 'partial', - 'effect', - 'component' -] - module.exports = { /** @@ -43,8 +35,12 @@ module.exports = { * @readonly */ - get assetTypes () { - return assetTypes - } + _assetTypes: [ + 'directive', + 'filter', + 'partial', + 'effect', + 'component' + ] } \ No newline at end of file diff --git a/test/unit/observer.js b/test/unit/observer.js index 1955e1e30c6..55e1bc03e3c 100644 --- a/test/unit/observer.js +++ b/test/unit/observer.js @@ -1,4 +1,4 @@ -var Observer = require('../../src/observer/observer') +var Observer = require('../../src/observe/observer') // internal emitter has fixed 3 arguments // so we need to fill up the assetions with undefined var u = undefined @@ -244,14 +244,14 @@ describe('Observer', function () { it('object.$add', function () { var obj = {a:{b:1}} var ob = Observer.create(obj) - ob.on('added', spy) + ob.on('add', spy) // add event - var added = {d:2} - obj.a.$add('c', added) - expect(spy).toHaveBeenCalledWith('a.c', added, u) + var add = {d:2} + obj.a.$add('c', add) + expect(spy).toHaveBeenCalledWith('a.c', add, u) - // check if added object is properly observed + // check if add object is properly observed ob.on('set', spy) obj.a.c.d = 3 expect(spy).toHaveBeenCalledWith('a.c.d', 3, u) @@ -260,7 +260,7 @@ describe('Observer', function () { it('object.$delete', function () { var obj = {a:{b:1}} var ob = Observer.create(obj) - ob.on('deleted', spy) + ob.on('delete', spy) obj.a.$delete('b') expect(spy).toHaveBeenCalledWith('a.b', u, u) From be19525402e7007031b4b9a0fcdeb13523e402ab Mon Sep 17 00:00:00 2001 From: Evan You Date: Sun, 20 Jul 2014 00:41:11 -0400 Subject: [PATCH 0042/1534] path --- src/instance/bindings.js | 12 ++- src/parse/path.js | 220 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 228 insertions(+), 4 deletions(-) diff --git a/src/instance/bindings.js b/src/instance/bindings.js index c9fd4c0a861..dca0f929a7f 100644 --- a/src/instance/bindings.js +++ b/src/instance/bindings.js @@ -29,11 +29,15 @@ exports._initBindings = function () { root.addChild('$parent', this.$parent._rootBinding) root.addChild('$root', this.$root._rootBinding) } + var self = this + var updateBindings = function (path) { + self._updateBindings(path) + } this._observer - .on('set', this._updateBindings) - .on('add', this._updateBindings) - .on('delete', this._updateBindings) - .on('mutate', this._updateBindings) + .on('set', updateBindings) + .on('add', updateBindings) + .on('delete', updateBindings) + .on('mutate', updateBindings) } /** diff --git a/src/parse/path.js b/src/parse/path.js index e69de29bb2d..ba7dd64aca0 100644 --- a/src/parse/path.js +++ b/src/parse/path.js @@ -0,0 +1,220 @@ +/** + * Path-parsing algorithm scooped from Polymer/observe-js + */ + +var pathStateMachine = { + 'beforePath': { + 'ws': ['beforePath'], + 'ident': ['inIdent', 'append'], + '[': ['beforeElement'], + 'eof': ['afterPath'] + }, + + 'inPath': { + 'ws': ['inPath'], + '.': ['beforeIdent'], + '[': ['beforeElement'], + 'eof': ['afterPath'] + }, + + 'beforeIdent': { + 'ws': ['beforeIdent'], + 'ident': ['inIdent', 'append'] + }, + + 'inIdent': { + 'ident': ['inIdent', 'append'], + '0': ['inIdent', 'append'], + 'number': ['inIdent', 'append'], + 'ws': ['inPath', 'push'], + '.': ['beforeIdent', 'push'], + '[': ['beforeElement', 'push'], + 'eof': ['afterPath', 'push'] + }, + + 'beforeElement': { + 'ws': ['beforeElement'], + '0': ['afterZero', 'append'], + 'number': ['inIndex', 'append'], + "'": ['inSingleQuote', 'append', ''], + '"': ['inDoubleQuote', 'append', ''] + }, + + 'afterZero': { + 'ws': ['afterElement', 'push'], + ']': ['inPath', 'push'] + }, + + 'inIndex': { + '0': ['inIndex', 'append'], + 'number': ['inIndex', 'append'], + 'ws': ['afterElement'], + ']': ['inPath', 'push'] + }, + + 'inSingleQuote': { + "'": ['afterElement'], + 'eof': ['error'], + 'else': ['inSingleQuote', 'append'] + }, + + 'inDoubleQuote': { + '"': ['afterElement'], + 'eof': ['error'], + 'else': ['inDoubleQuote', 'append'] + }, + + 'afterElement': { + 'ws': ['afterElement'], + ']': ['inPath', 'push'] + } +} + +function noop () {} + +function getPathCharType (char) { + if (char === undefined) + return 'eof' + + var code = char.charCodeAt(0) + + switch(code) { + case 0x5B: // [ + case 0x5D: // ] + case 0x2E: // . + case 0x22: // " + case 0x27: // ' + case 0x30: // 0 + return char + + case 0x5F: // _ + case 0x24: // $ + return 'ident' + + case 0x20: // Space + case 0x09: // Tab + case 0x0A: // Newline + case 0x0D: // Return + case 0xA0: // No-break space + case 0xFEFF: // Byte Order Mark + case 0x2028: // Line Separator + case 0x2029: // Paragraph Separator + return 'ws' + } + + // a-z, A-Z + if ((0x61 <= code && code <= 0x7A) || (0x41 <= code && code <= 0x5A)) + return 'ident' + + // 1-9 + if (0x31 <= code && code <= 0x39) + return 'number' + + return 'else' +} + +/** + * Parse a string path into an array of segments + * Todo implement cache + * + * @param {String} path + * @return {Array|undefined} + */ + +exports.parse = function (path) { + var keys = [] + var index = -1 + var c, newChar, key, type, transition, action, typeMap, mode = 'beforePath' + + var actions = { + push: function() { + if (key === undefined) + return + + keys.push(key) + key = undefined + }, + + append: function() { + if (key === undefined) + key = newChar + else + key += newChar + } + } + + function maybeUnescapeQuote() { + if (index >= path.length) + return + + var nextChar = path[index + 1] + if ((mode == 'inSingleQuote' && nextChar == "'") || + (mode == 'inDoubleQuote' && nextChar == '"')) { + index++ + newChar = nextChar + actions.append() + return true + } + } + + while (mode) { + index++ + c = path[index] + + if (c == '\\' && maybeUnescapeQuote(mode)) + continue + + type = getPathCharType(c) + typeMap = pathStateMachine[mode] + transition = typeMap[type] || typeMap['else'] || 'error' + + if (transition == 'error') + return // parse error + + mode = transition[0] + action = actions[transition[1]] || noop + newChar = transition[2] === undefined ? c : transition[2] + action() + + if (mode === 'afterPath') { + return keys + } + } + + return // parse error +} + +/** + * Get from an object from a path + * + * @param {Object} obj + * @param {String} path + */ + +exports.get = function (obj, path) { + +} + +/** + * Set on an object from a path + * + * @param {Object} obj + * @param {String} path + * @param {*} val + */ + +exports.set = function (obj, path, val) { + +} + +/** + * Traverse an object along a path and trigger callback + * + * @param {Object} obj + * @param {String} path + * @param {Function} cb + */ + +exports.traverse = function (obj, path, cb) { + +} \ No newline at end of file From 007e23fc2ee9b61cd1d78dcc21681fedc5f3cab4 Mon Sep 17 00:00:00 2001 From: Evan You Date: Sun, 20 Jul 2014 00:41:24 -0400 Subject: [PATCH 0043/1534] thoughts --- changes.md | 93 ++++++++++++++++++++++++++++-------------------------- 1 file changed, 48 insertions(+), 45 deletions(-) diff --git a/changes.md b/changes.md index 6c9bf5c696b..16774e627f3 100644 --- a/changes.md +++ b/changes.md @@ -2,20 +2,64 @@ If you happen to see this - note that most of these are just planned but subject ## Instantiation -Instances are no longer compiled at instantiation. Data will be observed, but no DOM compilation will happen until the new instance method `$mount` has been called. Also, when a new instance is created without `el` option, it no longers auto creates one. +**If no `el` option is provided** at instantiation, Vue will no longer auto-create an empty div for you. In this case, the instance is considered to be in "unmounted" state. Data will be observed, but no DOM compilation will happen until the new instance method `$mount` has been explicitly called. ``` js var vm = new Vue({ data: {a:1} }) // only observes the data vm.$mount('#app') // actually compile the DOM + +// in comparison, this will compile instantly just like before. +var vm = new Vue({ el: '#app', data: {a: 1} }) ``` -## Scope & Data +## New Scope Inheritance Model + +In the previous version, nested Vue instances do not have prototypal inheritance of their data scope. Although you can access parent data properties in templates, you need to explicitly travel up the scope chain with `this.$parent` in JavaScript code or use `this.$get()` to get a property on the scope chain. The expression parser also needs to do a lot of dirty work to determine the correct scope the variables belong to. + +In the new model, we provide a scope inehritance system similar to Angular, in which you can directly access properties that exist on parent scopes. The major difference is that setting a primitive value property on a child scope WILL affect that on the parent scope! This is one of the major gotchas in Angular. If you are somewhat familiar with how prototype inehritance works, you might be surprised how this is possible. Well, the reason is that all data properties in Vue are getter/setters, and invoking a setter will not cause the child scope shadowing parent scopes. See the example [here](http://jsfiddle.net/yyx990803/Px2n6/). + +The result of this model is a much cleaner expression evaluation implementation. All expressions can simply be evaluated with the vm's `$scope` as the `this` context. + +This is very useful, but it probably should only be available in implicit child instances created by flow-control directives like `v-repeat`, `v-if`, etc. Explicit components should retain its own root scope and use some sort of two way binding like `v-with` to bind to data from outer scope. - new option: `syncData`. -Each Vue instance now creates an associated `$scope` object which has prototypal inheritance similar to Angular. This makes expression evaluation much cleaner. A side effect of this change is that the `data` object being passed in is no longer mutated by default. You need to now explicitly pass in `syncData: true` in the options for direct property changes on the scope to be synced back to the root data object. In most cases, this is not necessary. +A side effect of the new scope/data model is that the `data` object being passed in is no longer mutated by default, because all its properties are copied into the scope instead. To sync changes to the scope back to the original data object, you need to now explicitly pass in `syncData: true` in the options. In most cases, this is not necessary, but you do need to be aware of this. + +## Two Way filters + +``` html + +``` + +``` js +Vue.filter('format', { + read: function (val) { + return val + '!' + }, + write: function (val, oldVal) { + return val.match(/ok/) ? val : oldVal + } +}) +``` + +## Block logic control + +``` html + +

{{title}}

+

{{content}}

+ +``` + +``` html + + + + +``` -## More flexible directive syntax +## (Experimental) More flexible directive syntax - v-on @@ -57,47 +101,6 @@ Each Vue instance now creates an associated `$scope` object which has prototypal ``` -## Two Way filters - -``` html - -``` - -``` js -Vue.filter('format', { - read: function (val) { - return val + '!' - }, - write: function (val, oldVal) { - return val.match(/ok/) ? val : oldVal - } -}) -``` - -## Block logic control - -``` html - -

{{title}}

-

{{content}}

- -``` - -``` html - - - - -``` - -## (Experimental) New Scope Inheritance Model - -In the previous version, nested Vue instances do not have prototypal inheritance of their data scope. Although you can access parent data properties in templates, you need to explicitly travel up the scope chain with `this.$parent` in JavaScript code or use `this.$get()` to get a property on the scope chain. The expression parser also needs to do a lot of dirty work to determine the correct scope the variables belong to. - -In the new model, we provide a scope inehritance system similar to Angular, in which you can directly access properties that exist on parent scopes. The major difference is that setting a primitive value property on a child scope WILL affect that on the parent scope! This is one of the major gotchas in Angular. If you are somewhat familiar with how prototype inehritance works, you might be surprised how this is possible. Well, the reason is that all data properties in Vue are getter/setters, and invoking a setter will not cause the child scope shadowing parent scopes. See the example [here](http://jsfiddle.net/yyx990803/Px2n6/). - -This is very powerful, but probably should only be available in implicit child instances created by `v-repeat` and `v-if`. Explicit components should retain its own root scope and use some sort of two way binding like `v-with` to sync with outer scope. - ## (Experimental) Validators ``` html From 5bff199db9d729b85c10093d763d5bddda2c1b4e Mon Sep 17 00:00:00 2001 From: Evan You Date: Sun, 20 Jul 2014 16:42:14 -0400 Subject: [PATCH 0044/1534] path and cache --- src/binding.js | 2 +- src/cache.js | 125 ++++++++++++++++++++ src/instance/bindings.js | 15 ++- src/parse/path.js | 90 ++++++++++---- test/unit/binding_spec.js | 3 + test/unit/cache_spec.js | 3 + test/unit/{observer.js => observer_spec.js} | 4 + test/unit/path_spec.js | 3 + test/unit/scope_spec.js | 3 + 9 files changed, 221 insertions(+), 27 deletions(-) create mode 100644 src/cache.js create mode 100644 test/unit/binding_spec.js create mode 100644 test/unit/cache_spec.js rename test/unit/{observer.js => observer_spec.js} (99%) create mode 100644 test/unit/path_spec.js create mode 100644 test/unit/scope_spec.js diff --git a/src/binding.js b/src/binding.js index 7af308387a3..cdf74867fc1 100644 --- a/src/binding.js +++ b/src/binding.js @@ -30,7 +30,7 @@ var p = Binding.prototype */ p.addChild = function (key, child) { - + this.children[key] = child } /** diff --git a/src/cache.js b/src/cache.js new file mode 100644 index 00000000000..9c8fa864bec --- /dev/null +++ b/src/cache.js @@ -0,0 +1,125 @@ +/** + * A doubly linked list-based Least Recently Used (LRU) cache. + * Will keep most recently used items while discarding least + * recently used items when its limit is reached. + * + * Licensed under MIT. + * Copyright (c) 2010 Rasmus Andersson + * + * Illustration of the design: + * + * entry entry entry entry + * ______ ______ ______ ______ + * | head |.newer => | |.newer => | |.newer => | tail | + * | A | | B | | C | | D | + * |______| <= older.|______| <= older.|______| <= older.|______| + * + * removed <-- <-- <-- <-- <-- <-- <-- <-- <-- <-- <-- added + * + * @param {Number} limit + * @constructor + */ + +function Cache (limit) { + this.size = 0 + this.limit = limit + this.head = this.tail = undefined + this._keymap = {} +} + +var p = Cache.prototype + +/** + * Put into the cache associated with . + * Returns the entry which was removed to make room for + * the new entry. Otherwise undefined is returned. + * (i.e. if there was enough room already). + * + * @param {String} key + * @param {*} value + * @return {Entry|undefined} + */ + +p.put = function (key, value) { + var entry = { + key:key, + value:value + } + this._keymap[key] = entry + if (this.tail) { + this.tail.newer = entry + entry.older = this.tail + } else { + this.head = entry + } + this.tail = entry + if (this.size === this.limit) { + return this.shift() + } else { + this.size++ + } +} + +/** + * Purge the least recently used (oldest) entry from the cache. + * Returns the removed entry or undefined if the cache was empty. + */ + +p.shift = function () { + var entry = this.head + if (entry) { + if (this.head.newer) { + this.head = this.head.newer + this.head.older = undefined + } else { + this.head = undefined + } + entry.newer = entry.older = undefined + this._keymap[entry.key] = undefined + } + return entry +} + +/** + * Get and register recent use of . Returns the value + * associated with or undefined if not in cache. + * + * @param {String} key + * @param {Boolean} returnEntry + * @return {Entry|*} + */ + +p.get = function (key, returnEntry) { + var entry = this._keymap[key] + if (entry === undefined) return + console.log('cache hit!') + if (entry === this.tail) { + return returnEntry + ? entry + : entry.value + } + // HEAD--------------TAIL + // <.older .newer> + // <--- add direction -- + // A B C E + if (entry.newer) { + if (entry === this.head) { + this.head = entry.newer + } + entry.newer.older = entry.older // C <-- E. + } + if (entry.older) { + entry.older.newer = entry.newer // C. --> E + } + entry.newer = undefined // D --x + entry.older = this.tail // D. --> E + if (this.tail) { + this.tail.newer = entry // E. <-- D + } + this.tail = entry + return returnEntry + ? entry + : entry.value +} + +module.exports = Cache \ No newline at end of file diff --git a/src/instance/bindings.js b/src/instance/bindings.js index dca0f929a7f..3b754b82ba9 100644 --- a/src/instance/bindings.js +++ b/src/instance/bindings.js @@ -1,6 +1,5 @@ var Binding = require('../binding') var Observer = require('../observe/observer') -var Path = require('../parse/path') /** * Setup the binding tree. @@ -43,17 +42,25 @@ exports._initBindings = function () { /** * Create bindings along a path * - * @param {String|Array} path + * @param {Array} path - this should already be a parsed Array. */ exports._createBindings = function (path) { - + var b = this._rootBinding + var child + for (var i = 0, l = path.length; i < l; i++) { + child = new Binding() + b.addChild(path[i], child) + b = child + } } /** * Traverse the binding tree * - * @param {String} path + * @param {String} path - this path comes directly from the + * data observer, so it is a single string + * delimited by "\b". */ exports._updateBindings = function (path) { diff --git a/src/parse/path.js b/src/parse/path.js index ba7dd64aca0..1ae5240bbac 100644 --- a/src/parse/path.js +++ b/src/parse/path.js @@ -1,3 +1,11 @@ +var Cache = require('../cache') + +/** + * Path cache + */ + +var pathCache = new Cache(1000) + /** * Path-parsing algorithm scooped from Polymer/observe-js */ @@ -72,6 +80,13 @@ var pathStateMachine = { function noop () {} +/** + * Determine the type of a character in a keypath. + * + * @param {Char} char + * @return {String} type + */ + function getPathCharType (char) { if (char === undefined) return 'eof' @@ -121,20 +136,19 @@ function getPathCharType (char) { * @return {Array|undefined} */ -exports.parse = function (path) { +function parsePath (path) { var keys = [] var index = -1 var c, newChar, key, type, transition, action, typeMap, mode = 'beforePath' var actions = { push: function() { - if (key === undefined) + if (key === undefined) { return - + } keys.push(key) key = undefined }, - append: function() { if (key === undefined) key = newChar @@ -144,12 +158,12 @@ exports.parse = function (path) { } function maybeUnescapeQuote() { - if (index >= path.length) + if (index >= path.length) { return - + } var nextChar = path[index + 1] - if ((mode == 'inSingleQuote' && nextChar == "'") || - (mode == 'inDoubleQuote' && nextChar == '"')) { + if ((mode === 'inSingleQuote' && nextChar === "'") || + (mode === 'inDoubleQuote' && nextChar === '"')) { index++ newChar = nextChar actions.append() @@ -161,15 +175,17 @@ exports.parse = function (path) { index++ c = path[index] - if (c == '\\' && maybeUnescapeQuote(mode)) + if (c === '\\' && maybeUnescapeQuote(mode)) { continue + } type = getPathCharType(c) typeMap = pathStateMachine[mode] transition = typeMap[type] || typeMap['else'] || 'error' - if (transition == 'error') + if (transition === 'error') { return // parse error + } mode = transition[0] action = actions[transition[1]] || noop @@ -185,36 +201,66 @@ exports.parse = function (path) { } /** - * Get from an object from a path + * External parse that check for a cache hit first * - * @param {Object} obj * @param {String} path + * @return {Array|undefined} */ -exports.get = function (obj, path) { - +exports.parse = function (path) { + var hit = pathCache.get(path) + if (!hit) { + hit = parsePath(path) + pathCache.put(path, hit) + } + return hit } /** - * Set on an object from a path + * Get from an object from a path * * @param {Object} obj * @param {String} path - * @param {*} val */ -exports.set = function (obj, path, val) { - +exports.get = function (obj, path) { + if (typeof path === 'string') { + path = exports.parse(path) + } + if (!path) { + return + } + for (var i = 0, l = path.length; i < l; i++) { + if (obj == null) return + obj = obj[path[i]] + } + return obj } /** - * Traverse an object along a path and trigger callback + * Set on an object from a path * * @param {Object} obj * @param {String} path - * @param {Function} cb + * @param {*} val */ -exports.traverse = function (obj, path, cb) { - +exports.set = function (obj, path, val) { + if (typeof path === 'string') { + path = exports.parse(path) + } + if (!path) { + return + } + for (var i = 0, l = path.length - 1; i < l; i++) { + if (typeof obj !== 'object') { + return false + } + obj = obj[path[i]] + } + if (typeof obj !== 'object') { + return false + } + obj[path] = val + return true } \ No newline at end of file diff --git a/test/unit/binding_spec.js b/test/unit/binding_spec.js new file mode 100644 index 00000000000..487a2065821 --- /dev/null +++ b/test/unit/binding_spec.js @@ -0,0 +1,3 @@ +/** + * Test the binding class + */ \ No newline at end of file diff --git a/test/unit/cache_spec.js b/test/unit/cache_spec.js new file mode 100644 index 00000000000..ab86d37e4df --- /dev/null +++ b/test/unit/cache_spec.js @@ -0,0 +1,3 @@ +/** + * Test cache hit/miss/expiration + */ \ No newline at end of file diff --git a/test/unit/observer.js b/test/unit/observer_spec.js similarity index 99% rename from test/unit/observer.js rename to test/unit/observer_spec.js index 55e1bc03e3c..f472bc86771 100644 --- a/test/unit/observer.js +++ b/test/unit/observer_spec.js @@ -1,3 +1,7 @@ +/** + * Test data observation. + */ + var Observer = require('../../src/observe/observer') // internal emitter has fixed 3 arguments // so we need to fill up the assetions with undefined diff --git a/test/unit/path_spec.js b/test/unit/path_spec.js new file mode 100644 index 00000000000..99eb1be94ff --- /dev/null +++ b/test/unit/path_spec.js @@ -0,0 +1,3 @@ +/** + * Test path parsing + */ \ No newline at end of file diff --git a/test/unit/scope_spec.js b/test/unit/scope_spec.js new file mode 100644 index 00000000000..8ad67fa167a --- /dev/null +++ b/test/unit/scope_spec.js @@ -0,0 +1,3 @@ +/** + * Test scope inheritance, data event propagation and data sync + */ \ No newline at end of file From 14d97d4d3582825efa112ba13fedaafec49d82f7 Mon Sep 17 00:00:00 2001 From: Evan You Date: Mon, 21 Jul 2014 00:28:44 -0400 Subject: [PATCH 0045/1534] binding --- src/binding.js | 49 ++++++++++++++++++++++------------------ src/directive.js | 4 ++-- src/instance/bindings.js | 18 +++++++++++---- 3 files changed, 42 insertions(+), 29 deletions(-) diff --git a/src/binding.js b/src/binding.js index cdf74867fc1..7d4eb056dc3 100644 --- a/src/binding.js +++ b/src/binding.js @@ -22,35 +22,38 @@ function Binding () { var p = Binding.prototype -/** - * Add a child binding to the tree. - * - * @param {String} key - * @param {Binding} child - */ - -p.addChild = function (key, child) { - this.children[key] = child -} - /** * Traverse along a path and trigger updates * along the way. * - * @param {String} path + * @param {Array} path */ p.updatePath = function (path) { - + var b = this + for (var i = 0, l = path.length; i < l; i++) { + if (!b) return + b.notify() + b = b.children[path[i]] + } + // for the destination of path, we need to trigger + // change for every children. i.e. when an object is + // swapped, all its content need to be updated. + if (b) { + b.updateSubTree() + } } /** * Trigger updates for the subtree starting at - * self as root. + * self as root. Recursive. */ -p.updateSubTree = function () { - +p.updateTree = function () { + this.notify() + for (var key in this.children) { + this.children[key].updateTree() + } } /** @@ -59,8 +62,8 @@ p.updateSubTree = function () { * @param {Directive} sub */ -p.addSubscriber = function (sub) { - +p.addSub = function (sub) { + this.subs.push(sub) } /** @@ -69,16 +72,18 @@ p.addSubscriber = function (sub) { * @param {Directive} sub */ -p.removeSubscriber = function (sub) { - +p.removeSub = function (sub) { + this.subs.splice(this.subs.indexOf(sub), 1) } /** * Notify all subscribers of a new value. */ -p.publish = function () { - +p.notify = function () { + for (var i = 0, l = this.subs.length; i++) { + this.subs[i]._update() + } } module.exports = Binding \ No newline at end of file diff --git a/src/directive.js b/src/directive.js index 6fd3a74f84d..3f4b403365a 100644 --- a/src/directive.js +++ b/src/directive.js @@ -4,14 +4,14 @@ * a list of dependencies (Bindings) and refreshes the list during * its getter evaluation. * - * @param {String} id + * @param {String} type * @param {Node} el * @param {Vue} vm * @param {String} expression * @constructor */ -function Directive (id, el, vm, expression) { +function Directive (type, el, vm, expression) { } diff --git a/src/instance/bindings.js b/src/instance/bindings.js index 3b754b82ba9..da68143f98f 100644 --- a/src/instance/bindings.js +++ b/src/instance/bindings.js @@ -40,19 +40,27 @@ exports._initBindings = function () { } /** - * Create bindings along a path + * Create a binding for a given path, and create necessary + * parent bindings along the path. Returns the binding at + * the end of thep path. * * @param {Array} path - this should already be a parsed Array. + * @return {Binding} - the binding created/retrieved at the destination. */ -exports._createBindings = function (path) { +exports._createBindingAt = function (path) { var b = this._rootBinding - var child + var child, key for (var i = 0, l = path.length; i < l; i++) { - child = new Binding() - b.addChild(path[i], child) + key = path[i] + child = b.children[key] + if (!child) { + child = new Binding() + b.children[key] = child + } b = child } + return b } /** From 611e2b3db463de25b0717af676b867d59a0023e5 Mon Sep 17 00:00:00 2001 From: Evan You Date: Mon, 21 Jul 2014 11:49:31 -0400 Subject: [PATCH 0046/1534] fix binding --- src/binding.js | 8 ++++---- src/instance/bindings.js | 22 +++++++++++----------- 2 files changed, 15 insertions(+), 15 deletions(-) diff --git a/src/binding.js b/src/binding.js index 7d4eb056dc3..45007b32482 100644 --- a/src/binding.js +++ b/src/binding.js @@ -15,7 +15,7 @@ var uid = 0 */ function Binding () { - this.uid = uid++ + this.id = uid++ this.children = Object.create(null) this.subs = [] } @@ -40,7 +40,7 @@ p.updatePath = function (path) { // change for every children. i.e. when an object is // swapped, all its content need to be updated. if (b) { - b.updateSubTree() + b.updateTree() } } @@ -81,8 +81,8 @@ p.removeSub = function (sub) { */ p.notify = function () { - for (var i = 0, l = this.subs.length; i++) { - this.subs[i]._update() + for (var i = 0, l = this.subs.length; i < l; i++) { + this.subs[i]._update(this) } } diff --git a/src/instance/bindings.js b/src/instance/bindings.js index da68143f98f..6d1eb6ea1c9 100644 --- a/src/instance/bindings.js +++ b/src/instance/bindings.js @@ -21,22 +21,22 @@ var Observer = require('../observe/observer') exports._initBindings = function () { var root = this._rootBinding = new Binding() // the $data binding points to the root itself! - root.addChild('$data', root) + root.children.$data = root // point $parent and $root bindings to their // repective owners. if (this.$parent) { - root.addChild('$parent', this.$parent._rootBinding) - root.addChild('$root', this.$root._rootBinding) + root.children.$parent = this.$parent._rootBinding + root.children.$root = this.$root._rootBinding } var self = this - var updateBindings = function (path) { - self._updateBindings(path) + var updateBinding = function (path) { + self._updateBinding(path) } this._observer - .on('set', updateBindings) - .on('add', updateBindings) - .on('delete', updateBindings) - .on('mutate', updateBindings) + .on('set', updateBinding) + .on('add', updateBinding) + .on('delete', updateBinding) + .on('mutate', updateBinding) } /** @@ -64,14 +64,14 @@ exports._createBindingAt = function (path) { } /** - * Traverse the binding tree + * Trigger a path update on the root binding. * * @param {String} path - this path comes directly from the * data observer, so it is a single string * delimited by "\b". */ -exports._updateBindings = function (path) { +exports._updateBinding = function (path) { path = path.split(Observer.pathDelimiter) this._rootBinding.updatePath(path) } \ No newline at end of file From b624a87845b06de46fc6f80e7271a933bae5573c Mon Sep 17 00:00:00 2001 From: Evan You Date: Mon, 21 Jul 2014 11:58:35 -0400 Subject: [PATCH 0047/1534] binding --- src/binding.js | 24 ++++++++++++++++++++++++ src/instance/bindings.js | 14 +++++--------- 2 files changed, 29 insertions(+), 9 deletions(-) diff --git a/src/binding.js b/src/binding.js index 45007b32482..f3db9122e25 100644 --- a/src/binding.js +++ b/src/binding.js @@ -22,6 +22,30 @@ function Binding () { var p = Binding.prototype +/** + * Add a child binding to the tree. + * + * @param {String} key + * @param {Binding} child + */ + +p.addChild = function (key, child) { + child = child || new Binding() + this.children[key] = child + return child +} + +/** + * Return the child at the given key + * + * @param {String} key + * @return {Binding} + */ + +p.getChild = function (key) { + return this.children[key] +} + /** * Traverse along a path and trigger updates * along the way. diff --git a/src/instance/bindings.js b/src/instance/bindings.js index 6d1eb6ea1c9..799582e48e0 100644 --- a/src/instance/bindings.js +++ b/src/instance/bindings.js @@ -21,12 +21,12 @@ var Observer = require('../observe/observer') exports._initBindings = function () { var root = this._rootBinding = new Binding() // the $data binding points to the root itself! - root.children.$data = root + root.addChild('$data', root) // point $parent and $root bindings to their // repective owners. if (this.$parent) { - root.children.$parent = this.$parent._rootBinding - root.children.$root = this.$root._rootBinding + root.addChild('$parent', this.$parent._rootBinding) + root.addChild('$root', this.$root._rootBinding) } var self = this var updateBinding = function (path) { @@ -42,7 +42,7 @@ exports._initBindings = function () { /** * Create a binding for a given path, and create necessary * parent bindings along the path. Returns the binding at - * the end of thep path. + * the end of the path. * * @param {Array} path - this should already be a parsed Array. * @return {Binding} - the binding created/retrieved at the destination. @@ -53,11 +53,7 @@ exports._createBindingAt = function (path) { var child, key for (var i = 0, l = path.length; i < l; i++) { key = path[i] - child = b.children[key] - if (!child) { - child = new Binding() - b.children[key] = child - } + child = b.getChild(key) || b.addChild(key) b = child } return b From c5ca84011170adba866a6d4386e1129692cf2ec2 Mon Sep 17 00:00:00 2001 From: Evan You Date: Mon, 21 Jul 2014 12:19:03 -0400 Subject: [PATCH 0048/1534] callback context --- src/instance/bindings.js | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/instance/bindings.js b/src/instance/bindings.js index 799582e48e0..d67453d8e00 100644 --- a/src/instance/bindings.js +++ b/src/instance/bindings.js @@ -28,10 +28,8 @@ exports._initBindings = function () { root.addChild('$parent', this.$parent._rootBinding) root.addChild('$root', this.$root._rootBinding) } - var self = this - var updateBinding = function (path) { - self._updateBinding(path) - } + // observer already has callback context set to `this` + var updateBinding = this._updateBinding this._observer .on('set', updateBinding) .on('add', updateBinding) From ed1a9d6c3e1f3bb5498de24b3fa1da1f9d98bd80 Mon Sep 17 00:00:00 2001 From: Evan You Date: Mon, 21 Jul 2014 12:56:27 -0400 Subject: [PATCH 0049/1534] getBindingAt --- src/instance/bindings.js | 34 +++++++++++++++++++++++++++------- 1 file changed, 27 insertions(+), 7 deletions(-) diff --git a/src/instance/bindings.js b/src/instance/bindings.js index d67453d8e00..00e24bd6c57 100644 --- a/src/instance/bindings.js +++ b/src/instance/bindings.js @@ -38,25 +38,45 @@ exports._initBindings = function () { } /** - * Create a binding for a given path, and create necessary - * parent bindings along the path. Returns the binding at - * the end of the path. + * Retrive a binding at a given path. + * If `create` is true, create all bindings that do not + * exist yet along the way. * - * @param {Array} path - this should already be a parsed Array. - * @return {Binding} - the binding created/retrieved at the destination. + * @param {Array} path + * @param {Boolean} create + * @return {Binding|undefined} */ -exports._createBindingAt = function (path) { +exports._getBindingAt = function (path, create) { var b = this._rootBinding var child, key for (var i = 0, l = path.length; i < l; i++) { key = path[i] - child = b.getChild(key) || b.addChild(key) + child = b.getChild(key) + if (!child) { + if (create) { + child = b.addChild(key) + } else { + return + } + } b = child } return b } +/** + * Create a binding at a given path. Will also create + * all bindings that do not exist yet along the way. + * + * @param {Array} path + * @return {Binding} + */ + +exports._createBindingAt = function (path) { + return this._getBindingAt(path, true) +} + /** * Trigger a path update on the root binding. * From 5bb8607771765385252372c039b10ed1476a3851 Mon Sep 17 00:00:00 2001 From: Evan You Date: Tue, 22 Jul 2014 01:24:36 -0400 Subject: [PATCH 0050/1534] working on compile --- src/api/global.js | 2 +- src/api/lifecycle.js | 39 ++++++++++++++++-- src/instance/compile.js | 66 +++++++++++++++++++++++++++++- src/observe/array-augmentations.js | 3 +- src/util.js | 12 ++++++ src/vue.js | 9 ++++ 6 files changed, 123 insertions(+), 8 deletions(-) diff --git a/src/api/global.js b/src/api/global.js index bb99594ae4d..4309632e684 100644 --- a/src/api/global.js +++ b/src/api/global.js @@ -62,7 +62,7 @@ exports.use = function (plugin) { } } // additional parameters - var args = [].slice.call(arguments, 1) + var args = _.toArray(arguments, 1) args.unshift(this) if (typeof plugin.install === 'function') { plugin.install.apply(plugin, args) diff --git a/src/api/lifecycle.js b/src/api/lifecycle.js index 140f1554684..29d24e97d89 100644 --- a/src/api/lifecycle.js +++ b/src/api/lifecycle.js @@ -1,7 +1,38 @@ -exports.$mount = function () { - +var _ = require('../util') + +/** + * Set instance target element and kick off the compilation process. + * The passed in `el` can be a selector string, an existing Element, + * or a DocumentFragment (for block instances). + * + * @param {Element|DocumentFragment|string} el + * @public + */ + +exports.$mount = function (el) { + if (typeof el === 'string') { + el = document.querySelector(el) + } + // If the passed in `el` is a DocumentFragment, the instance is + // considered a "block instance" which manages not a single element, + // but multiple elements. A block instance's `$el` is an Array of + // the elements it manages. + this._isBlock = el instanceof DocumentFragment + this.$el = this._isBlock + ? _.toArray(el.childNodes) + : el + this._compile() + this._isMounted = true } -exports.$destroy = function () { - +/** + * Teardown an instance, unobserves the data, unbind all the + * directives, turn off all the event listeners, etc. + * + * @param {Boolean} remove - whether to remove the DOM node. + * @public + */ + +exports.$destroy = function (remove) { + } \ No newline at end of file diff --git a/src/instance/compile.js b/src/instance/compile.js index 751d94434ab..1b92c33cdc0 100644 --- a/src/instance/compile.js +++ b/src/instance/compile.js @@ -1,7 +1,71 @@ +var config = require('../config') + /** - * Compile the instance's template. + * The main entrance to the compilation process. + * Calling this function requires the instance's `$el` to + * be already set up, and it should be called only once + * during an instance's entire lifecycle. */ exports._compile = function () { + if (this._isBlock) { + this.$el.forEach(this._compileNode, this) + } else { + this._compileNode(this.$el) + } +} + +/** + * Generic compile function. Determines the actual compile + * function to use based on the nodeType. + * + * @param {Node} node + */ + +exports._compileNode = function (node) { + switch (node.nodeType) { + case 1: // element + if (node.tagName !== 'SCRIPT') { + this._compileElement(node) + } + break + case 3: // text + if (config.interpolate) { + this._compileTextNode(node) + } + break + case 8: // comment + this._compileComment(node) + break + } +} + +/** + * Compile an HTMLElement + * + * @param {HTMLElement} node + */ + +exports._compileElement = function (node) { + +} + +/** + * Compile a TextNode + * + * @param {TextNode} node + */ + +exports._compileTextNode = function (node) { + +} + +/** + * Compile a comment node (check for block flow-controls) + * + * @param {CommentNode} node + */ + +exports._compileComment = function (node) { } \ No newline at end of file diff --git a/src/observe/array-augmentations.js b/src/observe/array-augmentations.js index c4458e6b7c1..e3b2d2c4cb3 100644 --- a/src/observe/array-augmentations.js +++ b/src/observe/array-augmentations.js @@ -1,5 +1,4 @@ var _ = require('../util') -var slice = [].slice var arrayAugmentations = Object.create(Array.prototype) /** @@ -21,7 +20,7 @@ var arrayAugmentations = Object.create(Array.prototype) // define wrapped method _.define(arrayAugmentations, method, function () { - var args = slice.call(arguments) + var args = _.toArray(arguments) var result = original.apply(this, args) var ob = this.$observer var inserted, removed, index diff --git a/src/util.js b/src/util.js index c029db6f85c..c43abba864b 100644 --- a/src/util.js +++ b/src/util.js @@ -1,4 +1,16 @@ var config = require('./config') +var slice = [].slice + +/** + * Convert an Array-like object to a real Array. + * + * @param {Array-like} list + * @param {Number} [i] - start index + */ + +exports.toArray = function (list, i) { + return slice.call(list, i || 0) +} /** * Mix properties into target object. diff --git a/src/vue.js b/src/vue.js index cda083874b4..83f5d48ddb0 100644 --- a/src/vue.js +++ b/src/vue.js @@ -14,7 +14,12 @@ var _ = require('./util') */ function Vue (options) { + // instance options this.$options = options || {} + // general state + this._isBlock = false + this._isMounted = false + this._isDestroyed = false // create scope this._initScope() // setup initial data. @@ -23,6 +28,10 @@ function Vue (options) { this._initProxy() // setup binding tree this._initBindings() + // if `el` option is passed, start compilation. + if (this.$options.el) { + this.$mount(this.$options.el) + } } /** From cc6593f1231be27c0cf014ab1fb9417df2fd1db9 Mon Sep 17 00:00:00 2001 From: Evan You Date: Thu, 24 Jul 2014 17:30:31 -0400 Subject: [PATCH 0051/1534] instantiation tests wip --- src/instance/data.js | 2 +- src/instance/init.js | 66 ++++++++++++++++ src/vue.js | 20 +---- test/unit/instantiation_spec.js | 128 ++++++++++++++++++++++++++++++++ test/unit/scope_spec.js | 3 - 5 files changed, 197 insertions(+), 22 deletions(-) create mode 100644 src/instance/init.js create mode 100644 test/unit/instantiation_spec.js delete mode 100644 test/unit/scope_spec.js diff --git a/src/instance/data.js b/src/instance/data.js index 1e7fb529724..fad8ea353b5 100644 --- a/src/instance/data.js +++ b/src/instance/data.js @@ -49,8 +49,8 @@ exports._initData = function (data, init) { } // setup sync between scope and new data + this._data = data if (options.syncData) { - this._data = data this._dataObserver = Observer.create(data) this._sync() } diff --git a/src/instance/init.js b/src/instance/init.js new file mode 100644 index 00000000000..1bf33d87c27 --- /dev/null +++ b/src/instance/init.js @@ -0,0 +1,66 @@ +/** + * The main init sequence. This is called for every instance, + * including ones that are created from extended constructors. + * + * @param {Object} options - this options object should be the + * result of merging class options + * and the options passed in to the + * constructor. + */ + +exports._init = function (options) { + + /** + * Expose instance options. + * @type {Object} + * + * @public + */ + + this.$options = options || {} + + /** + * Indicates whether this is a block instance. (One with more + * than one top-level nodes) + * + * @type {Boolean} + * @private + */ + + this._isBlock = false + + /** + * Indicates whether the instance has been mounted to a DOM node. + * + * @type {Boolean} + * @private + */ + + this._isMounted = false + + /** + * Indicates whether the instance has been destroyed. + * + * @type {Boolean} + * @private + */ + + this._isDestroyed = false + + // create scope. + this._initScope() + + // setup initial data. + this._initData(this.$options.data || {}, true) + + // setup property proxying + this._initProxy() + + // setup binding tree. + this._initBindings() + + // if `el` option is passed, start compilation. + if (this.$options.el) { + this.$mount(this.$options.el) + } +} \ No newline at end of file diff --git a/src/vue.js b/src/vue.js index 83f5d48ddb0..1a6c9f50ee3 100644 --- a/src/vue.js +++ b/src/vue.js @@ -14,24 +14,7 @@ var _ = require('./util') */ function Vue (options) { - // instance options - this.$options = options || {} - // general state - this._isBlock = false - this._isMounted = false - this._isDestroyed = false - // create scope - this._initScope() - // setup initial data. - this._initData(this.$options.data || {}, true) - // setup property proxying - this._initProxy() - // setup binding tree - this._initBindings() - // if `el` option is passed, start compilation. - if (this.$options.el) { - this.$mount(this.$options.el) - } + this._init(options) } /** @@ -71,6 +54,7 @@ Object.defineProperty(p, '$data', { * Mixin internal instance methods */ +_.mixin(p, require('./instance/init')) _.mixin(p, require('./instance/scope')) _.mixin(p, require('./instance/data')) _.mixin(p, require('./instance/proxy')) diff --git a/test/unit/instantiation_spec.js b/test/unit/instantiation_spec.js new file mode 100644 index 00000000000..fa0267fc0c1 --- /dev/null +++ b/test/unit/instantiation_spec.js @@ -0,0 +1,128 @@ +/** + * Test property proxy, scope inheritance, + * data event propagation and data sync + */ + +var Vue = require('../../src/vue') +var Observer = require('../../src/observe/observer') +var u = undefined +Observer.pathDelimiter = '.' + +describe('Scope', function () { + + describe('basic', function () { + + var vm = new Vue({ + data: { + a: 1, + b: { + c: 2 + } + } + }) + + it('should copy over data properties', function () { + expect(vm.$scope.a).toEqual(vm.$data.a) + expect(vm.$scope.b).toEqual(vm.$data.b) + }) + + it('should proxy these properties', function () { + expect(vm.a).toEqual(vm.$scope.a) + expect(vm.b).toEqual(vm.$scope.b) + }) + + it('should trigger set events', function () { + var spy = jasmine.createSpy('instantiation') + vm._observer.on('set', spy) + + // set on scope + vm.$scope.a = 2 + expect(spy.callCount).toEqual(1) + expect(spy).toHaveBeenCalledWith('a', 2, u) + + // set on vm + vm.b.c = 3 + expect(spy.callCount).toEqual(2) + expect(spy).toHaveBeenCalledWith('b.c', 3, u) + }) + + it('should trigger add/delete events', function () { + var spy = jasmine.createSpy('instantiation') + vm._observer + .on('add', spy) + .on('delete', spy) + + // add on scope + vm.$scope.$add('c', 123) + expect(spy.callCount).toEqual(1) + expect(spy).toHaveBeenCalledWith('c', 123, u) + + // delete on scope + vm.$scope.$delete('c') + expect(spy.callCount).toEqual(2) + expect(spy).toHaveBeenCalledWith('c', u, u) + + // vm $add/$delete are tested in the api suite + }) + + }) + + describe('data sync', function () { + + var data = { + a: 1, + b: { + c: 2 + } + } + + var vm = new Vue({ + syncData: true, + data: data + }) + + it('should retain data reference', function () { + expect(vm.$data).toEqual(data) + }) + + it('should sync set', function () { + // vm -> data + vm.a = 2 + expect(data.a).toEqual(2) + // data -> vm + data.b = {d:3} + expect(vm.$scope.b).toEqual(data.b) + expect(vm.b).toEqual(data.b) + }) + + it('should sync add', function () { + // vm -> data + vm.$scope.$add('c', 123) + expect(data.c).toEqual(123) + // data -> vm + data.$add('d', 456) + expect(vm.$scope.d).toEqual(456) + expect(vm.d).toEqual(456) + }) + + it('should sync delete', function () { + // vm -> data + vm.$scope.$delete('d') + expect(data.hasOwnProperty('d')).toBeFalsy() + // data -> vm + data.$delete('c') + expect(vm.$scope.hasOwnProperty('c')).toBeFalsy() + expect(vm.hasOwnProperty('c')).toBeFalsy() + }) + + }) + + describe('inheritance', function () { + // body... + }) + + describe('inheritance with data sync', function () { + // body... + }) + +}) \ No newline at end of file diff --git a/test/unit/scope_spec.js b/test/unit/scope_spec.js deleted file mode 100644 index 8ad67fa167a..00000000000 --- a/test/unit/scope_spec.js +++ /dev/null @@ -1,3 +0,0 @@ -/** - * Test scope inheritance, data event propagation and data sync - */ \ No newline at end of file From 437c65de9a23891de6dad59918ee199f87a4ff80 Mon Sep 17 00:00:00 2001 From: Evan You Date: Thu, 24 Jul 2014 18:14:06 -0400 Subject: [PATCH 0052/1534] test for data/scope & inheritance --- test/unit/data_spec.js | 235 ++++++++++++++++++++++++++++++++ test/unit/instantiation_spec.js | 128 ----------------- 2 files changed, 235 insertions(+), 128 deletions(-) create mode 100644 test/unit/data_spec.js delete mode 100644 test/unit/instantiation_spec.js diff --git a/test/unit/data_spec.js b/test/unit/data_spec.js new file mode 100644 index 00000000000..e5610d97c11 --- /dev/null +++ b/test/unit/data_spec.js @@ -0,0 +1,235 @@ +/** + * Test property proxy, scope inheritance, + * data event propagation and data sync + */ + +var Vue = require('../../src/vue') +var Observer = require('../../src/observe/observer') +var u = undefined +Observer.pathDelimiter = '.' + +describe('Scope', function () { + + describe('basic', function () { + + var vm = new Vue({ + data: { + a: 1, + b: { + c: 2 + } + } + }) + + it('should copy over data properties', function () { + expect(vm.$scope.a).toEqual(vm.$data.a) + expect(vm.$scope.b).toEqual(vm.$data.b) + }) + + it('should proxy these properties', function () { + expect(vm.a).toEqual(vm.$scope.a) + expect(vm.b).toEqual(vm.$scope.b) + }) + + it('should trigger set events', function () { + var spy = jasmine.createSpy('basic') + vm._observer.on('set', spy) + + // set on scope + vm.$scope.a = 2 + expect(spy.callCount).toEqual(1) + expect(spy).toHaveBeenCalledWith('a', 2, u) + + // set on vm + vm.b.c = 3 + expect(spy.callCount).toEqual(2) + expect(spy).toHaveBeenCalledWith('b.c', 3, u) + }) + + it('should trigger add/delete events', function () { + var spy = jasmine.createSpy('instantiation') + vm._observer + .on('add', spy) + .on('delete', spy) + + // add on scope + vm.$scope.$add('c', 123) + expect(spy.callCount).toEqual(1) + expect(spy).toHaveBeenCalledWith('c', 123, u) + + // delete on scope + vm.$scope.$delete('c') + expect(spy.callCount).toEqual(2) + expect(spy).toHaveBeenCalledWith('c', u, u) + + // vm $add/$delete are tested in the api suite + }) + + }) + + describe('data sync', function () { + + var data = { + a: 1, + b: { + c: 2 + } + } + + var vm = new Vue({ + syncData: true, + data: data + }) + + it('should retain data reference', function () { + expect(vm.$data).toEqual(data) + }) + + it('should sync set', function () { + // vm -> data + vm.a = 2 + expect(data.a).toEqual(2) + // data -> vm + data.b = {d:3} + expect(vm.$scope.b).toEqual(data.b) + expect(vm.b).toEqual(data.b) + }) + + it('should sync add', function () { + // vm -> data + vm.$scope.$add('c', 123) + expect(data.c).toEqual(123) + // data -> vm + data.$add('d', 456) + expect(vm.$scope.d).toEqual(456) + expect(vm.d).toEqual(456) + }) + + it('should sync delete', function () { + // vm -> data + vm.$scope.$delete('d') + expect(data.hasOwnProperty('d')).toBeFalsy() + // data -> vm + data.$delete('c') + expect(vm.$scope.hasOwnProperty('c')).toBeFalsy() + expect(vm.hasOwnProperty('c')).toBeFalsy() + }) + + }) + + describe('inheritance', function () { + + var parent = new Vue({ + data: { + a: 'parent a', + b: { c: 2 }, + c: 'parent c', + arr: [{a:1},{a:2}] + } + }) + + var child = new Vue({ + parent: parent, + data: { + a: 'child a' + } + }) + + it('child should inherit parent data on scope', function () { + expect(child.$scope.b).toEqual(parent.b) // object + expect(child.$scope.c).toEqual(parent.c) // primitive value + }) + + it('child should not ineherit data on instance', function () { + expect(child.b).toBeUndefined() + expect(child.c).toBeUndefined() + }) + + it('child should shadow parent property with same key', function () { + expect(parent.a).toEqual('parent a') + expect(child.$scope.a).toEqual('child a') + expect(child.a).toEqual('child a') + }) + + it('setting scope properties on child should affect parent', function () { + child.$scope.c = 'modified by child' + expect(parent.c).toEqual('modified by child') + }) + + it('events on parent should propagate down to child', function () { + // when a shadowed property changed on parent scope, + // the event should NOT be propagated down + var spy = jasmine.createSpy('inheritance') + child._observer.on('set', spy) + parent.c = 'c changed' + expect(spy.callCount).toEqual(1) + expect(spy).toHaveBeenCalledWith('c', 'c changed', u) + + spy = jasmine.createSpy('inheritance') + child._observer.on('add', spy) + parent.$scope.$add('e', 123) + expect(spy.callCount).toEqual(1) + expect(spy).toHaveBeenCalledWith('e', 123, u) + + spy = jasmine.createSpy('inheritance') + child._observer.on('delete', spy) + parent.$scope.$delete('e') + expect(spy.callCount).toEqual(1) + expect(spy).toHaveBeenCalledWith('e', u, u) + + spy = jasmine.createSpy('inheritance') + child._observer.on('mutate', spy) + parent.arr.reverse() + expect(spy.mostRecentCall.args[0]).toEqual('arr') + expect(spy.mostRecentCall.args[1]).toEqual(parent.arr) + expect(spy.mostRecentCall.args[2].method).toEqual('reverse') + + }) + + it('shadowed properties change on parent should not propagate down', function () { + // when a shadowed property changed on parent scope, + // the event should NOT be propagated down + var spy = jasmine.createSpy('inheritance') + child._observer.on('set', spy) + parent.a = 'a changed' + expect(spy.callCount).toEqual(0) + }) + + }) + + describe('inheritance with data sync on parent data', function () { + + var parent = new Vue({ + data: { + arr: [{a:1},{a:2}] + } + }) + + var child = new Vue({ + parent: parent, + syncData: true, + data: parent.arr[0] + }) + + it('should trigger proper events', function () { + + var parentSpy = jasmine.createSpy('parent') + var childSpy = jasmine.createSpy('child') + parent._observer.on('set', parentSpy) + child._observer.on('set', childSpy) + child.a = 3 + + // make sure data sync is working + expect(parent.arr[0].a).toEqual(3) + + expect(parentSpy.callCount).toEqual(1) + expect(parentSpy).toHaveBeenCalledWith('arr.0.a', 3, u) + + expect(childSpy.callCount).toEqual(2) + expect(childSpy).toHaveBeenCalledWith('a', 3, u) + expect(childSpy).toHaveBeenCalledWith('arr.0.a', 3, u) + }) + + }) + +}) \ No newline at end of file diff --git a/test/unit/instantiation_spec.js b/test/unit/instantiation_spec.js deleted file mode 100644 index fa0267fc0c1..00000000000 --- a/test/unit/instantiation_spec.js +++ /dev/null @@ -1,128 +0,0 @@ -/** - * Test property proxy, scope inheritance, - * data event propagation and data sync - */ - -var Vue = require('../../src/vue') -var Observer = require('../../src/observe/observer') -var u = undefined -Observer.pathDelimiter = '.' - -describe('Scope', function () { - - describe('basic', function () { - - var vm = new Vue({ - data: { - a: 1, - b: { - c: 2 - } - } - }) - - it('should copy over data properties', function () { - expect(vm.$scope.a).toEqual(vm.$data.a) - expect(vm.$scope.b).toEqual(vm.$data.b) - }) - - it('should proxy these properties', function () { - expect(vm.a).toEqual(vm.$scope.a) - expect(vm.b).toEqual(vm.$scope.b) - }) - - it('should trigger set events', function () { - var spy = jasmine.createSpy('instantiation') - vm._observer.on('set', spy) - - // set on scope - vm.$scope.a = 2 - expect(spy.callCount).toEqual(1) - expect(spy).toHaveBeenCalledWith('a', 2, u) - - // set on vm - vm.b.c = 3 - expect(spy.callCount).toEqual(2) - expect(spy).toHaveBeenCalledWith('b.c', 3, u) - }) - - it('should trigger add/delete events', function () { - var spy = jasmine.createSpy('instantiation') - vm._observer - .on('add', spy) - .on('delete', spy) - - // add on scope - vm.$scope.$add('c', 123) - expect(spy.callCount).toEqual(1) - expect(spy).toHaveBeenCalledWith('c', 123, u) - - // delete on scope - vm.$scope.$delete('c') - expect(spy.callCount).toEqual(2) - expect(spy).toHaveBeenCalledWith('c', u, u) - - // vm $add/$delete are tested in the api suite - }) - - }) - - describe('data sync', function () { - - var data = { - a: 1, - b: { - c: 2 - } - } - - var vm = new Vue({ - syncData: true, - data: data - }) - - it('should retain data reference', function () { - expect(vm.$data).toEqual(data) - }) - - it('should sync set', function () { - // vm -> data - vm.a = 2 - expect(data.a).toEqual(2) - // data -> vm - data.b = {d:3} - expect(vm.$scope.b).toEqual(data.b) - expect(vm.b).toEqual(data.b) - }) - - it('should sync add', function () { - // vm -> data - vm.$scope.$add('c', 123) - expect(data.c).toEqual(123) - // data -> vm - data.$add('d', 456) - expect(vm.$scope.d).toEqual(456) - expect(vm.d).toEqual(456) - }) - - it('should sync delete', function () { - // vm -> data - vm.$scope.$delete('d') - expect(data.hasOwnProperty('d')).toBeFalsy() - // data -> vm - data.$delete('c') - expect(vm.$scope.hasOwnProperty('c')).toBeFalsy() - expect(vm.hasOwnProperty('c')).toBeFalsy() - }) - - }) - - describe('inheritance', function () { - // body... - }) - - describe('inheritance with data sync', function () { - // body... - }) - -}) \ No newline at end of file From c7fba5ffc48fe6040eb8f0730090935875bd4bdb Mon Sep 17 00:00:00 2001 From: Evan You Date: Sat, 26 Jul 2014 00:26:02 -0400 Subject: [PATCH 0053/1534] working on template and element processing --- package.json | 3 +- src/api/lifecycle.js | 12 +-- src/instance/element.js | 98 +++++++++++++++++++++++++ src/instance/init.js | 11 +++ src/parse/template.js | 157 ++++++++++++++++++++++++++++++++++++++++ src/util.js | 68 +++++++++++++++++ 6 files changed, 337 insertions(+), 12 deletions(-) create mode 100644 src/instance/element.js diff --git a/package.json b/package.json index ef5cd7f5cd6..611120da57b 100644 --- a/package.json +++ b/package.json @@ -17,7 +17,8 @@ "bugs": "https://github.com/yyx990803/vue/issues", "homepage": "http://vuejs.org", "scripts": { - "test": "grunt ci" + "test": "grunt ci", + "jasmine": "jasmine-node test/unit/ --verbose" }, "devDependencies": { "browserify": "^4.2.0", diff --git a/src/api/lifecycle.js b/src/api/lifecycle.js index 29d24e97d89..b6937eb830d 100644 --- a/src/api/lifecycle.js +++ b/src/api/lifecycle.js @@ -10,17 +10,7 @@ var _ = require('../util') */ exports.$mount = function (el) { - if (typeof el === 'string') { - el = document.querySelector(el) - } - // If the passed in `el` is a DocumentFragment, the instance is - // considered a "block instance" which manages not a single element, - // but multiple elements. A block instance's `$el` is an Array of - // the elements it manages. - this._isBlock = el instanceof DocumentFragment - this.$el = this._isBlock - ? _.toArray(el.childNodes) - : el + this._initElement(el) this._compile() this._isMounted = true } diff --git a/src/instance/element.js b/src/instance/element.js new file mode 100644 index 00000000000..ef76961e5fd --- /dev/null +++ b/src/instance/element.js @@ -0,0 +1,98 @@ +var _ = require('../util') +var templateParser = require('../parse/template') + +/** + * Setup the instance's element before compilation. + * 1. Setup $el + * 2. Process the template option + * 3. Resolve insertion points + * + * @param {Node|String} el + */ + +exports._initElement = function (el) { + if (typeof el === 'string') { + el = document.querySelector(el) + } + // If the passed in `el` is a DocumentFragment, the instance is + // considered a "block instance" which manages not a single element, + // but multiple elements. A block instance's `$el` is an Array of + // the elements it manages. + if (el instanceof DocumentFragment) { + this._isBlock = true + this.$el = _.toArray(el.childNodes) + } else { + this.$el = el + } + this._initTemplate() + this._initContent() +} + +/** + * Process the template option. + * If the replace option is true this will also modify the $el. + */ + +exports._initTemplate = function () { + var el = this.$el + var options = this.$options + var template = options.template + if (template) { + var frag = templateParser.parse(template) + if (!frag) { + _.warn('Invalid template option: ' + template) + } else { + // collect raw content. this wipes out the container el. + this._collectRawContent() + frag = frag.cloneNode(true) + if (options.replace) { + // replace + if (frag.childNodes.length > 1) { + // the template contains multiple nodes + // in this case the original `el` is simply + // a placeholder. + this._isBlock = true + this.$el = _.toArray(frag.childNodes) + } else { + // 1 to 1 replace, we need to copy all the + // attributes from the original el to the replacer + this.$el = frag.firstChild + _.copyAttributes(el, this.$el) + } + if (el.parentNode) { + _.before(this.$el, el) + _.remove(el) + } + } else { + // simply insert. + el.appendChild(frag) + } + } + } +} + +/** + * Collect raw content inside $el before they are + * replaced by template content. + */ + +exports._collectRawContent = function () { + if (el.hasChildNodes()) { + this.rawContent = document.createElement('div') + /* jshint boss: true */ + while (child = el.firstChild) { + this.rawContent.appendChild(child) + } + } +} + +/** + * Resolve insertion points per W3C Web Components + * working draft: + * + * http://www.w3.org/TR/2013/WD-components-intro-20130606/#insertion-points + */ + +exports._initContent = function () { + // TODO +} \ No newline at end of file diff --git a/src/instance/init.js b/src/instance/init.js index 1bf33d87c27..37c696988d7 100644 --- a/src/instance/init.js +++ b/src/instance/init.js @@ -47,6 +47,17 @@ exports._init = function (options) { this._isDestroyed = false + /** + * If the instance has a template option, the raw content it holds + * before compilation will be preserved so they can be queried against + * during content insertion. + * + * @type {DocumentFragment} + * @private + */ + + this._rawContent = null + // create scope. this._initScope() diff --git a/src/parse/template.js b/src/parse/template.js index e69de29bb2d..10d8cb86824 100644 --- a/src/parse/template.js +++ b/src/parse/template.js @@ -0,0 +1,157 @@ +var Cache = require('../cache') +var templateCache = new Cache(100) + +var map = { + legend : [1, '
', '
'], + tr : [2, '', '
'], + col : [2, '', '
'], + _default : [0, '', ''] +} + +map.td = +map.th = [3, '', '
'] + +map.option = +map.optgroup = [1, ''] + +map.thead = +map.tbody = +map.colgroup = +map.caption = +map.tfoot = [1, '', '
'] + +map.text = +map.circle = +map.ellipse = +map.line = +map.path = +map.polygon = +map.polyline = +map.rect = [1, '',''] + +var TAG_RE = /<([\w:]+)/ + +/** + * Convert a string template to a DocumentFragment. + * Determines correct wrapping by tag types. Wrapping strategy + * originally from jQuery, scooped from component/domify. + * + * @param {String} templateString + * @return {DocumentFragment} + */ + +function stringToFragment (templateString) { + // try a cache hit first + var hit = templateCache.get(templateString) + if (hit) { + return hit + } + + var frag = document.createDocumentFragment() + var tagMatch = TAG_RE.exec(templateString) + + if (!tagMatch) { + // text only, return a single text node. + frag.appendChild(document.createTextNode(templateString)) + } else { + + var tag = tagMatch[1] + var wrap = map[tag] || map._default + var depth = wrap[0] + var prefix = wrap[1] + var suffix = wrap[2] + var node = document.createElement('div') + + node.innerHTML = prefix + templateString.trim() + suffix + while (depth--) { + node = node.lastChild + } + + if (node.firstChild === node.lastChild) { + // one element + frag.appendChild(node.firstChild) + templateCache.put(templateString, frag) + return frag + } else { + // multiple nodes, return a fragment + /* jshint boss: true */ + var child + while (child = node.firstChild) { + if (node.nodeType === 1) { + frag.appendChild(child) + } + } + } + } + templateCache.put(templateString, frag) + return frag +} + +/** + * Convert a template node to a DocumentFragment. + * + * @param {Node} node + * @return {DocumentFragment} + */ + +function nodeToFragment (node) { + var tag = node.tagName + // if its a template tag and the browser supports it, + // its content is already a document fragment. + if (tag === 'TEMPLATE' && node.content) { + return node.content + } + // script tag + if (tag === 'SCRIPT') { + return stringToFragment(node.textContent) + } + // non-script node. not recommended... + return toFragment(node.outerHTML) +} + +/** + * Process the template option and normalizes it into a + * a DocumentFragment that can be used as a partial or a + * instance template. + * + * @param {*} template + * Possible values include: + * - DocumentFragment object + * - Node object of type Template + * - id selector: '#some-template-id' + * - template string: '
my template
' + * @return {DocumentFragment|undefined} + */ + +exports.parse = function (template) { + var node, frag + + // if the template is already a document fragment -- do nothing + if (template instanceof DocumentFragment) { + return template + } + + if (typeof template === 'string') { + // id selector + if (template.charAt(0) === '#') { + // id selector can be cached too + frag = templateCache.get(template) + if (!frag) { + node = document.getElementById(template.slice(1)) + if (node) { + frag = nodeToFragment(node) + // save selector to cache + templateCache.put(template, frag) + } + } + } else { + // normal string template + frag = stringToFragment(template) + } + } else if (template.nodeType) { + // a direct node + frag = nodeToFragment(template) + } + + return frag +} \ No newline at end of file diff --git a/src/util.js b/src/util.js index c43abba864b..f268595873d 100644 --- a/src/util.js +++ b/src/util.js @@ -137,6 +137,74 @@ exports.mergeOptions = function (parent, child, noRecurse) { // - use prototypal inheritance where appropriate } +/** + * Insert el before target + * + * @param {Element} el + * @param {Element} target + */ + +exports.before = function (el, target) { + target.parentNode.insertBefore(el, target) +} + +/** + * Insert el after target + * + * @param {Element} el + * @param {Element} target + */ + +exports.after = function (el, target) { + if (target.nextSibling) { + exports.before(el, target.nextSibling) + } else { + target.parentNode.appendChild(el) + } +} + +/** + * Remove el from DOM + * + * @param {Element} el + */ + +exports.remove = function (el) { + el.parentNode.removeChild(el) +} + +/** + * Prepend el to target + * + * @param {Element} el + * @param {Element} target + */ + +exports.prepend = function (el, target) { + if (target.firstChild) { + exports.before(el, target.firstChild) + } else { + target.appendChild(el) + } +} + +/** + * Copy attributes from one element to another. + * + * @param {Element} from + * @param {Element} to + */ + +exports.copyAttributes = function (from, to) { + if (from.hasAttributes()) { + var attrs = from.attributes + for (var i = 0, l = attrs.length; i < l; i++) { + var attr = attrs[i] + to.setAttribute(attr.name, attr.value) + } + } +} + /** * Enable debug utilities. The enableDebug() function and all * _.log() & _.warn() calls will be dropped in the minified From ed688dc1222dde47384fc39bd07f56c1712884eb Mon Sep 17 00:00:00 2001 From: Evan You Date: Sat, 26 Jul 2014 00:41:19 -0400 Subject: [PATCH 0054/1534] batcher --- src/batcher.js | 77 ++++++++++++++++++++++++++++++++++++++++++++++++++ src/util.js | 14 +++++++++ 2 files changed, 91 insertions(+) diff --git a/src/batcher.js b/src/batcher.js index e69de29bb2d..698e66a525b 100644 --- a/src/batcher.js +++ b/src/batcher.js @@ -0,0 +1,77 @@ +var _ = require('./util') + +/** + * The Batcher maintains a job queue to be executed + * async on the next event loop. + */ + +function Batcher () { + this._preFlush = null + this.reset() +} + +var p = Batcher.prototype + +/** + * Push a job into the job queue. + * Jobs with duplicate IDs will be skipped, however we can + * use the `override` option to override existing jobs. + * + * @param {Object} job + * properties: + * - {String|Number} id + * - {Boolean} override + * - {Function} execute + */ + +p.push = function (job) { + if (!job.id || !this.has[job.id]) { + this.queue.push(job) + this.has[job.id] = job + if (!this.waiting) { + this.waiting = true + var self = this + _.nextTick(function () { + self.flush() + }) + } + } else if (job.override) { + var oldJob = this.has[job.id] + oldJob.cancelled = true + this.queue.push(job) + this.has[job.id] = job + } +} + +/** + * Flush the queue and run the jobs. + * Will call a preFlush hook if has one. + */ + +p.flush = function () { + // before flush hook + if (this._preFlush) { + this._preFlush() + } + // do not cache length because more jobs might be pushed + // as we execute existing jobs + for (var i = 0; i < this.queue.length; i++) { + var job = this.queue[i] + if (!job.cancelled) { + job.execute() + } + } + this.reset() +} + +/** + * Reset the batcher's state. + */ + +p.reset = function () { + this.has = {} + this.queue = [] + this.waiting = false +} + +module.exports = Batcher \ No newline at end of file diff --git a/src/util.js b/src/util.js index f268595873d..2c55909b91f 100644 --- a/src/util.js +++ b/src/util.js @@ -1,5 +1,19 @@ var config = require('./config') var slice = [].slice +var defer = + win.requestAnimationFrame || + win.webkitRequestAnimationFrame || + win.setTimeout + +/** + * Defer a task to the start of the next event loop + * + * @param {Function} fn + */ + +exports.nextTick = function (fn) { + return defer(fn, 0) +} /** * Convert an Array-like object to a real Array. From 405b5fe36d89256797188f332df81969fb3c5a99 Mon Sep 17 00:00:00 2001 From: Evan You Date: Sat, 26 Jul 2014 11:29:56 -0400 Subject: [PATCH 0055/1534] test for template parser --- gruntfile.js | 2 +- src/cache.js | 1 - src/parse/template.js | 27 +++---- src/util.js | 13 ++-- test/unit/data_spec.js | 68 +++++++++--------- test/unit/observer_spec.js | 136 +++++++++++++++++------------------ test/unit/template-parser.js | 103 ++++++++++++++++++++++++++ 7 files changed, 222 insertions(+), 128 deletions(-) create mode 100644 test/unit/template-parser.js diff --git a/gruntfile.js b/gruntfile.js index 2cc6e229b76..d646460a817 100644 --- a/gruntfile.js +++ b/gruntfile.js @@ -14,7 +14,7 @@ module.exports = function (grunt) { src: 'src/**/*.js' }, test: { - src: 'test/*/specs/*.js' + src: 'test/**/*.js' } }, diff --git a/src/cache.js b/src/cache.js index 9c8fa864bec..a606dd9e638 100644 --- a/src/cache.js +++ b/src/cache.js @@ -92,7 +92,6 @@ p.shift = function () { p.get = function (key, returnEntry) { var entry = this._keymap[key] if (entry === undefined) return - console.log('cache hit!') if (entry === this.tail) { return returnEntry ? entry diff --git a/src/parse/template.js b/src/parse/template.js index 10d8cb86824..14f3db69e2a 100644 --- a/src/parse/template.js +++ b/src/parse/template.js @@ -67,22 +67,14 @@ function stringToFragment (templateString) { node = node.lastChild } - if (node.firstChild === node.lastChild) { - // one element - frag.appendChild(node.firstChild) - templateCache.put(templateString, frag) - return frag - } else { - // multiple nodes, return a fragment - /* jshint boss: true */ - var child - while (child = node.firstChild) { - if (node.nodeType === 1) { - frag.appendChild(child) - } + var child + while (child = node.firstChild) { + if (node.nodeType === 1) { + frag.appendChild(child) } } } + templateCache.put(templateString, frag) return frag } @@ -101,12 +93,9 @@ function nodeToFragment (node) { if (tag === 'TEMPLATE' && node.content) { return node.content } - // script tag - if (tag === 'SCRIPT') { - return stringToFragment(node.textContent) - } - // non-script node. not recommended... - return toFragment(node.outerHTML) + return tag === 'SCRIPT' + ? stringToFragment(node.textContent) + : stringToFragment(node.outerHTML) } /** diff --git a/src/util.js b/src/util.js index 2c55909b91f..5e5854b1b6d 100644 --- a/src/util.js +++ b/src/util.js @@ -1,9 +1,4 @@ var config = require('./config') -var slice = [].slice -var defer = - win.requestAnimationFrame || - win.webkitRequestAnimationFrame || - win.setTimeout /** * Defer a task to the start of the next event loop @@ -11,6 +6,12 @@ var defer = * @param {Function} fn */ +var defer = typeof window === 'undefined' + ? setTimeout + : (window.requestAnimationFrame || + window.webkitRequestAnimationFrame || + setTimeout) + exports.nextTick = function (fn) { return defer(fn, 0) } @@ -22,6 +23,8 @@ exports.nextTick = function (fn) { * @param {Number} [i] - start index */ +var slice = [].slice + exports.toArray = function (list, i) { return slice.call(list, i || 0) } diff --git a/test/unit/data_spec.js b/test/unit/data_spec.js index e5610d97c11..c27b07687c7 100644 --- a/test/unit/data_spec.js +++ b/test/unit/data_spec.js @@ -22,13 +22,13 @@ describe('Scope', function () { }) it('should copy over data properties', function () { - expect(vm.$scope.a).toEqual(vm.$data.a) - expect(vm.$scope.b).toEqual(vm.$data.b) + expect(vm.$scope.a).toBe(vm.$data.a) + expect(vm.$scope.b).toBe(vm.$data.b) }) it('should proxy these properties', function () { - expect(vm.a).toEqual(vm.$scope.a) - expect(vm.b).toEqual(vm.$scope.b) + expect(vm.a).toBe(vm.$scope.a) + expect(vm.b).toBe(vm.$scope.b) }) it('should trigger set events', function () { @@ -37,12 +37,12 @@ describe('Scope', function () { // set on scope vm.$scope.a = 2 - expect(spy.callCount).toEqual(1) + expect(spy.callCount).toBe(1) expect(spy).toHaveBeenCalledWith('a', 2, u) // set on vm vm.b.c = 3 - expect(spy.callCount).toEqual(2) + expect(spy.callCount).toBe(2) expect(spy).toHaveBeenCalledWith('b.c', 3, u) }) @@ -54,12 +54,12 @@ describe('Scope', function () { // add on scope vm.$scope.$add('c', 123) - expect(spy.callCount).toEqual(1) + expect(spy.callCount).toBe(1) expect(spy).toHaveBeenCalledWith('c', 123, u) // delete on scope vm.$scope.$delete('c') - expect(spy.callCount).toEqual(2) + expect(spy.callCount).toBe(2) expect(spy).toHaveBeenCalledWith('c', u, u) // vm $add/$delete are tested in the api suite @@ -82,37 +82,37 @@ describe('Scope', function () { }) it('should retain data reference', function () { - expect(vm.$data).toEqual(data) + expect(vm.$data).toBe(data) }) it('should sync set', function () { // vm -> data vm.a = 2 - expect(data.a).toEqual(2) + expect(data.a).toBe(2) // data -> vm data.b = {d:3} - expect(vm.$scope.b).toEqual(data.b) - expect(vm.b).toEqual(data.b) + expect(vm.$scope.b).toBe(data.b) + expect(vm.b).toBe(data.b) }) it('should sync add', function () { // vm -> data vm.$scope.$add('c', 123) - expect(data.c).toEqual(123) + expect(data.c).toBe(123) // data -> vm data.$add('d', 456) - expect(vm.$scope.d).toEqual(456) - expect(vm.d).toEqual(456) + expect(vm.$scope.d).toBe(456) + expect(vm.d).toBe(456) }) it('should sync delete', function () { // vm -> data vm.$scope.$delete('d') - expect(data.hasOwnProperty('d')).toBeFalsy() + expect(data.hasOwnProperty('d')).toBe(false) // data -> vm data.$delete('c') - expect(vm.$scope.hasOwnProperty('c')).toBeFalsy() - expect(vm.hasOwnProperty('c')).toBeFalsy() + expect(vm.$scope.hasOwnProperty('c')).toBe(false) + expect(vm.hasOwnProperty('c')).toBe(false) }) }) @@ -136,8 +136,8 @@ describe('Scope', function () { }) it('child should inherit parent data on scope', function () { - expect(child.$scope.b).toEqual(parent.b) // object - expect(child.$scope.c).toEqual(parent.c) // primitive value + expect(child.$scope.b).toBe(parent.b) // object + expect(child.$scope.c).toBe(parent.c) // primitive value }) it('child should not ineherit data on instance', function () { @@ -146,14 +146,14 @@ describe('Scope', function () { }) it('child should shadow parent property with same key', function () { - expect(parent.a).toEqual('parent a') - expect(child.$scope.a).toEqual('child a') - expect(child.a).toEqual('child a') + expect(parent.a).toBe('parent a') + expect(child.$scope.a).toBe('child a') + expect(child.a).toBe('child a') }) it('setting scope properties on child should affect parent', function () { child.$scope.c = 'modified by child' - expect(parent.c).toEqual('modified by child') + expect(parent.c).toBe('modified by child') }) it('events on parent should propagate down to child', function () { @@ -162,27 +162,27 @@ describe('Scope', function () { var spy = jasmine.createSpy('inheritance') child._observer.on('set', spy) parent.c = 'c changed' - expect(spy.callCount).toEqual(1) + expect(spy.callCount).toBe(1) expect(spy).toHaveBeenCalledWith('c', 'c changed', u) spy = jasmine.createSpy('inheritance') child._observer.on('add', spy) parent.$scope.$add('e', 123) - expect(spy.callCount).toEqual(1) + expect(spy.callCount).toBe(1) expect(spy).toHaveBeenCalledWith('e', 123, u) spy = jasmine.createSpy('inheritance') child._observer.on('delete', spy) parent.$scope.$delete('e') - expect(spy.callCount).toEqual(1) + expect(spy.callCount).toBe(1) expect(spy).toHaveBeenCalledWith('e', u, u) spy = jasmine.createSpy('inheritance') child._observer.on('mutate', spy) parent.arr.reverse() - expect(spy.mostRecentCall.args[0]).toEqual('arr') - expect(spy.mostRecentCall.args[1]).toEqual(parent.arr) - expect(spy.mostRecentCall.args[2].method).toEqual('reverse') + expect(spy.mostRecentCall.args[0]).toBe('arr') + expect(spy.mostRecentCall.args[1]).toBe(parent.arr) + expect(spy.mostRecentCall.args[2].method).toBe('reverse') }) @@ -192,7 +192,7 @@ describe('Scope', function () { var spy = jasmine.createSpy('inheritance') child._observer.on('set', spy) parent.a = 'a changed' - expect(spy.callCount).toEqual(0) + expect(spy.callCount).toBe(0) }) }) @@ -220,12 +220,12 @@ describe('Scope', function () { child.a = 3 // make sure data sync is working - expect(parent.arr[0].a).toEqual(3) + expect(parent.arr[0].a).toBe(3) - expect(parentSpy.callCount).toEqual(1) + expect(parentSpy.callCount).toBe(1) expect(parentSpy).toHaveBeenCalledWith('arr.0.a', 3, u) - expect(childSpy.callCount).toEqual(2) + expect(childSpy.callCount).toBe(2) expect(childSpy).toHaveBeenCalledWith('a', 3, u) expect(childSpy).toHaveBeenCalledWith('arr.0.a', 3, u) }) diff --git a/test/unit/observer_spec.js b/test/unit/observer_spec.js index f472bc86771..2a162414dfd 100644 --- a/test/unit/observer_spec.js +++ b/test/unit/observer_spec.js @@ -30,12 +30,12 @@ describe('Observer', function () { var t = obj.a expect(spy).toHaveBeenCalledWith('a', u, u) - expect(spy.callCount).toEqual(1) + expect(spy.callCount).toBe(1) t = obj.b.c expect(spy).toHaveBeenCalledWith('b', u, u) expect(spy).toHaveBeenCalledWith('b.c', u, u) - expect(spy.callCount).toEqual(3) + expect(spy.callCount).toBe(3) Observer.emitGet = false }) @@ -52,21 +52,21 @@ describe('Observer', function () { obj.a = 3 expect(spy).toHaveBeenCalledWith('a', 3, u) - expect(spy.callCount).toEqual(1) + expect(spy.callCount).toBe(1) obj.b.c = 4 expect(spy).toHaveBeenCalledWith('b.c', 4, u) - expect(spy.callCount).toEqual(2) + expect(spy.callCount).toBe(2) // swap set var newB = { c: 5 } obj.b = newB expect(spy).toHaveBeenCalledWith('b', newB, u) - expect(spy.callCount).toEqual(3) + expect(spy.callCount).toBe(3) // same value set should not emit events obj.a = 3 - expect(spy.callCount).toEqual(3) + expect(spy.callCount).toBe(3) }) it('array get', function () { @@ -82,7 +82,7 @@ describe('Observer', function () { var t = obj.arr[0].a expect(spy).toHaveBeenCalledWith('arr', u, u) expect(spy).toHaveBeenCalledWith('arr.0.a', u, u) - expect(spy.callCount).toEqual(2) + expect(spy.callCount).toBe(2) Observer.emitGet = false }) @@ -108,15 +108,15 @@ describe('Observer', function () { var ob = Observer.create(arr) ob.on('mutate', spy) arr.push({a:3}) - expect(spy.mostRecentCall.args[0]).toEqual('') - expect(spy.mostRecentCall.args[1]).toEqual(arr) + expect(spy.mostRecentCall.args[0]).toBe('') + expect(spy.mostRecentCall.args[1]).toBe(arr) var mutation = spy.mostRecentCall.args[2] expect(mutation).toBeDefined() - expect(mutation.method).toEqual('push') - expect(mutation.index).toEqual(2) - expect(mutation.removed.length).toEqual(0) - expect(mutation.inserted.length).toEqual(1) - expect(mutation.inserted[0]).toEqual(arr[2]) + expect(mutation.method).toBe('push') + expect(mutation.index).toBe(2) + expect(mutation.removed.length).toBe(0) + expect(mutation.inserted.length).toBe(1) + expect(mutation.inserted[0]).toBe(arr[2]) // test index update after mutation ob.on('set', spy) arr[2].a = 4 @@ -129,15 +129,15 @@ describe('Observer', function () { var ob = Observer.create(arr) ob.on('mutate', spy) arr.pop() - expect(spy.mostRecentCall.args[0]).toEqual('') - expect(spy.mostRecentCall.args[1]).toEqual(arr) + expect(spy.mostRecentCall.args[0]).toBe('') + expect(spy.mostRecentCall.args[1]).toBe(arr) var mutation = spy.mostRecentCall.args[2] expect(mutation).toBeDefined() - expect(mutation.method).toEqual('pop') - expect(mutation.index).toEqual(1) - expect(mutation.inserted.length).toEqual(0) - expect(mutation.removed.length).toEqual(1) - expect(mutation.removed[0]).toEqual(popped) + expect(mutation.method).toBe('pop') + expect(mutation.index).toBe(1) + expect(mutation.inserted.length).toBe(0) + expect(mutation.removed.length).toBe(1) + expect(mutation.removed[0]).toBe(popped) }) it('array shift', function () { @@ -146,15 +146,15 @@ describe('Observer', function () { var ob = Observer.create(arr) ob.on('mutate', spy) arr.shift() - expect(spy.mostRecentCall.args[0]).toEqual('') - expect(spy.mostRecentCall.args[1]).toEqual(arr) + expect(spy.mostRecentCall.args[0]).toBe('') + expect(spy.mostRecentCall.args[1]).toBe(arr) var mutation = spy.mostRecentCall.args[2] expect(mutation).toBeDefined() - expect(mutation.method).toEqual('shift') - expect(mutation.index).toEqual(0) - expect(mutation.inserted.length).toEqual(0) - expect(mutation.removed.length).toEqual(1) - expect(mutation.removed[0]).toEqual(shifted) + expect(mutation.method).toBe('shift') + expect(mutation.index).toBe(0) + expect(mutation.inserted.length).toBe(0) + expect(mutation.removed.length).toBe(1) + expect(mutation.removed[0]).toBe(shifted) // test index update after mutation ob.on('set', spy) arr[0].a = 4 @@ -167,15 +167,15 @@ describe('Observer', function () { var ob = Observer.create(arr) ob.on('mutate', spy) arr.unshift(unshifted) - expect(spy.mostRecentCall.args[0]).toEqual('') - expect(spy.mostRecentCall.args[1]).toEqual(arr) + expect(spy.mostRecentCall.args[0]).toBe('') + expect(spy.mostRecentCall.args[1]).toBe(arr) var mutation = spy.mostRecentCall.args[2] expect(mutation).toBeDefined() - expect(mutation.method).toEqual('unshift') - expect(mutation.index).toEqual(0) - expect(mutation.removed.length).toEqual(0) - expect(mutation.inserted.length).toEqual(1) - expect(mutation.inserted[0]).toEqual(unshifted) + expect(mutation.method).toBe('unshift') + expect(mutation.index).toBe(0) + expect(mutation.removed.length).toBe(0) + expect(mutation.inserted.length).toBe(1) + expect(mutation.inserted[0]).toBe(unshifted) // test index update after mutation ob.on('set', spy) arr[1].a = 4 @@ -189,16 +189,16 @@ describe('Observer', function () { var ob = Observer.create(arr) ob.on('mutate', spy) arr.splice(1, 1, inserted) - expect(spy.mostRecentCall.args[0]).toEqual('') - expect(spy.mostRecentCall.args[1]).toEqual(arr) + expect(spy.mostRecentCall.args[0]).toBe('') + expect(spy.mostRecentCall.args[1]).toBe(arr) var mutation = spy.mostRecentCall.args[2] expect(mutation).toBeDefined() - expect(mutation.method).toEqual('splice') - expect(mutation.index).toEqual(1) - expect(mutation.removed.length).toEqual(1) - expect(mutation.inserted.length).toEqual(1) - expect(mutation.removed[0]).toEqual(removed) - expect(mutation.inserted[0]).toEqual(inserted) + expect(mutation.method).toBe('splice') + expect(mutation.index).toBe(1) + expect(mutation.removed.length).toBe(1) + expect(mutation.inserted.length).toBe(1) + expect(mutation.removed[0]).toBe(removed) + expect(mutation.inserted[0]).toBe(inserted) // test index update after mutation ob.on('set', spy) arr[1].a = 4 @@ -212,14 +212,14 @@ describe('Observer', function () { arr.sort(function (a, b) { return a.a < b.a ? 1 : -1 }) - expect(spy.mostRecentCall.args[0]).toEqual('') - expect(spy.mostRecentCall.args[1]).toEqual(arr) + expect(spy.mostRecentCall.args[0]).toBe('') + expect(spy.mostRecentCall.args[1]).toBe(arr) var mutation = spy.mostRecentCall.args[2] expect(mutation).toBeDefined() - expect(mutation.method).toEqual('sort') + expect(mutation.method).toBe('sort') expect(mutation.index).toBeUndefined() - expect(mutation.removed.length).toEqual(0) - expect(mutation.inserted.length).toEqual(0) + expect(mutation.removed.length).toBe(0) + expect(mutation.inserted.length).toBe(0) // test index update after mutation ob.on('set', spy) arr[1].a = 4 @@ -231,14 +231,14 @@ describe('Observer', function () { var ob = Observer.create(arr) ob.on('mutate', spy) arr.reverse() - expect(spy.mostRecentCall.args[0]).toEqual('') - expect(spy.mostRecentCall.args[1]).toEqual(arr) + expect(spy.mostRecentCall.args[0]).toBe('') + expect(spy.mostRecentCall.args[1]).toBe(arr) var mutation = spy.mostRecentCall.args[2] expect(mutation).toBeDefined() - expect(mutation.method).toEqual('reverse') + expect(mutation.method).toBe('reverse') expect(mutation.index).toBeUndefined() - expect(mutation.removed.length).toEqual(0) - expect(mutation.inserted.length).toEqual(0) + expect(mutation.removed.length).toBe(0) + expect(mutation.inserted.length).toBe(0) // test index update after mutation ob.on('set', spy) arr[1].a = 4 @@ -278,16 +278,16 @@ describe('Observer', function () { var removed = arr[1] arr.$set(1, inserted) - expect(spy.mostRecentCall.args[0]).toEqual('') - expect(spy.mostRecentCall.args[1]).toEqual(arr) + expect(spy.mostRecentCall.args[0]).toBe('') + expect(spy.mostRecentCall.args[1]).toBe(arr) var mutation = spy.mostRecentCall.args[2] expect(mutation).toBeDefined() - expect(mutation.method).toEqual('splice') - expect(mutation.index).toEqual(1) - expect(mutation.removed.length).toEqual(1) - expect(mutation.inserted.length).toEqual(1) - expect(mutation.removed[0]).toEqual(removed) - expect(mutation.inserted[0]).toEqual(inserted) + expect(mutation.method).toBe('splice') + expect(mutation.index).toBe(1) + expect(mutation.removed.length).toBe(1) + expect(mutation.inserted.length).toBe(1) + expect(mutation.removed[0]).toBe(removed) + expect(mutation.inserted[0]).toBe(inserted) ob.on('set', spy) arr[1].a = 4 @@ -300,15 +300,15 @@ describe('Observer', function () { ob.on('mutate', spy) var removed = arr.$remove(0) - expect(spy.mostRecentCall.args[0]).toEqual('') - expect(spy.mostRecentCall.args[1]).toEqual(arr) + expect(spy.mostRecentCall.args[0]).toBe('') + expect(spy.mostRecentCall.args[1]).toBe(arr) var mutation = spy.mostRecentCall.args[2] expect(mutation).toBeDefined() - expect(mutation.method).toEqual('splice') - expect(mutation.index).toEqual(0) - expect(mutation.removed.length).toEqual(1) - expect(mutation.inserted.length).toEqual(0) - expect(mutation.removed[0]).toEqual(removed) + expect(mutation.method).toBe('splice') + expect(mutation.index).toBe(0) + expect(mutation.removed.length).toBe(1) + expect(mutation.inserted.length).toBe(0) + expect(mutation.removed[0]).toBe(removed) ob.on('set', spy) arr[0].a = 3 diff --git a/test/unit/template-parser.js b/test/unit/template-parser.js new file mode 100644 index 00000000000..a4548d7c50d --- /dev/null +++ b/test/unit/template-parser.js @@ -0,0 +1,103 @@ +var templateParser = require('../../src/parse/template') +var parse = templateParser.parse +var testString = '
hello

world

' + +describe('Template Parser', function () { + + it('should return same if argument is already a fragment', function () { + var frag = document.createDocumentFragment() + var res = parse(frag) + expect(res).toBe(frag) + }) + + // only test template node if it works in the browser being tested. + var templateNode = document.createElement('template') + if (templateNode.content) { + it('should return content if argument is a valid template node', function () { + var res = parse(templateNode) + expect(res).toBe(templateNode.content) + }) + } + + it('should parse if argument is a template string', function () { + var res = parse(testString) + expect(res instanceof DocumentFragment).toBeTruthy() + expect(res.childNodes.length).toBe(2) + expect(res.querySelector('.test').textContent).toBe('world') + }) + + it('should parse textContent if argument is a script node', function () { + var node = document.createElement('script') + node.textContent = testString + var res = parse(node) + expect(res instanceof DocumentFragment).toBeTruthy() + expect(res.childNodes.length).toBe(2) + expect(res.querySelector('.test').textContent).toBe('world') + }) + + it('should parse outerHTML if argument is a normal node', function () { + var node = document.createElement('div') + node.innerHTML = testString + var res = parse(node) + expect(res instanceof DocumentFragment).toBeTruthy() + expect(res.childNodes.length).toBe(1) + expect(res.querySelector('div .test').textContent).toBe('world') + }) + + it('should retrieve and parse if argument is an id selector', function () { + var node = document.createElement('script') + node.setAttribute('id', 'template-test') + node.setAttribute('type', 'x/template') + node.textContent = testString + document.head.appendChild(node) + var res = parse('#template-test') + expect(res instanceof DocumentFragment).toBeTruthy() + expect(res.childNodes.length).toBe(2) + expect(res.querySelector('.test').textContent).toBe('world') + document.head.removeChild(node) + }) + + it('should work for table elements', function () { + var res = parse('hello') + expect(res instanceof DocumentFragment).toBeTruthy() + expect(res.childNodes.length).toBe(1) + expect(res.firstChild.tagName).toBe('TD') + expect(res.firstChild.textContent).toBe('hello') + }) + + it('should work for option elements', function () { + var res = parse('') + expect(res instanceof DocumentFragment).toBeTruthy() + expect(res.childNodes.length).toBe(1) + expect(res.firstChild.tagName).toBe('OPTION') + expect(res.firstChild.textContent).toBe('hello') + }) + + it('should work for svg elements', function () { + var res = parse('') + expect(res instanceof DocumentFragment).toBeTruthy() + expect(res.childNodes.length).toBe(1) + // SVG tagNames should be lowercase because they are XML nodes not HTML + expect(res.firstChild.tagName).toBe('circle') + expect(res.firstChild.namespaceURI).toBe('http://www.w3.org/2000/svg') + }) + + it('should cache template strings', function () { + var res1 = parse(testString) + var res2 = parse(testString) + expect(res1).toBe(res2) + }) + + it('should cache id selectors', function () { + var node = document.createElement('script') + node.setAttribute('id', 'template-test') + node.setAttribute('type', 'x/template') + node.textContent = '
never seen before content
' + document.head.appendChild(node) + var res1 = parse('#template-test') + var res2 = parse('#template-test') + expect(res1).toBe(res2) + document.head.removeChild(node) + }) + +}) \ No newline at end of file From 73b8c85286033c067a6a99169f21f53d12f5004a Mon Sep 17 00:00:00 2001 From: Evan You Date: Sat, 26 Jul 2014 11:46:44 -0400 Subject: [PATCH 0056/1534] test for cache --- src/cache.js | 16 +++--------- test/unit/cache_spec.js | 55 +++++++++++++++++++++++++++++++++++++++-- 2 files changed, 56 insertions(+), 15 deletions(-) diff --git a/src/cache.js b/src/cache.js index a606dd9e638..102737d99b6 100644 --- a/src/cache.js +++ b/src/cache.js @@ -1,20 +1,10 @@ /** * A doubly linked list-based Least Recently Used (LRU) cache. * Will keep most recently used items while discarding least - * recently used items when its limit is reached. + * recently used items when its limit is reached. This is a + * bare-bone version of Rasmus Andersson's js-lru: * - * Licensed under MIT. - * Copyright (c) 2010 Rasmus Andersson - * - * Illustration of the design: - * - * entry entry entry entry - * ______ ______ ______ ______ - * | head |.newer => | |.newer => | |.newer => | tail | - * | A | | B | | C | | D | - * |______| <= older.|______| <= older.|______| <= older.|______| - * - * removed <-- <-- <-- <-- <-- <-- <-- <-- <-- <-- <-- added + * https://github.com/rsms/js-lru * * @param {Number} limit * @constructor diff --git a/test/unit/cache_spec.js b/test/unit/cache_spec.js index ab86d37e4df..0fa29df6f17 100644 --- a/test/unit/cache_spec.js +++ b/test/unit/cache_spec.js @@ -1,3 +1,54 @@ +var Cache = require('../../src/cache') + /** - * Test cache hit/miss/expiration - */ \ No newline at end of file + * Debug function to assert cache state + * + * @param {Cache} cache + */ + +function toString (cache) { + var s = '' + var entry = cache.head + while (entry) { + s += String(entry.key) + ':' + entry.value + entry = entry.newer + if (entry) { + s += ' < ' + } + } + return s +} + +describe('Cache', function () { + + var c = new Cache(4) + + it('put', function () { + c.put('adam', 29) + c.put('john', 26) + c.put('angela', 24) + c.put('bob', 48) + expect(c.size).toBe(4) + expect(toString(c)).toBe('adam:29 < john:26 < angela:24 < bob:48') + }) + + it('get', function () { + expect(c.get('adam')).toBe(29) + expect(c.get('john')).toBe(26) + expect(c.get('angela')).toBe(24) + expect(c.get('bob')).toBe(48) + expect(toString(c)).toBe('adam:29 < john:26 < angela:24 < bob:48') + + expect(c.get('angela')).toBe(24) + // angela should now be the tail + expect(toString(c)).toBe('adam:29 < john:26 < bob:48 < angela:24') + }) + + it('expire', function () { + c.put('ygwie', 81) + expect(c.size).toBe(4) + expect(toString(c)).toBe('john:26 < bob:48 < angela:24 < ygwie:81') + expect(c.get('adam')).toBeUndefined() + }) + +}) \ No newline at end of file From 0813e461b71dca26eae9bd1b0362a88602ecc1e0 Mon Sep 17 00:00:00 2001 From: Evan You Date: Sat, 26 Jul 2014 15:56:32 -0400 Subject: [PATCH 0057/1534] refactor util --- src/api/global.js | 25 ++-- src/config.js | 16 +-- src/instance/init.js | 68 ++++------- src/util.js | 264 ------------------------------------------- src/util/dom.js | 67 +++++++++++ src/util/env.js | 87 ++++++++++++++ src/util/index.js | 136 ++++++++++++++++++++++ src/util/lang.js | 121 ++++++++++++++++++++ 8 files changed, 448 insertions(+), 336 deletions(-) delete mode 100644 src/util.js create mode 100644 src/util/dom.js create mode 100644 src/util/env.js create mode 100644 src/util/index.js create mode 100644 src/util/lang.js diff --git a/src/api/global.js b/src/api/global.js index 4309632e684..1277db39171 100644 --- a/src/api/global.js +++ b/src/api/global.js @@ -1,5 +1,11 @@ var _ = require('../util') -var config = require('../config') +var assetTypes = [ + 'directive', + 'filter', + 'partial', + 'effect', + 'component' +] /** * Vue and every constructor that extends Vue has an associated @@ -19,9 +25,9 @@ exports.options = { * Expose useful internals */ -exports.util = _ -exports.config = config -exports.nextTick = _.nextTick +exports.util = _ +exports.nextTick = _.nextTick +exports.config = require('../config') exports.transition = require('../transition/transition') /** @@ -33,12 +39,15 @@ exports.transition = require('../transition/transition') exports.extend = function (extendOptions) { var Super = this var Sub = function (instanceOptions) { - var mergedOptions = _.mergeOptions(Sub.options, instanceOptions) - Super.call(this, mergedOptions) + Super.call(this, instanceOptions) } Sub.prototype = Object.create(Super.prototype) _.define(Sub.prototype, 'constructor', Sub) - Sub.options = _.mergeOptions(Super.options, extendOptions) + Sub.options = _.mergeOptions( + Super.options, + extendOptions, + true // indicates an inheritance merge + ) Sub.super = Super // allow further extension Sub.extend = Super.extend @@ -80,7 +89,7 @@ exports.use = function (plugin) { createAssetRegisters(exports) function createAssetRegisters (Ctor) { - config._assetTypes.forEach(function (type) { + assetTypes.forEach(function (type) { /** * Asset registration method. diff --git a/src/config.js b/src/config.js index ab293bebd98..4474e94ab51 100644 --- a/src/config.js +++ b/src/config.js @@ -27,20 +27,6 @@ module.exports = { * @type {Boolean} */ - interpolate: true, - - /** - * Asset types - * @type {Array.} - * @readonly - */ - - _assetTypes: [ - 'directive', - 'filter', - 'partial', - 'effect', - 'component' - ] + interpolate: true } \ No newline at end of file diff --git a/src/instance/init.js b/src/instance/init.js index 37c696988d7..fda9ca1e397 100644 --- a/src/instance/init.js +++ b/src/instance/init.js @@ -1,3 +1,5 @@ +var _ = require('../util') + /** * The main init sequence. This is called for every instance, * including ones that are created from extended constructors. @@ -9,67 +11,35 @@ */ exports._init = function (options) { - - /** - * Expose instance options. - * @type {Object} - * - * @public - */ - - this.$options = options || {} - - /** - * Indicates whether this is a block instance. (One with more - * than one top-level nodes) - * - * @type {Boolean} - * @private - */ - - this._isBlock = false - - /** - * Indicates whether the instance has been mounted to a DOM node. - * - * @type {Boolean} - * @private - */ - - this._isMounted = false - - /** - * Indicates whether the instance has been destroyed. - * - * @type {Boolean} - * @private - */ - - this._isDestroyed = false - - /** - * If the instance has a template option, the raw content it holds - * before compilation will be preserved so they can be queried against - * during content insertion. - * - * @type {DocumentFragment} - * @private - */ - - this._rawContent = null + // merge options. + this.$options = _.mergeOptions( + this.constructor.options, + options + ) // create scope. + // @creates this.$parent + // @creates this.$scope this._initScope() // setup initial data. + // @creates this._data this._initData(this.$options.data || {}, true) // setup property proxying this._initProxy() // setup binding tree. + // @creates this._rootBinding this._initBindings() - + + // compilation and lifecycle related state properties + this.$el = null + this._rawContent = null + this._isBlock = false + this._isMounted = false + this._isDestroyed = false + // if `el` option is passed, start compilation. if (this.$options.el) { this.$mount(this.$options.el) diff --git a/src/util.js b/src/util.js deleted file mode 100644 index 5e5854b1b6d..00000000000 --- a/src/util.js +++ /dev/null @@ -1,264 +0,0 @@ -var config = require('./config') - -/** - * Defer a task to the start of the next event loop - * - * @param {Function} fn - */ - -var defer = typeof window === 'undefined' - ? setTimeout - : (window.requestAnimationFrame || - window.webkitRequestAnimationFrame || - setTimeout) - -exports.nextTick = function (fn) { - return defer(fn, 0) -} - -/** - * Convert an Array-like object to a real Array. - * - * @param {Array-like} list - * @param {Number} [i] - start index - */ - -var slice = [].slice - -exports.toArray = function (list, i) { - return slice.call(list, i || 0) -} - -/** - * Mix properties into target object. - * - * @param {Object} to - * @param {Object} from - */ - -exports.mixin = function (to, from) { - for (var key in from) { - if (to[key] !== from[key]) { - to[key] = from[key] - } - } -} - -/** - * Mixin including non-enumerables, and copy property descriptors. - * - * @param {Object} to - * @param {Object} from - */ - -exports.deepMixin = function (to, from) { - Object.getOwnPropertyNames(from).forEach(function (key) { - var descriptor = Object.getOwnPropertyDescriptor(from, key) - Object.defineProperty(to, key, descriptor) - }) -} - -/** - * Proxy a property on one object to another. - * - * @param {Object} to - * @param {Object} from - * @param {String} key - */ - -exports.proxy = function (to, from, key) { - if (to.hasOwnProperty(key)) return - Object.defineProperty(to, key, { - enumerable: true, - configurable: true, - get: function () { - return from[key] - }, - set: function (val) { - from[key] = val - } - }) -} - -/** - * Object type check. Only returns true - * for plain JavaScript objects. - * - * @param {*} obj - * @return {Boolean} - */ - -exports.isObject = function (obj) { - return Object.prototype.toString.call(obj) === '[object Object]' -} - -/** - * Array type check. - * - * @param {*} obj - * @return {Boolean} - */ - -exports.isArray = function (obj) { - return Array.isArray(obj) -} - -/** - * Define a non-enumerable property - * - * @param {Object} obj - * @param {String} key - * @param {*} val - * @param {Boolean} [enumerable] - */ - -exports.define = function (obj, key, val, enumerable) { - Object.defineProperty(obj, key, { - value : val, - enumerable : !!enumerable, - writable : true, - configurable : true - }) -} - -/** - * Augment an target Object or Array by either - * intercepting the prototype chain using __proto__, - * or copy over property descriptors - * - * @param {Object|Array} target - * @param {Object} proto - */ - -if ('__proto__' in {}) { - exports.augment = function (target, proto) { - target.__proto__ = proto - } -} else { - exports.augment = exports.deepMixin -} - -/** - * Merge two option objects. - * - * @param {Object} parent - * @param {Object} child - * @param {Boolean} noRecurse - */ - -exports.mergeOptions = function (parent, child, noRecurse) { - // TODO - // - merge lifecycle hooks - // - merge asset registries - // - else override - // - use prototypal inheritance where appropriate -} - -/** - * Insert el before target - * - * @param {Element} el - * @param {Element} target - */ - -exports.before = function (el, target) { - target.parentNode.insertBefore(el, target) -} - -/** - * Insert el after target - * - * @param {Element} el - * @param {Element} target - */ - -exports.after = function (el, target) { - if (target.nextSibling) { - exports.before(el, target.nextSibling) - } else { - target.parentNode.appendChild(el) - } -} - -/** - * Remove el from DOM - * - * @param {Element} el - */ - -exports.remove = function (el) { - el.parentNode.removeChild(el) -} - -/** - * Prepend el to target - * - * @param {Element} el - * @param {Element} target - */ - -exports.prepend = function (el, target) { - if (target.firstChild) { - exports.before(el, target.firstChild) - } else { - target.appendChild(el) - } -} - -/** - * Copy attributes from one element to another. - * - * @param {Element} from - * @param {Element} to - */ - -exports.copyAttributes = function (from, to) { - if (from.hasAttributes()) { - var attrs = from.attributes - for (var i = 0, l = attrs.length; i < l; i++) { - var attr = attrs[i] - to.setAttribute(attr.name, attr.value) - } - } -} - -/** - * Enable debug utilities. The enableDebug() function and all - * _.log() & _.warn() calls will be dropped in the minified - * production build. - */ - -enableDebug() - -function enableDebug () { - - var hasConsole = typeof console !== 'undefined' - - /** - * Log a message. - * - * @param {String} msg - */ - - exports.log = function (msg) { - if (hasConsole && config.debug) { - console.log(msg) - } - } - - /** - * We've got a problem here. - * - * @param {String} msg - */ - - exports.warn = function (msg) { - if (hasConsole && !config.silent) { - console.warn(msg) - if (config.debug && console.trace) { - console.trace(msg) - } - } - } - -} \ No newline at end of file diff --git a/src/util/dom.js b/src/util/dom.js new file mode 100644 index 00000000000..ec38d338fcf --- /dev/null +++ b/src/util/dom.js @@ -0,0 +1,67 @@ +/** + * Insert el before target + * + * @param {Element} el + * @param {Element} target + */ + +exports.before = function (el, target) { + target.parentNode.insertBefore(el, target) +} + +/** + * Insert el after target + * + * @param {Element} el + * @param {Element} target + */ + +exports.after = function (el, target) { + if (target.nextSibling) { + exports.before(el, target.nextSibling) + } else { + target.parentNode.appendChild(el) + } +} + +/** + * Remove el from DOM + * + * @param {Element} el + */ + +exports.remove = function (el) { + el.parentNode.removeChild(el) +} + +/** + * Prepend el to target + * + * @param {Element} el + * @param {Element} target + */ + +exports.prepend = function (el, target) { + if (target.firstChild) { + exports.before(el, target.firstChild) + } else { + target.appendChild(el) + } +} + +/** + * Copy attributes from one element to another. + * + * @param {Element} from + * @param {Element} to + */ + +exports.copyAttributes = function (from, to) { + if (from.hasAttributes()) { + var attrs = from.attributes + for (var i = 0, l = attrs.length; i < l; i++) { + var attr = attrs[i] + to.setAttribute(attr.name, attr.value) + } + } +} \ No newline at end of file diff --git a/src/util/env.js b/src/util/env.js new file mode 100644 index 00000000000..a03f2802507 --- /dev/null +++ b/src/util/env.js @@ -0,0 +1,87 @@ +/** + * Are we in a browser or in Node? + * + * @type {Boolean} + */ + +var inBrowser = exports.inBrowser = + typeof window !== 'undefined' && + Object.prototype.toString.call(window) === '[object global]' + +/** + * Defer a task to the start of the next event loop + * + * @param {Function} fn + */ + +var defer = inBrowser + ? (window.requestAnimationFrame || + window.webkitRequestAnimationFrame || + setTimeout) + : setTimeout + +exports.nextTick = function (fn) { + return defer(fn, 0) +} + +/** + * Detect if the environment allows creating + * a function from strings. + * + * @type {Boolean} + */ + +exports.hasEval = (function () { + // chrome apps enforces CSP + if (typeof chrome !== 'undefined' && + chrome.app && + chrome.app.runtime) { + return false + } + try { + var f = new Function('', 'return true;') + return f() + } catch (e) { + return false + } +})() + +/** + * Detect if we are in IE9... + * + * @type {Boolean} + */ + +exports.isIE9 = + inBrowser && + navigator.userAgent.indexOf('MSIE 9.0') > 0 + +/** + * Detect transition and animation end events. + */ + +var testElement = inBrowser + ? document.createElement('div') + : null + +exports.transitionEndEvent = (function () { + if (!inBrowser) { + return null + } + var map = { + 'webkitTransition' : 'webkitTransitionEnd', + 'transition' : 'transitionend', + 'mozTransition' : 'transitionend' + } + for (var prop in map) { + if (testElement.style[prop] !== undefined) { + return map[prop] + } + } +})() + +exports.animationEndEvent = inBrowser + ? testElement.style.animation !== undefined + ? 'animationend' + : 'webkitAnimationEnd' + : null \ No newline at end of file diff --git a/src/util/index.js b/src/util/index.js new file mode 100644 index 00000000000..315442442d4 --- /dev/null +++ b/src/util/index.js @@ -0,0 +1,136 @@ +var _ = exports +var env = require('./env') +var lang = require('./lang') +var dom = require('./dom') + +var mixin = lang.mixin + +mixin(_, env) +mixin(_, lang) +mixin(_, dom) + +/** + * Option overwriting strategies are functions that handle + * how to merge a parent option value and a child option + * value into the final value. + */ + +var strats = {} + +/** + * Hooks and param attributes are merged as arrays. + */ + +strats.created = +strats.ready = +strats.attached = +strats.detached = +strats.beforeDestroy = +strats.afterDestroy = +strats.paramAttributes = function (parentVal, childVal) { + return (parentVal || []).concat(childVal || []) +} + +/** + * Assets, methods and computed properties are hash objects, + * and are merged with prototypal inheritance. + */ + +strats.directives = +strats.filters = +strats.partials = +strats.effects = +strats.components = +strats.methods = +strats.computed = function (parentVal, childVal) { + var ret = Object.create(parentVal || null) + for (var key in childVal) { + ret[key] = childVal[key] + } + return ret +} + +/** + * Default strategy - overwrite if child value is not undefined. + */ + +var defaultStrat = function (parentVal, childVal) { + return childVal === undefined + ? parentVal + : childVal +} + +/** + * Merge two option objects into a new one. + * Core utility used in both instantiation and inheritance. + * + * @param {Object} parent + * @param {Object} child + * @param {Boolean} inheriting + */ + +exports.mergeOptions = function (parent, child, inheriting) { + var options = {} + var key + for (key in parent) { + merge(key) + } + for (key in child) { + if (!(key in parent)) { + merge(key) + } + } + function merge (key) { + if (inheriting && (key === 'el' || key === 'data')) { + _.warn( + 'The "' + key + '" option can only be used as an instantiation ' + + 'option and will be ignored in Vue.extend().' + ) + return + } + var strat = strats[key] || defaultStrat + options[key] = strat(parent[key], child[key]) + } + return options +} + +/** + * Enable debug utilities. The enableDebug() function and all + * _.log() & _.warn() calls will be dropped in the minified + * production build. + */ + +enableDebug() + +function enableDebug () { + + var hasConsole = typeof console !== 'undefined' + + /** + * Log a message. + * + * @param {String} msg + */ + + exports.log = function (msg) { + if (hasConsole && config.debug) { + console.log(msg) + } + } + + /** + * We've got a problem here. + * + * @param {String} msg + */ + + exports.warn = function (msg) { + if (hasConsole && !config.silent) { + console.warn(msg) + if (config.debug && console.trace) { + console.trace(msg) + } + } + } + +} \ No newline at end of file diff --git a/src/util/lang.js b/src/util/lang.js new file mode 100644 index 00000000000..c295f49ce5d --- /dev/null +++ b/src/util/lang.js @@ -0,0 +1,121 @@ +/** + * Convert an Array-like object to a real Array. + * + * @param {Array-like} list + * @param {Number} [i] - start index + */ + +var slice = [].slice + +exports.toArray = function (list, i) { + return slice.call(list, i || 0) +} + +/** + * Mix properties into target object. + * + * @param {Object} to + * @param {Object} from + */ + +exports.mixin = function (to, from) { + for (var key in from) { + if (to[key] !== from[key]) { + to[key] = from[key] + } + } +} + +/** + * Mixin including non-enumerables, and copy property descriptors. + * + * @param {Object} to + * @param {Object} from + */ + +exports.deepMixin = function (to, from) { + Object.getOwnPropertyNames(from).forEach(function (key) { + var descriptor = Object.getOwnPropertyDescriptor(from, key) + Object.defineProperty(to, key, descriptor) + }) +} + +/** + * Proxy a property on one object to another. + * + * @param {Object} to + * @param {Object} from + * @param {String} key + */ + +exports.proxy = function (to, from, key) { + if (to.hasOwnProperty(key)) return + Object.defineProperty(to, key, { + enumerable: true, + configurable: true, + get: function () { + return from[key] + }, + set: function (val) { + from[key] = val + } + }) +} + +/** + * Object type check. Only returns true + * for plain JavaScript objects. + * + * @param {*} obj + * @return {Boolean} + */ + +exports.isObject = function (obj) { + return Object.prototype.toString.call(obj) === '[object Object]' +} + +/** + * Array type check. + * + * @param {*} obj + * @return {Boolean} + */ + +exports.isArray = function (obj) { + return Array.isArray(obj) +} + +/** + * Define a non-enumerable property + * + * @param {Object} obj + * @param {String} key + * @param {*} val + * @param {Boolean} [enumerable] + */ + +exports.define = function (obj, key, val, enumerable) { + Object.defineProperty(obj, key, { + value : val, + enumerable : !!enumerable, + writable : true, + configurable : true + }) +} + +/** + * Augment an target Object or Array by either + * intercepting the prototype chain using __proto__, + * or copy over property descriptors + * + * @param {Object|Array} target + * @param {Object} proto + */ + +if ('__proto__' in {}) { + exports.augment = function (target, proto) { + target.__proto__ = proto + } +} else { + exports.augment = exports.deepMixin +} \ No newline at end of file From 52ff531b6660cb22ac049b7bec53f2908884d4ce Mon Sep 17 00:00:00 2001 From: Evan You Date: Sat, 26 Jul 2014 17:39:50 -0400 Subject: [PATCH 0058/1534] tests for path parser --- src/parse/path.js | 87 +++++++++--- src/util/env.js | 4 + test/unit/path_spec.js | 125 +++++++++++++++++- test/unit/{template-parser.js => template.js} | 0 4 files changed, 197 insertions(+), 19 deletions(-) rename test/unit/{template-parser.js => template.js} (100%) diff --git a/src/parse/path.js b/src/parse/path.js index 1ae5240bbac..fb5c05eeceb 100644 --- a/src/parse/path.js +++ b/src/parse/path.js @@ -1,10 +1,9 @@ +var _ = require('../util') var Cache = require('../cache') - -/** - * Path cache - */ - var pathCache = new Cache(1000) +var identStart = '[\$_a-zA-Z]' +var identPart = '[\$_a-zA-Z0-9]' +var IDENT_RE = new RegExp('^' + identStart + '+' + identPart + '*' + '$') /** * Path-parsing algorithm scooped from Polymer/observe-js @@ -62,13 +61,13 @@ var pathStateMachine = { 'inSingleQuote': { "'": ['afterElement'], - 'eof': ['error'], + 'eof': 'error', 'else': ['inSingleQuote', 'append'] }, 'inDoubleQuote': { '"': ['afterElement'], - 'eof': ['error'], + 'eof': 'error', 'else': ['inDoubleQuote', 'append'] }, @@ -88,8 +87,9 @@ function noop () {} */ function getPathCharType (char) { - if (char === undefined) + if (char === undefined) { return 'eof' + } var code = char.charCodeAt(0) @@ -118,12 +118,15 @@ function getPathCharType (char) { } // a-z, A-Z - if ((0x61 <= code && code <= 0x7A) || (0x41 <= code && code <= 0x5A)) + if ((0x61 <= code && code <= 0x7A) || + (0x41 <= code && code <= 0x5A)) { return 'ident' + } // 1-9 - if (0x31 <= code && code <= 0x39) + if (0x31 <= code && code <= 0x39) { return 'number' + } return 'else' } @@ -150,10 +153,11 @@ function parsePath (path) { key = undefined }, append: function() { - if (key === undefined) + if (key === undefined) { key = newChar - else + } else { key += newChar + } } } @@ -200,6 +204,47 @@ function parsePath (path) { return // parse error } +function isIndex(s) { + return +s === s >>> 0; +} + +function isIdent(s) { + return IDENT_RE.test(s); +} + +function formatAccessor(key) { + if (isIdent(key)) { + return '.' + key + } else if (isIndex(key)) { + return '[' + key + ']'; + } else { + return '["' + key.replace(/"/g, '\\"') + '"]'; + } +} + +/** + * Compiles a getter function with a set path, which + * is much more efficient than the dynamic path getter. + * + * @param {Array} path + * @return {Function} + */ + +exports.compileGetter = function (path) { + var body = 'if (o != null' + var pathString = 'o' + var key + for (var i = 0, l = path.length - 1; i < l; i++) { + key = path[i] + pathString += formatAccessor(key) + body += ' && ' + pathString + ' != null' + } + key = path[i] + pathString += formatAccessor(key) + body += ') return ' + pathString + return new Function('o', body) +} + /** * External parse that check for a cache hit first * @@ -211,7 +256,12 @@ exports.parse = function (path) { var hit = pathCache.get(path) if (!hit) { hit = parsePath(path) - pathCache.put(path, hit) + if (hit) { + if (_.hasEval) { + hit.get = exports.compileGetter(hit) + } + pathCache.put(path, hit) + } } return hit } @@ -230,6 +280,11 @@ exports.get = function (obj, path) { if (!path) { return } + // path has compiled getter + if (path.get) { + return path.get(obj) + } + // else do the traversal for (var i = 0, l = path.length; i < l; i++) { if (obj == null) return obj = obj[path[i]] @@ -253,14 +308,14 @@ exports.set = function (obj, path, val) { return } for (var i = 0, l = path.length - 1; i < l; i++) { - if (typeof obj !== 'object') { + if (!obj || typeof obj !== 'object') { return false } obj = obj[path[i]] } - if (typeof obj !== 'object') { + if (!obj || typeof obj !== 'object') { return false } - obj[path] = val + obj[path[i]] = val return true } \ No newline at end of file diff --git a/src/util/env.js b/src/util/env.js index a03f2802507..52bcedb7ef6 100644 --- a/src/util/env.js +++ b/src/util/env.js @@ -38,6 +38,10 @@ exports.hasEval = (function () { chrome.app.runtime) { return false } + // so does Firefox OS apps... + if (inBrowser && navigator.getDeviceStorage) { + return false + } try { var f = new Function('', 'return true;') return f() diff --git a/test/unit/path_spec.js b/test/unit/path_spec.js index 99eb1be94ff..05418c9a2f7 100644 --- a/test/unit/path_spec.js +++ b/test/unit/path_spec.js @@ -1,3 +1,122 @@ -/** - * Test path parsing - */ \ No newline at end of file +var Path = require('../../src/parse/path') + +function assertPath (str, expected) { + var path = Path.parse(str) + expect(pathMatch(path, expected)).toBe(true) +} + +function assertInvalidPath (str) { + var path = Path.parse(str) + expect(path).toBeUndefined() +} + +function pathMatch (a, b) { + if (a.length !== b.length) { + return false + } + for (var i = 0; i < a.length; i++) { + if (a[i] !== b[i]) { + return false + } + } + return true +} + +describe('Path', function () { + + it('parse', function () { + assertPath('', []) + assertPath(' ', []) + assertPath('a', ['a']) + assertPath('a.b', ['a', 'b']) + assertPath('a. b', ['a', 'b']) + assertPath('a .b', ['a', 'b']) + assertPath('a . b', ['a', 'b']) + assertPath(' a . b ', ['a', 'b']) + assertPath('a[0]', ['a', '0']) + assertPath('a [0]', ['a', '0']) + assertPath('a[0][1]', ['a', '0', '1']) + assertPath('a [ 0 ] [ 1 ] ', ['a', '0', '1']) + assertPath('[1234567890] ', ['1234567890']) + assertPath(' [1234567890] ', ['1234567890']) + assertPath('opt0', ['opt0']) + assertPath('$foo.$bar._baz', ['$foo', '$bar', '_baz']) + assertPath('foo["baz"]', ['foo', 'baz']) + assertPath('foo["b\\"az"]', ['foo', 'b"az']) + assertPath("foo['b\\'az']", ['foo', "b'az"]) + }) + + it('handle invalid paths', function () { + assertInvalidPath('.') + assertInvalidPath(' . ') + assertInvalidPath('..') + assertInvalidPath('a[4') + assertInvalidPath('a.b.') + assertInvalidPath('a,b') + assertInvalidPath('a["foo]') + assertInvalidPath('[0x04]') + assertInvalidPath('[0foo]') + assertInvalidPath('[foo-bar]') + assertInvalidPath('foo-bar') + assertInvalidPath('42') + assertInvalidPath('a[04]') + assertInvalidPath(' a [ 04 ]') + assertInvalidPath(' 42 ') + assertInvalidPath('foo["bar]') + assertInvalidPath("foo['bar]") + }) + + it('caching', function () { + var path1 = Path.parse('a.b.c') + var path2 = Path.parse('a.b.c') + expect(path1).toBe(path2) + }) + + it('compiled getter', function () { + var path = ['a', 'b-$$-c', '0'] + var obj = { + a: { + 'b-$$-c': [12345] + } + } + var getter = Path.compileGetter(path) + expect(getter(obj)).toBe(12345) + + var path = Path.parse('a["b-$$-c"][0]') + expect(path.get(obj)).toBe(12345) + }) + + it('get', function () { + var path = 'a[\'b"b"c\'][0]' + var obj = { + a: { + 'b"b"c': [12345] + } + } + expect(Path.get(obj, path)).toBe(12345) + }) + + it('set success', function () { + var path = 'a.b.c' + var obj = { + a: { + b: { + c: null + } + } + } + var res = Path.set(obj, path, 12345) + expect(res).toBe(true) + expect(obj.a.b.c).toBe(12345) + }) + + it('set fail', function () { + var path = 'a.b.c' + var obj = { + a: null + } + var res = Path.set(obj, path, 12345) + expect(res).toBe(false) + }) + +}) \ No newline at end of file diff --git a/test/unit/template-parser.js b/test/unit/template.js similarity index 100% rename from test/unit/template-parser.js rename to test/unit/template.js From 64870b77ee7b9eeb4d4b245f76e040cb1c9300f1 Mon Sep 17 00:00:00 2001 From: Evan You Date: Mon, 28 Jul 2014 00:03:43 -0400 Subject: [PATCH 0059/1534] mergeOptions should perform 3-way merge for assets --- src/api/global.js | 6 +----- src/api/lifecycle.js | 1 - src/instance/init.js | 26 ++++++++++++----------- src/instance/scope.js | 2 +- src/util/index.js | 48 ++++++++++++++++++++++++++++++------------- src/util/lang.js | 4 +--- 6 files changed, 51 insertions(+), 36 deletions(-) diff --git a/src/api/global.js b/src/api/global.js index 1277db39171..3b7187843de 100644 --- a/src/api/global.js +++ b/src/api/global.js @@ -43,11 +43,7 @@ exports.extend = function (extendOptions) { } Sub.prototype = Object.create(Super.prototype) _.define(Sub.prototype, 'constructor', Sub) - Sub.options = _.mergeOptions( - Super.options, - extendOptions, - true // indicates an inheritance merge - ) + Sub.options = _.mergeOptions(Super.options, extendOptions) Sub.super = Super // allow further extension Sub.extend = Super.extend diff --git a/src/api/lifecycle.js b/src/api/lifecycle.js index b6937eb830d..784b049687b 100644 --- a/src/api/lifecycle.js +++ b/src/api/lifecycle.js @@ -12,7 +12,6 @@ var _ = require('../util') exports.$mount = function (el) { this._initElement(el) this._compile() - this._isMounted = true } /** diff --git a/src/instance/init.js b/src/instance/init.js index fda9ca1e397..5b4e580faf5 100644 --- a/src/instance/init.js +++ b/src/instance/init.js @@ -11,20 +11,29 @@ var _ = require('../util') */ exports._init = function (options) { + + options = options || {} + + this.$el = null + this.$parent = options.parent + this._isBlock = false + this._isDestroyed = false + this._rawContent = null + // merge options. this.$options = _.mergeOptions( this.constructor.options, - options + options, + this ) // create scope. - // @creates this.$parent // @creates this.$scope this._initScope() // setup initial data. // @creates this._data - this._initData(this.$options.data || {}, true) + this._initData(options.data || {}, true) // setup property proxying this._initProxy() @@ -33,15 +42,8 @@ exports._init = function (options) { // @creates this._rootBinding this._initBindings() - // compilation and lifecycle related state properties - this.$el = null - this._rawContent = null - this._isBlock = false - this._isMounted = false - this._isDestroyed = false - // if `el` option is passed, start compilation. - if (this.$options.el) { - this.$mount(this.$options.el) + if (options.el) { + this.$mount(options.el) } } \ No newline at end of file diff --git a/src/instance/scope.js b/src/instance/scope.js index 6d721db6975..3a265570884 100644 --- a/src/instance/scope.js +++ b/src/instance/scope.js @@ -11,7 +11,7 @@ var scopeEvents = ['set', 'mutate', 'add', 'delete'] */ exports._initScope = function () { - var parent = this.$parent = this.$options.parent + var parent = this.$parent var scope = this.$scope = parent ? Object.create(parent.$scope) : {} diff --git a/src/util/index.js b/src/util/index.js index 315442442d4..afb600f382e 100644 --- a/src/util/index.js +++ b/src/util/index.js @@ -1,8 +1,8 @@ var _ = exports +var config = require('../config') var env = require('./env') var lang = require('./lang') var dom = require('./dom') - var mixin = lang.mixin mixin(_, env) @@ -13,6 +13,12 @@ mixin(_, dom) * Option overwriting strategies are functions that handle * how to merge a parent option value and a child option * value into the final value. + * + * All strategy functions follow the same signature: + * + * @param {*} parentVal + * @param {*} childVal + * @param {Vue} [vm] */ var strats = {} @@ -32,26 +38,39 @@ strats.paramAttributes = function (parentVal, childVal) { } /** - * Assets, methods and computed properties are hash objects, - * and are merged with prototypal inheritance. + * Assets + * + * When a vm is present (instance creation), we need to do a + * 3-way merge for assets: constructor assets, instance assets, + * and instance scope assets. */ strats.directives = strats.filters = strats.partials = strats.effects = -strats.components = +strats.components = function (parentVal, childVal, key, vm) { + var ret = Object.create(vm.$parent + ? vm.$parent.$options[key] + : null) + mixin(ret, parentVal) + mixin(ret, childVal) + return ret +} + +/** + * Methods and computed properties + */ + strats.methods = strats.computed = function (parentVal, childVal) { var ret = Object.create(parentVal || null) - for (var key in childVal) { - ret[key] = childVal[key] - } + mixin(ret, childVal) return ret } /** - * Default strategy - overwrite if child value is not undefined. + * Default strategy */ var defaultStrat = function (parentVal, childVal) { @@ -66,10 +85,11 @@ var defaultStrat = function (parentVal, childVal) { * * @param {Object} parent * @param {Object} child - * @param {Boolean} inheriting + * @param {Vue} [vm] - if vm is present, indicates this is + * an instantiation merge. */ -exports.mergeOptions = function (parent, child, inheriting) { +_.mergeOptions = function (parent, child, vm) { var options = {} var key for (key in parent) { @@ -81,7 +101,7 @@ exports.mergeOptions = function (parent, child, inheriting) { } } function merge (key) { - if (inheriting && (key === 'el' || key === 'data')) { + if (!vm && (key === 'el' || key === 'data' || key === 'parent')) { _.warn( 'The "' + key + '" option can only be used as an instantiation ' + 'option and will be ignored in Vue.extend().' @@ -89,7 +109,7 @@ exports.mergeOptions = function (parent, child, inheriting) { return } var strat = strats[key] || defaultStrat - options[key] = strat(parent[key], child[key]) + options[key] = strat(parent[key], child[key], key, vm) } return options } @@ -112,7 +132,7 @@ function enableDebug () { * @param {String} msg */ - exports.log = function (msg) { + _.log = function (msg) { if (hasConsole && config.debug) { console.log(msg) } @@ -124,7 +144,7 @@ function enableDebug () { * @param {String} msg */ - exports.warn = function (msg) { + _.warn = function (msg) { if (hasConsole && !config.silent) { console.warn(msg) if (config.debug && console.trace) { diff --git a/src/util/lang.js b/src/util/lang.js index c295f49ce5d..6a4f75a6b9a 100644 --- a/src/util/lang.js +++ b/src/util/lang.js @@ -20,9 +20,7 @@ exports.toArray = function (list, i) { exports.mixin = function (to, from) { for (var key in from) { - if (to[key] !== from[key]) { - to[key] = from[key] - } + to[key] = from[key] } } From 9b06655b36534413dc3d8c13a95e1cdb34964c4e Mon Sep 17 00:00:00 2001 From: Evan You Date: Mon, 28 Jul 2014 00:08:07 -0400 Subject: [PATCH 0060/1534] mixin -> extend --- src/instance/init.js | 4 ++-- src/util/index.js | 14 +++++++------- src/util/lang.js | 2 +- src/vue.js | 24 ++++++++++++------------ 4 files changed, 22 insertions(+), 22 deletions(-) diff --git a/src/instance/init.js b/src/instance/init.js index 5b4e580faf5..32c20ec6e6d 100644 --- a/src/instance/init.js +++ b/src/instance/init.js @@ -1,4 +1,4 @@ -var _ = require('../util') +var mergeOptions = require('../util').mergeOptions /** * The main init sequence. This is called for every instance, @@ -21,7 +21,7 @@ exports._init = function (options) { this._rawContent = null // merge options. - this.$options = _.mergeOptions( + this.$options = mergeOptions( this.constructor.options, options, this diff --git a/src/util/index.js b/src/util/index.js index afb600f382e..97f7325892a 100644 --- a/src/util/index.js +++ b/src/util/index.js @@ -3,11 +3,11 @@ var config = require('../config') var env = require('./env') var lang = require('./lang') var dom = require('./dom') -var mixin = lang.mixin +var extend = lang.extend -mixin(_, env) -mixin(_, lang) -mixin(_, dom) +extend(_, env) +extend(_, lang) +extend(_, dom) /** * Option overwriting strategies are functions that handle @@ -53,8 +53,8 @@ strats.components = function (parentVal, childVal, key, vm) { var ret = Object.create(vm.$parent ? vm.$parent.$options[key] : null) - mixin(ret, parentVal) - mixin(ret, childVal) + extend(ret, parentVal) + extend(ret, childVal) return ret } @@ -65,7 +65,7 @@ strats.components = function (parentVal, childVal, key, vm) { strats.methods = strats.computed = function (parentVal, childVal) { var ret = Object.create(parentVal || null) - mixin(ret, childVal) + extend(ret, childVal) return ret } diff --git a/src/util/lang.js b/src/util/lang.js index 6a4f75a6b9a..a3c7f7bfb1c 100644 --- a/src/util/lang.js +++ b/src/util/lang.js @@ -18,7 +18,7 @@ exports.toArray = function (list, i) { * @param {Object} from */ -exports.mixin = function (to, from) { +exports.extend = function (to, from) { for (var key in from) { to[key] = from[key] } diff --git a/src/vue.js b/src/vue.js index 1a6c9f50ee3..04ce7f283d6 100644 --- a/src/vue.js +++ b/src/vue.js @@ -1,4 +1,4 @@ -var _ = require('./util') +var extend = require('./util').extend /** * The exposed Vue constructor. @@ -54,26 +54,26 @@ Object.defineProperty(p, '$data', { * Mixin internal instance methods */ -_.mixin(p, require('./instance/init')) -_.mixin(p, require('./instance/scope')) -_.mixin(p, require('./instance/data')) -_.mixin(p, require('./instance/proxy')) -_.mixin(p, require('./instance/bindings')) -_.mixin(p, require('./instance/compile')) +extend(p, require('./instance/init')) +extend(p, require('./instance/scope')) +extend(p, require('./instance/data')) +extend(p, require('./instance/proxy')) +extend(p, require('./instance/bindings')) +extend(p, require('./instance/compile')) /** * Mixin public API methods */ -_.mixin(p, require('./api/data')) -_.mixin(p, require('./api/dom')) -_.mixin(p, require('./api/events')) -_.mixin(p, require('./api/lifecycle')) +extend(p, require('./api/data')) +extend(p, require('./api/dom')) +extend(p, require('./api/events')) +extend(p, require('./api/lifecycle')) /** * Mixin global API */ -_.mixin(Vue, require('./api/global')) +extend(Vue, require('./api/global')) module.exports = Vue \ No newline at end of file From 69e915401ea18e9b66fd6c2474a465f1df21cf44 Mon Sep 17 00:00:00 2001 From: Evan You Date: Mon, 28 Jul 2014 11:26:56 -0400 Subject: [PATCH 0061/1534] more util refactor, add build banner --- gruntfile.js | 32 ++++++++- package.json | 1 + src/util/debug.js | 40 ++++++++++++ src/util/index.js | 160 ++------------------------------------------- src/util/option.js | 106 ++++++++++++++++++++++++++++++ 5 files changed, 184 insertions(+), 155 deletions(-) create mode 100644 src/util/debug.js create mode 100644 src/util/option.js diff --git a/gruntfile.js b/gruntfile.js index d646460a817..67bcb698fa4 100644 --- a/gruntfile.js +++ b/gruntfile.js @@ -1,5 +1,13 @@ module.exports = function (grunt) { + var version = grunt.file.readJSON('package.json').version + var banner = + '/**\n' + + ' * Vue.js v' + version + '\n' + + ' * (c) ' + new Date().getFullYear() + ' Evan You\n' + + ' * Released under the MIT License.\n' + + ' */\n' + grunt.initConfig({ jshint: { @@ -52,6 +60,9 @@ module.exports = function (grunt) { options: { bundleOptions: { standalone: 'Vue' + }, + postBundleCB: function (err, src, next) { + next(err, banner + src) } } }, @@ -70,12 +81,31 @@ module.exports = function (grunt) { src: ['benchmarks/*.js', '!benchmarks/browser.js'], dest: 'benchmarks/browser.js' } + }, + + uglify: { + build: { + options: { + banner: banner, + compress: { + pure_funcs: [ + '_.log', + '_.warn', + 'enableDebug' + ] + } + }, + files: { + 'dist/vue.min.js': ['dist/vue.js'] + } + } } }) // load npm tasks grunt.loadNpmTasks('grunt-contrib-jshint') + grunt.loadNpmTasks('grunt-contrib-uglify') grunt.loadNpmTasks('grunt-karma') grunt.loadNpmTasks('grunt-browserify') @@ -87,6 +117,6 @@ module.exports = function (grunt) { grunt.registerTask('unit', ['karma:browsers']) grunt.registerTask('phantom', ['karma:phantom']) grunt.registerTask('watch', ['browserify:watch']) - grunt.registerTask('build', ['browserify:build']) + grunt.registerTask('build', ['browserify:build', 'uglify:build']) } \ No newline at end of file diff --git a/package.json b/package.json index 611120da57b..c053465134c 100644 --- a/package.json +++ b/package.json @@ -25,6 +25,7 @@ "grunt": "^0.4.5", "grunt-browserify": "^2.1.3", "grunt-contrib-jshint": "^0.10.0", + "grunt-contrib-uglify": "^0.5.1", "grunt-karma": "^0.8.3", "jshint-stylish": "^0.3.0", "karma": "^0.12.16", diff --git a/src/util/debug.js b/src/util/debug.js new file mode 100644 index 00000000000..4df9c5bbc10 --- /dev/null +++ b/src/util/debug.js @@ -0,0 +1,40 @@ +var config = require('../config') + +/** + * Enable debug utilities. The enableDebug() function and all + * _.log() & _.warn() calls will be dropped in the minified + * production build. + */ + +enableDebug() + +function enableDebug () { + var hasConsole = typeof console !== 'undefined' + + /** + * Log a message. + * + * @param {String} msg + */ + + exports.log = function (msg) { + if (hasConsole && config.debug) { + console.log(msg) + } + } + + /** + * We've got a problem here. + * + * @param {String} msg + */ + + exports.warn = function (msg) { + if (hasConsole && !config.silent) { + console.warn(msg) + if (config.debug && console.trace) { + console.trace(msg) + } + } + } +} \ No newline at end of file diff --git a/src/util/index.js b/src/util/index.js index 97f7325892a..9831fa9e4b2 100644 --- a/src/util/index.js +++ b/src/util/index.js @@ -1,156 +1,8 @@ -var _ = exports -var config = require('../config') -var env = require('./env') -var lang = require('./lang') -var dom = require('./dom') +var lang = require('./lang') var extend = lang.extend -extend(_, env) -extend(_, lang) -extend(_, dom) - -/** - * Option overwriting strategies are functions that handle - * how to merge a parent option value and a child option - * value into the final value. - * - * All strategy functions follow the same signature: - * - * @param {*} parentVal - * @param {*} childVal - * @param {Vue} [vm] - */ - -var strats = {} - -/** - * Hooks and param attributes are merged as arrays. - */ - -strats.created = -strats.ready = -strats.attached = -strats.detached = -strats.beforeDestroy = -strats.afterDestroy = -strats.paramAttributes = function (parentVal, childVal) { - return (parentVal || []).concat(childVal || []) -} - -/** - * Assets - * - * When a vm is present (instance creation), we need to do a - * 3-way merge for assets: constructor assets, instance assets, - * and instance scope assets. - */ - -strats.directives = -strats.filters = -strats.partials = -strats.effects = -strats.components = function (parentVal, childVal, key, vm) { - var ret = Object.create(vm.$parent - ? vm.$parent.$options[key] - : null) - extend(ret, parentVal) - extend(ret, childVal) - return ret -} - -/** - * Methods and computed properties - */ - -strats.methods = -strats.computed = function (parentVal, childVal) { - var ret = Object.create(parentVal || null) - extend(ret, childVal) - return ret -} - -/** - * Default strategy - */ - -var defaultStrat = function (parentVal, childVal) { - return childVal === undefined - ? parentVal - : childVal -} - -/** - * Merge two option objects into a new one. - * Core utility used in both instantiation and inheritance. - * - * @param {Object} parent - * @param {Object} child - * @param {Vue} [vm] - if vm is present, indicates this is - * an instantiation merge. - */ - -_.mergeOptions = function (parent, child, vm) { - var options = {} - var key - for (key in parent) { - merge(key) - } - for (key in child) { - if (!(key in parent)) { - merge(key) - } - } - function merge (key) { - if (!vm && (key === 'el' || key === 'data' || key === 'parent')) { - _.warn( - 'The "' + key + '" option can only be used as an instantiation ' + - 'option and will be ignored in Vue.extend().' - ) - return - } - var strat = strats[key] || defaultStrat - options[key] = strat(parent[key], child[key], key, vm) - } - return options -} - -/** - * Enable debug utilities. The enableDebug() function and all - * _.log() & _.warn() calls will be dropped in the minified - * production build. - */ - -enableDebug() - -function enableDebug () { - - var hasConsole = typeof console !== 'undefined' - - /** - * Log a message. - * - * @param {String} msg - */ - - _.log = function (msg) { - if (hasConsole && config.debug) { - console.log(msg) - } - } - - /** - * We've got a problem here. - * - * @param {String} msg - */ - - _.warn = function (msg) { - if (hasConsole && !config.silent) { - console.warn(msg) - if (config.debug && console.trace) { - console.trace(msg) - } - } - } - -} \ No newline at end of file +extend(exports, lang) +extend(exports, require('./env')) +extend(exports, require('./dom')) +extend(exports, require('./option')) +extend(exports, require('./debug')) \ No newline at end of file diff --git a/src/util/option.js b/src/util/option.js new file mode 100644 index 00000000000..5433ae9cc64 --- /dev/null +++ b/src/util/option.js @@ -0,0 +1,106 @@ +var extend = require('./lang').extend + +/** + * Option overwriting strategies are functions that handle + * how to merge a parent option value and a child option + * value into the final value. + * + * All strategy functions follow the same signature: + * + * @param {*} parentVal + * @param {*} childVal + * @param {Vue} [vm] + */ + +var strats = {} + +/** + * Hooks and param attributes are merged as arrays. + */ + +strats.created = +strats.ready = +strats.attached = +strats.detached = +strats.beforeDestroy = +strats.afterDestroy = +strats.paramAttributes = function (parentVal, childVal) { + return (parentVal || []).concat(childVal || []) +} + +/** + * Assets + * + * When a vm is present (instance creation), we need to do a + * 3-way merge for assets: constructor assets, instance assets, + * and instance scope assets. + */ + +strats.directives = +strats.filters = +strats.partials = +strats.effects = +strats.components = function (parentVal, childVal, key, vm) { + var ret = Object.create(vm.$parent + ? vm.$parent.$options[key] + : null) + extend(ret, parentVal) + extend(ret, childVal) + return ret +} + +/** + * Methods and computed properties + */ + +strats.methods = +strats.computed = function (parentVal, childVal) { + var ret = Object.create(parentVal || null) + extend(ret, childVal) + return ret +} + +/** + * Default strategy + */ + +var defaultStrat = function (parentVal, childVal) { + return childVal === undefined + ? parentVal + : childVal +} + +/** + * Merge two option objects into a new one. + * Core utility used in both instantiation and inheritance. + * + * @param {Object} parent + * @param {Object} child + * @param {Vue} [vm] - if vm is present, indicates this is + * an instantiation merge. + */ + +exports.mergeOptions = function (parent, child, vm) { + var options = {} + var key + for (key in parent) { + merge(key) + } + for (key in child) { + if (!(key in parent)) { + merge(key) + } + } + function merge (key) { + if (!vm && (key === 'el' || key === 'data' || key === 'parent')) { + _.warn( + 'The "' + key + '" option can only be used as an instantiation ' + + 'option and will be ignored in Vue.extend().' + ) + return + } + var strat = strats[key] || defaultStrat + options[key] = strat(parent[key], child[key], key, vm) + } + return options +} \ No newline at end of file From af194e48bb03da9d231fdc58f3774a83f0e63899 Mon Sep 17 00:00:00 2001 From: Evan You Date: Mon, 28 Jul 2014 16:51:06 -0400 Subject: [PATCH 0062/1534] expression parser & test --- package.json | 2 +- src/parse/expression.js | 155 +++++++++++++++++++++++++++++++++++ test/unit/expression_spec.js | 98 ++++++++++++++++++++++ 3 files changed, 254 insertions(+), 1 deletion(-) create mode 100644 test/unit/expression_spec.js diff --git a/package.json b/package.json index c053465134c..651f3b61ca6 100644 --- a/package.json +++ b/package.json @@ -18,7 +18,7 @@ "homepage": "http://vuejs.org", "scripts": { "test": "grunt ci", - "jasmine": "jasmine-node test/unit/ --verbose" + "jasmine": "jasmine-node test/unit/" }, "devDependencies": { "browserify": "^4.2.0", diff --git a/src/parse/expression.js b/src/parse/expression.js index e69de29bb2d..9595a12eee7 100644 --- a/src/parse/expression.js +++ b/src/parse/expression.js @@ -0,0 +1,155 @@ +var _ = require('../util') +var Cache = require('../cache') +var expressionCache = new Cache(1000) + +function noop () {} + +/** + * Extract all accessor paths from an expression. + * + * @param {String} code + * @return {Array} - extracted paths + */ + +// remove strings and object literal keys that could contain arbitrary chars +var PREPARE_RE = /'[^']*'|"[^"]*"|[\{,]\s*[\w\$_]+\s*:/g +// turn anything that is not valid path char into commas +var CONVERT_RE = /[^\w$\.]+/g +// remove keywords & number literals +var KEYWORDS = 'Math,break,case,catch,continue,debugger,default,delete,do,else,false,finally,for,function,if,in,instanceof,new,null,return,switch,this,throw,true,try,typeof,var,void,while,with,undefined,abstract,boolean,byte,char,class,const,double,enum,export,extends,final,float,goto,implements,import,int,interface,long,native,package,private,protected,public,short,static,super,synchronized,throws,transient,volatile,arguments,let,yield' +var KEYWORDS_RE = new RegExp('\\b' + KEYWORDS.replace(/,/g, '\\b|\\b') + '\\b|\\b\\d[^,]*', 'g') +// remove trailing commas +var COMMA_RE = /^,+|,+$/ +// split by commas +var SPLIT_RE = /,+/ + +function extractPaths (code) { + code = code + .replace(PREPARE_RE, ',') + .replace(CONVERT_RE, ',') + .replace(KEYWORDS_RE, '') + .replace(COMMA_RE, '') + return code + ? code.split(SPLIT_RE) + : [] +} + +/** + * Escape leading dollar signs from paths for regex construction. + * Otherwise it can be mistaken as the linestart token. + * + * @param {String} path + * @return {String} + */ + +function escapeDollar (path) { + return path.charAt(0) === '$' + ? '\\' + path + : path +} + +/** + * Save / Rewrite / Restore + * + * When rewriting paths found in an expression, it is possible + * for the same letter sequences to be found in strings and Object + * literal property keys. Therefore we remove and store these + * parts in a temporary array, and restore them after the path + * rewrite. + */ + +var saved = [] +var NEWLINE_RE = /\n/g +var RESTORE_RE = /<%(\d+)%>/g + +/** + * Save replacer + * + * @param {String} str + * @return {String} - placeholder with index + */ + +function save (str) { + var i = saved.length + saved[i] = str.replace(NEWLINE_RE, '\\n') + return '<%' + i + '%>' +} + +/** + * Path rewrite replacer + * + * @param {String} path + * @return {String} + */ + +function rewrite (path) { + return path.charAt(0) + 'scope.' + path.slice(1) +} + +/** + * Restore replacer + * + * @param {String} str + * @param {String} i - matched save index + * @return {String} + */ + +function restore (str, i) { + return saved[i] +} + +/** + * Build a getter function. Requires eval. + * We isolate the try/catch so it doesn't affect the optimization + * of the parse function when it is not called. + * + * @param {String} body + * @return {Function|undefined} + */ + +function build (body) { + try { + return new Function('scope', body) + } catch (e) {} +} + +/** + * Parse an expression and rewrite into a getter function + * + * @param {String} code + * @return {Function} + */ + +exports.parse = function (code) { + // try cache + var hit = expressionCache.get(code) + if (hit) { + return hit + } + // extract paths + var paths = extractPaths(code) + var body = 'return ' + code + ';' + // rewrite paths + if (paths.length) { + var pathRE = new RegExp( + '[^$\\w\\.](' + + paths.map(escapeDollar).join('|') + + ')[^$\\w\\.]', + 'g' + ) + saved.length = 0 + body = body + .replace(PREPARE_RE, save) + .replace(pathRE, rewrite) + .replace(RESTORE_RE, restore) + } + // generate function + var fn = build(body) + if (fn) { + expressionCache.put(code, fn) + } else { + _.warn('Invalid expression: "' + code + '"\nGenerated function body: ' + body) + console.log(paths) + } + return fn || noop +} \ No newline at end of file diff --git a/test/unit/expression_spec.js b/test/unit/expression_spec.js new file mode 100644 index 00000000000..8c59414099f --- /dev/null +++ b/test/unit/expression_spec.js @@ -0,0 +1,98 @@ +var expParser = require('../../src/parse/expression') + +function assertExp (testCase) { + var fn = expParser.parse(testCase.exp) + expect(fn(testCase.scope)).toEqual(testCase.expected) +} + +var testCases = [ + { + // string concat + exp: 'a + b', + scope: { + a: 'hello', + b: 'world' + }, + expected: 'helloworld' + }, + { + // math + exp: 'a - b * 2 + 45', + scope: { + a: 100, + b: 23 + }, + expected: 100 - 23 * 2 + 45 + }, + { + // boolean logic + exp: '(a && b) ? c : d || e', + scope: { + a: true, + b: false, + c: null, + d: false, + e: 'worked' + }, + expected: 'worked' + }, + { + // inline string + exp: "a + 'hello'", + scope: { + a: 'inline ' + }, + expected: 'inline hello' + }, + { + // complex with nested values + exp: "todo.title + ' : ' + (todo.done ? 'yep' : 'nope')", + scope: { + todo: { + title: 'write tests', + done: false + } + }, + expected: 'write tests : nope' + }, + { + // expression with no data variables + exp: "'a' + 'b'", + scope: {}, + expected: 'ab' + }, + { + // values with same variable name inside strings + exp: "'\"test\"' + test + \"'hi'\" + hi", + scope: { + test: 1, + hi: 2 + }, + expected: '"test"1\'hi\'2' + }, + { + // expressions with inline object literals + exp: "sortRows({ column: 'name', test: haha, durrr: 123 })", + scope: { + sortRows: function (params) { + return params.column + params.test + params.durrr + }, + haha: 'hoho' + }, + expected: 'namehoho123' + } +] + +describe('Expression Parser', function () { + + it('parse', function () { + testCases.forEach(assertExp) + }) + + it('cache', function () { + var fn1 = expParser.parse('a + b') + var fn2 = expParser.parse('a + b') + expect(fn1).toBe(fn2) + }) + +}) \ No newline at end of file From 2bb8d115abc91c9414d5706d7676b91e4b59ace9 Mon Sep 17 00:00:00 2001 From: Evan You Date: Mon, 28 Jul 2014 17:08:08 -0400 Subject: [PATCH 0063/1534] remove log --- src/parse/expression.js | 1 - 1 file changed, 1 deletion(-) diff --git a/src/parse/expression.js b/src/parse/expression.js index 9595a12eee7..e7bb62ffc89 100644 --- a/src/parse/expression.js +++ b/src/parse/expression.js @@ -149,7 +149,6 @@ exports.parse = function (code) { expressionCache.put(code, fn) } else { _.warn('Invalid expression: "' + code + '"\nGenerated function body: ' + body) - console.log(paths) } return fn || noop } \ No newline at end of file From 7b142af05aa38764a4d3f6a72e119a3d00df0565 Mon Sep 17 00:00:00 2001 From: Evan You Date: Mon, 28 Jul 2014 17:08:23 -0400 Subject: [PATCH 0064/1534] allow $parent/$root access on $scope --- src/instance/scope.js | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/src/instance/scope.js b/src/instance/scope.js index 3a265570884..62a4d2e5a80 100644 --- a/src/instance/scope.js +++ b/src/instance/scope.js @@ -23,6 +23,21 @@ exports._initScope = function () { if (!parent) return + // scope parent accessor + Object.defineProperty(scope, '$parent', { + get: function () { + return parent.$scope + } + }) + + // scope root accessor + var self = this + Object.defineProperty(scope, '$root', { + get: function () { + return self.$root.$scope + } + }) + // relay change events that sent down from // the scope prototype chain. var ob = this._observer From a8419063d4a81027c1654ee5e30db74950888d91 Mon Sep 17 00:00:00 2001 From: Evan You Date: Mon, 28 Jul 2014 17:16:28 -0400 Subject: [PATCH 0065/1534] improve expression test cases --- test/unit/expression_spec.js | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/test/unit/expression_spec.js b/test/unit/expression_spec.js index 8c59414099f..3fc6fc2f0d5 100644 --- a/test/unit/expression_spec.js +++ b/test/unit/expression_spec.js @@ -37,12 +37,21 @@ var testCases = [ expected: 'worked' }, { - // inline string - exp: "a + 'hello'", + // inline string with newline + exp: "a + 'hel\nlo'", scope: { a: 'inline ' }, - expected: 'inline hello' + expected: 'inline hel\nlo' + }, + { + // dollar signs and underscore + exp: "_a + ' ' + $b", + scope: { + _a: 'underscore', + $b: 'dollar' + }, + expected: 'underscore dollar' }, { // complex with nested values From 724aec3aaf6e37ef442016607833d596acfb445a Mon Sep 17 00:00:00 2001 From: Evan You Date: Tue, 29 Jul 2014 10:26:16 -0400 Subject: [PATCH 0066/1534] update component.json --- component.json | 46 +++++++++++++++++++++++++---------------- src/parse/expression.js | 1 - tasks/component.js | 5 ++++- 3 files changed, 32 insertions(+), 20 deletions(-) diff --git a/component.json b/component.json index 5c8a3a953ec..bb65c44d4e7 100644 --- a/component.json +++ b/component.json @@ -11,33 +11,43 @@ ], "license": "MIT", "scripts": [ - "src/api/asset-register.js", - "src/api/config.js", - "src/api/extend.js", - "src/api/require.js", - "src/api/use.js", + "src/api/data.js", + "src/api/dom.js", + "src/api/events.js", + "src/api/global.js", + "src/api/lifecycle.js", "src/batcher.js", "src/binding.js", - "src/compiler/compiler.js", + "src/cache.js", "src/config.js", "src/directive.js", + "src/directives/index.js", "src/emitter.js", + "src/filters/index.js", + "src/instance/bindings.js", + "src/instance/compile.js", "src/instance/data.js", - "src/instance/dom.js", - "src/instance/events.js", - "src/instance/lifecycle.js", - "src/observer/array-augmentations.js", - "src/observer/object-augmentations.js", - "src/observer/observer.js", - "src/parsers/directive.js", - "src/parsers/expression.js", - "src/parsers/path.js", - "src/parsers/text.js", - "src/template.js", + "src/instance/element.js", + "src/instance/init.js", + "src/instance/proxy.js", + "src/instance/scope.js", + "src/observe/array-augmentations.js", + "src/observe/object-augmentations.js", + "src/observe/observer.js", + "src/parse/directive.js", + "src/parse/expression.js", + "src/parse/path.js", + "src/parse/template.js", + "src/parse/text.js", "src/transition/css.js", "src/transition/js.js", "src/transition/transition.js", - "src/util.js", + "src/util/debug.js", + "src/util/dom.js", + "src/util/env.js", + "src/util/index.js", + "src/util/lang.js", + "src/util/option.js", "src/vue.js" ] } \ No newline at end of file diff --git a/src/parse/expression.js b/src/parse/expression.js index e7bb62ffc89..56213c16fb6 100644 --- a/src/parse/expression.js +++ b/src/parse/expression.js @@ -36,7 +36,6 @@ function extractPaths (code) { /** * Escape leading dollar signs from paths for regex construction. - * Otherwise it can be mistaken as the linestart token. * * @param {String} path * @return {String} diff --git a/tasks/component.js b/tasks/component.js index cef54a4cd51..03f7d789008 100644 --- a/tasks/component.js +++ b/tasks/component.js @@ -3,11 +3,14 @@ module.exports = function (grunt) { grunt.registerTask('component', function () { + var jsRE = /\.js$/ var component = grunt.file.readJSON('component.json') component.scripts = [] grunt.file.recurse('src', function (file) { - component.scripts.push(file) + if (jsRE.test(file)) { + component.scripts.push(file) + } }) grunt.file.write('component.json', JSON.stringify(component, null, 2)) From 0060fe0f0bb90e2b5cc1d30cce9eb292e28b67b2 Mon Sep 17 00:00:00 2001 From: Evan You Date: Tue, 29 Jul 2014 13:40:05 -0400 Subject: [PATCH 0067/1534] directive parser and test --- src/parse/directive.js | 132 ++++++++++++++++++ test/unit/directive_parser_spec.js | 106 ++++++++++++++ ...sion_spec.js => expression_parser_spec.js} | 0 .../{path_spec.js => path_parser_spec.js} | 0 4 files changed, 238 insertions(+) create mode 100644 test/unit/directive_parser_spec.js rename test/unit/{expression_spec.js => expression_parser_spec.js} (100%) rename test/unit/{path_spec.js => path_parser_spec.js} (100%) diff --git a/src/parse/directive.js b/src/parse/directive.js index e69de29bb2d..f99799c5013 100644 --- a/src/parse/directive.js +++ b/src/parse/directive.js @@ -0,0 +1,132 @@ +var Cache = require('../cache') +var cache = new Cache(1000) +var ARG_RE = /^[\w\$-]+$/ +var FILTER_TOKEN_RE = /[^\s'"]+|'[^']+'|"[^"]+"/g + +/** + * Parser state + */ + +var str +var c, i, l +var inSingle +var inDouble +var curly +var square +var paren +var begin +var argIndex +var dirs +var dir +var lastFilterIndex +var arg + +/** + * Push a directive object into the result Array + */ + +function pushDir () { + dir.raw = str.slice(begin, i).trim() + if (dir.expression === undefined) { + dir.expression = str.slice(argIndex, i).trim() + } else if (lastFilterIndex !== begin) { + pushFilter() + } + if (i === 0 || dir.expression) { + dirs.push(dir) + } +} + +/** + * Push a filter to the current directive object + */ + +function pushFilter () { + var exp = str.slice(lastFilterIndex, i).trim() + var filter + if (exp) { + filter = {} + var tokens = exp.match(FILTER_TOKEN_RE) + filter.name = tokens[0] + filter.args = tokens.length > 1 ? tokens.slice(1) : null + } + if (filter) { + (dir.filters = dir.filters || []).push(filter) + } + lastFilterIndex = i + 1 +} + +/** + * Parse a directive string into an Array of AST-like objects + * representing directives. + * + * @param {String} str + * @return {Array} + */ + +exports.parse = function (s) { + + var hit = cache.get(s) + if (hit) { + return hit + } + + // reset parser state + str = s + inSingle = inDouble = false + curly = square = paren = begin = argIndex = lastFilterIndex = 0 + dirs = [] + dir = {} + arg = null + + for (i = 0, l = str.length; i < l; i++) { + c = str.charAt(i) + if (inSingle) { + // check single quote + if (c === "'") inSingle = !inSingle + } else if (inDouble) { + // check double quote + if (c === '"') inDouble = !inDouble + } else if (c === ',' && !paren && !curly && !square) { + // reached the end of a directive + pushDir() + // reset & skip the comma + dir = {} + begin = argIndex = lastFilterIndex = i + 1 + } else if (c === ':' && !dir.expression && !dir.arg) { + // argument + arg = str.slice(begin, i).trim() + if (ARG_RE.test(arg)) { + argIndex = i + 1 + dir.arg = arg + } + } else if (c === '|' && str.charAt(i + 1) !== '|' && str.charAt(i - 1) !== '|') { + if (dir.expression === undefined) { + // first filter, end of expression + lastFilterIndex = i + 1 + dir.expression = str.slice(argIndex, i).trim() + } else { + // already has filter + pushFilter() + } + } else { + switch (c) { + case '"': inDouble = true; break + case "'": inSingle = true; break + case '(': paren++; break + case ')': paren--; break + case '[': square++; break + case ']': square--; break + case '{': curly++; break + case '}': curly--; break + } + } + } + + if (i === 0 || begin !== i) { + pushDir() + } + + cache.put(s, dirs) + return dirs +} \ No newline at end of file diff --git a/test/unit/directive_parser_spec.js b/test/unit/directive_parser_spec.js new file mode 100644 index 00000000000..7b244d1331d --- /dev/null +++ b/test/unit/directive_parser_spec.js @@ -0,0 +1,106 @@ +var parse = require('../../src/parse/directive').parse + +describe('Directive Parser', function () { + + it('exp', function () { + var res = parse('exp') + expect(res.length).toBe(1) + expect(res[0].expression).toBe('exp') + expect(res[0].raw).toBe('exp') + }) + + it('arg:exp', function () { + var res = parse('arg:exp') + expect(res.length).toBe(1) + expect(res[0].expression).toBe('exp') + expect(res[0].arg).toBe('arg') + expect(res[0].raw).toBe('arg:exp') + }) + + it('arg : exp | abc', function () { + var res = parse(' arg : exp | abc de') + expect(res.length).toBe(1) + expect(res[0].expression).toBe('exp') + expect(res[0].arg).toBe('arg') + expect(res[0].raw).toBe('arg : exp | abc de') + expect(res[0].filters.length).toBe(1) + expect(res[0].filters[0].name).toBe('abc') + expect(res[0].filters[0].args.length).toBe(1) + expect(res[0].filters[0].args[0]).toBe('de') + }) + + it('a || b | c', function () { + var res = parse('a || b | c') + expect(res.length).toBe(1) + expect(res[0].expression).toBe('a || b') + expect(res[0].raw).toBe('a || b | c') + expect(res[0].filters.length).toBe(1) + expect(res[0].filters[0].name).toBe('c') + expect(res[0].filters[0].args).toBeNull() + }) + + it('a ? b : c', function () { + var res = parse('a ? b : c') + expect(res.length).toBe(1) + expect(res[0].expression).toBe('a ? b : c') + expect(res[0].filters).toBeUndefined() + }) + + it('"a:b:c||d|e|f" || d ? a : b', function () { + var res = parse('"a:b:c||d|e|f" || d ? a : b') + expect(res.length).toBe(1) + expect(res[0].expression).toBe('"a:b:c||d|e|f" || d ? a : b') + expect(res[0].filters).toBeUndefined() + expect(res[0].arg).toBeUndefined() + }) + + it('a, b, c', function () { + var res = parse('a, b, c') + expect(res.length).toBe(3) + expect(res[0].expression).toBe('a') + expect(res[1].expression).toBe('b') + expect(res[2].expression).toBe('c') + }) + + it('a:b | c, d:e | f, g:h | i', function () { + var res = parse('a:b | c, d:e | f, g:h | i') + expect(res.length).toBe(3) + + expect(res[0].arg).toBe('a') + expect(res[0].expression).toBe('b') + expect(res[0].filters.length).toBe(1) + expect(res[0].filters[0].name).toBe('c') + expect(res[0].filters[0].args).toBeNull() + + expect(res[1].arg).toBe('d') + expect(res[1].expression).toBe('e') + expect(res[1].filters.length).toBe(1) + expect(res[1].filters[0].name).toBe('f') + expect(res[1].filters[0].args).toBeNull() + + expect(res[2].arg).toBe('g') + expect(res[2].expression).toBe('h') + expect(res[2].filters.length).toBe(1) + expect(res[2].filters[0].name).toBe('i') + expect(res[2].filters[0].args).toBeNull() + }) + + it('click:test(c.indexOf(d,f),"e,f"), input: d || [e,f], ok:{a:1,b:2}', function () { + var res = parse('click:test(c.indexOf(d,f),"e,f"), input: d || [e,f], ok:{a:1,b:2}') + expect(res.length).toBe(3) + expect(res[0].arg).toBe('click') + expect(res[0].expression).toBe('test(c.indexOf(d,f),"e,f")') + expect(res[1].arg).toBe('input') + expect(res[1].expression).toBe('d || [e,f]') + expect(res[1].filters).toBeUndefined() + expect(res[2].arg).toBe('ok') + expect(res[2].expression).toBe('{a:1,b:2}') + }) + + it('cache', function () { + var res1 = parse('a || b | c') + var res2 = parse('a || b | c') + expect(res1).toBe(res2) + }) + +}) \ No newline at end of file diff --git a/test/unit/expression_spec.js b/test/unit/expression_parser_spec.js similarity index 100% rename from test/unit/expression_spec.js rename to test/unit/expression_parser_spec.js diff --git a/test/unit/path_spec.js b/test/unit/path_parser_spec.js similarity index 100% rename from test/unit/path_spec.js rename to test/unit/path_parser_spec.js From c5cb21bbe83dc4a135200f78d8cc60146c801f4a Mon Sep 17 00:00:00 2001 From: Evan You Date: Tue, 29 Jul 2014 15:39:57 -0400 Subject: [PATCH 0068/1534] test for util --- changes.md | 42 ------- src/util/env.js | 6 +- src/util/option.js | 19 ++- test/unit/util_spec.js | 274 +++++++++++++++++++++++++++++++++++++++++ 4 files changed, 291 insertions(+), 50 deletions(-) create mode 100644 test/unit/util_spec.js diff --git a/changes.md b/changes.md index 16774e627f3..f48a99707c5 100644 --- a/changes.md +++ b/changes.md @@ -59,48 +59,6 @@ Vue.filter('format', { ``` -## (Experimental) More flexible directive syntax - - - v-on - - ``` html - - ``` - - - v-style - - ``` html - - ``` - - - custom directive - - ``` html - fsef - ``` - - - v-with - - ``` html -
- ``` - - - v-repeat - - ``` html -
    -
  • -
- ``` - ## (Experimental) Validators ``` html diff --git a/src/util/env.js b/src/util/env.js index 52bcedb7ef6..e8ce99e4a6c 100644 --- a/src/util/env.js +++ b/src/util/env.js @@ -1,12 +1,14 @@ /** * Are we in a browser or in Node? + * Calling toString on window has inconsistent results in browsers + * so we do it on the document instead. * * @type {Boolean} */ var inBrowser = exports.inBrowser = - typeof window !== 'undefined' && - Object.prototype.toString.call(window) === '[object global]' + typeof document !== 'undefined' && + Object.prototype.toString.call(document) === '[object HTMLDocument]' /** * Defer a task to the start of the next event loop diff --git a/src/util/option.js b/src/util/option.js index 5433ae9cc64..f8c983d49fc 100644 --- a/src/util/option.js +++ b/src/util/option.js @@ -1,3 +1,5 @@ +// alias debug as _ so we can drop _.warn during uglification +var _ = require('./debug') var extend = require('./lang').extend /** @@ -41,21 +43,26 @@ strats.filters = strats.partials = strats.effects = strats.components = function (parentVal, childVal, key, vm) { - var ret = Object.create(vm.$parent - ? vm.$parent.$options[key] - : null) + var ret = Object.create( + vm && vm.$parent + ? vm.$parent.$options[key] + : null + ) extend(ret, parentVal) extend(ret, childVal) return ret } /** - * Methods and computed properties + * Other object hashes. + * These are instance-specific and do not inehrit from nested parents. */ strats.methods = -strats.computed = function (parentVal, childVal) { - var ret = Object.create(parentVal || null) +strats.computed = +strats.events = function (parentVal, childVal) { + var ret = Object.create(null) + extend(ret, parentVal) extend(ret, childVal) return ret } diff --git a/test/unit/util_spec.js b/test/unit/util_spec.js new file mode 100644 index 00000000000..4605ee79795 --- /dev/null +++ b/test/unit/util_spec.js @@ -0,0 +1,274 @@ +var _ = require('../../src/util') +var config = require('../../src/config') +config.silent = true + +describe('Util', function () { + + describe('Language Enhancement', function () { + + it('toArray', function () { + // should make a copy of original array + var arr = [1,2,3] + var res = _.toArray(arr) + expect(Array.isArray(res)).toBe(true) + expect(res.toString()).toEqual('1,2,3') + expect(res).not.toBe(arr) + + // should work on arguments + ;(function () { + var res = _.toArray(arguments) + expect(Array.isArray(res)).toBe(true) + expect(res.toString()).toEqual('1,2,3') + })(1,2,3) + }) + + it('extend', function () { + var from = {a:1,b:2} + var to = {} + _.extend(to, from) + expect(to.a).toBe(from.a) + expect(to.b).toBe(from.b) + }) + + it('deepMixin', function () { + var from = Object.create({c:123}) + var to = {} + Object.defineProperty(from, 'a', { + enumerable: false, + configurable: true, + get: function () { + return 'AAA' + } + }) + Object.defineProperty(from, 'b', { + enumerable: true, + configurable: false, + value: 'BBB' + }) + _.deepMixin(to, from) + var descA = Object.getOwnPropertyDescriptor(to, 'a') + var descB = Object.getOwnPropertyDescriptor(to, 'b') + + expect(descA.enumerable).toBe(false) + expect(descA.configurable).toBe(true) + expect(to.a).toBe('AAA') + + expect(descB.enumerable).toBe(true) + expect(descB.configurable).toBe(false) + expect(to.b).toBe('BBB') + + expect(to.c).toBeUndefined() + }) + + it('proxy', function () { + var to = { test2: 'to' } + var from = { test2: 'from' } + var val = '123' + Object.defineProperty(from, 'test', { + get: function () { + return val + }, + set: function (v) { + val = v + } + }) + _.proxy(to, from, 'test') + expect(to.test).toBe(val) + to.test = '234' + expect(val).toBe('234') + expect(to.test).toBe(val) + // should not overwrite existing property + _.proxy(to, from, 'test2') + expect(to.test2).toBe('to') + + }) + + it('isObject', function () { + expect(_.isObject({})).toBe(true) + expect(_.isObject([])).toBe(false) + expect(_.isObject(null)).toBe(false) + if (_.inBrowser) { + expect(_.isObject(window)).toBe(false) + } + }) + + it('isArray', function () { + expect(_.isArray([])).toBe(true) + expect(_.isArray({})).toBe(false) + expect(_.isArray(arguments)).toBe(false) + }) + + it('define', function () { + var obj = {} + _.define(obj, 'test', 123) + expect(obj.test).toBe(123) + var desc = Object.getOwnPropertyDescriptor(obj, 'test') + expect(desc.enumerable).toBe(false) + + _.define(obj, 'test2', 123, true) + expect(obj.test2).toBe(123) + var desc = Object.getOwnPropertyDescriptor(obj, 'test2') + expect(desc.enumerable).toBe(true) + }) + + it('augment', function () { + if ('__proto__' in {}) { + var target = {} + var proto = {} + _.augment(target, proto) + expect(target.__proto__).toBe(proto) + } else { + expect(_.augment).toBe(_.deepMixin) + } + }) + + }) + + if (_.inBrowser) { + + describe('DOM', function () { + + var parent, child, target + + function div () { + return document.createElement('div') + } + + beforeEach(function () { + parent = div() + child = div() + target = div() + parent.appendChild(child) + }) + + it('before', function () { + _.before(target, child) + expect(target.parentNode).toBe(parent) + expect(target.nextSibling).toBe(child) + }) + + it('after', function () { + _.after(target, child) + expect(target.parentNode).toBe(parent) + expect(child.nextSibling).toBe(target) + }) + + it('remove', function () { + _.remove(child) + expect(child.parentNode).toBeNull() + expect(parent.childNodes.length).toBe(0) + }) + + it('prepend', function () { + _.prepend(target, parent) + expect(target.parentNode).toBe(parent) + expect(parent.firstChild).toBe(target) + }) + + it('copyAttributes', function () { + parent.setAttribute('test1', 1) + parent.setAttribute('test2', 2) + _.copyAttributes(parent, target) + expect(target.attributes.length).toBe(2) + expect(target.getAttribute('test1')).toBe('1') + expect(target.getAttribute('test2')).toBe('2') + }) + + }) + + } + + describe('Option merging', function () { + + var merge = _.mergeOptions + + it('default strat', function () { + // child undefined + var res = merge({replace:true}, {}).replace + expect(res).toBe(true) + // child overwrite + var res = merge({replace:true}, {replace:false}).replace + expect(res).toBe(false) + }) + + it('hooks & paramAttributes', function () { + var fn1 = function () {} + var fn2 = function () {} + var res + // parent undefined + res = merge({}, {created: fn1}).created + expect(Array.isArray(res)).toBe(true) + expect(res.length).toBe(1) + expect(res[0]).toBe(fn1) + // child undefined + res = merge({created: [fn1]}, {}).created + expect(Array.isArray(res)).toBe(true) + expect(res.length).toBe(1) + expect(res[0]).toBe(fn1) + // both defined + res = merge({created: [fn1]}, {created: fn2}).created + expect(Array.isArray(res)).toBe(true) + expect(res.length).toBe(2) + expect(res[0]).toBe(fn1) + expect(res[1]).toBe(fn2) + }) + + it('object hashes', function () { + var fn1 = function () {} + var fn2 = function () {} + var res + // parent undefined + res = merge({}, {events: {test: fn1}}).events + expect(res.test).toBe(fn1) + // child undefined + res = merge({events: {test: fn1}}, {}).events + expect(res.test).toBe(fn1) + // both defined + var parent = {events: {test: fn1}} + res = merge(parent, {events: {test2: fn2}}).events + expect(res.test).toBe(fn1) + expect(res.test2).toBe(fn2) + }) + + it('assets', function () { + var asset1 = {} + var asset2 = {} + var asset3 = {} + // mock vm + var vm = { + $parent: { + $options: { + directives: { + c: asset3 + } + } + } + } + var res = merge( + { directives: { a: asset1 }}, + { directives: { b: asset2 }}, + vm + ).directives + expect(res.a).toBe(asset1) + expect(res.b).toBe(asset2) + expect(res.c).toBe(asset3) + // test prototypal inheritance + var asset4 = vm.$parent.$options.directives.d = {} + expect(res.d).toBe(asset4) + }) + + it('ignore el, data & parent when inheriting', function () { + var res = merge({}, {el:1, data:2, parent:3}) + expect(res.el).toBeUndefined() + expect(res.data).toBeUndefined() + expect(res.parent).toBeUndefined() + + res = merge({}, {el:1, data:2, parent:3}, {}) + expect(res.el).toBe(1) + expect(res.data).toBe(2) + expect(res.parent).toBe(3) + }) + + }) + +}) \ No newline at end of file From 0474b1f9d970a23dd3e58030c72ef3a67570ab02 Mon Sep 17 00:00:00 2001 From: Evan You Date: Tue, 29 Jul 2014 15:44:19 -0400 Subject: [PATCH 0069/1534] jshint pass --- src/instance/element.js | 4 +++- src/parse/path.js | 4 ++-- src/parse/template.js | 3 ++- src/util/env.js | 2 ++ 4 files changed, 9 insertions(+), 4 deletions(-) diff --git a/src/instance/element.js b/src/instance/element.js index ef76961e5fd..f3f18ced375 100644 --- a/src/instance/element.js +++ b/src/instance/element.js @@ -18,7 +18,7 @@ exports._initElement = function (el) { // considered a "block instance" which manages not a single element, // but multiple elements. A block instance's `$el` is an Array of // the elements it manages. - if (el instanceof DocumentFragment) { + if (el instanceof window.DocumentFragment) { this._isBlock = true this.$el = _.toArray(el.childNodes) } else { @@ -77,6 +77,8 @@ exports._initTemplate = function () { */ exports._collectRawContent = function () { + var el = this.$el + var child if (el.hasChildNodes()) { this.rawContent = document.createElement('div') /* jshint boss: true */ diff --git a/src/parse/path.js b/src/parse/path.js index fb5c05eeceb..fdaf9d02cbd 100644 --- a/src/parse/path.js +++ b/src/parse/path.js @@ -1,8 +1,8 @@ var _ = require('../util') var Cache = require('../cache') var pathCache = new Cache(1000) -var identStart = '[\$_a-zA-Z]' -var identPart = '[\$_a-zA-Z0-9]' +var identStart = '[$_a-zA-Z]' +var identPart = '[$_a-zA-Z0-9]' var IDENT_RE = new RegExp('^' + identStart + '+' + identPart + '*' + '$') /** diff --git a/src/parse/template.js b/src/parse/template.js index 14f3db69e2a..81ea8033721 100644 --- a/src/parse/template.js +++ b/src/parse/template.js @@ -68,6 +68,7 @@ function stringToFragment (templateString) { } var child + /* jshint boss:true */ while (child = node.firstChild) { if (node.nodeType === 1) { frag.appendChild(child) @@ -116,7 +117,7 @@ exports.parse = function (template) { var node, frag // if the template is already a document fragment -- do nothing - if (template instanceof DocumentFragment) { + if (template instanceof window.DocumentFragment) { return template } diff --git a/src/util/env.js b/src/util/env.js index e8ce99e4a6c..224c6e4ddaa 100644 --- a/src/util/env.js +++ b/src/util/env.js @@ -1,3 +1,5 @@ +/* global chrome */ + /** * Are we in a browser or in Node? * Calling toString on window has inconsistent results in browsers From e2ddfa6f579adfdaa8dd14349dc42b99bc31a579 Mon Sep 17 00:00:00 2001 From: Evan You Date: Tue, 29 Jul 2014 16:36:56 -0400 Subject: [PATCH 0070/1534] record more changes --- changes.md | 24 +++++++++++++++++++++++- src/util/option.js | 10 +++++++--- 2 files changed, 30 insertions(+), 4 deletions(-) diff --git a/changes.md b/changes.md index f48a99707c5..c8e0911e5eb 100644 --- a/changes.md +++ b/changes.md @@ -22,10 +22,32 @@ The result of this model is a much cleaner expression evaluation implementation. This is very useful, but it probably should only be available in implicit child instances created by flow-control directives like `v-repeat`, `v-if`, etc. Explicit components should retain its own root scope and use some sort of two way binding like `v-with` to bind to data from outer scope. -- new option: `syncData`. +## Option changes + +### new option: `syncData`. A side effect of the new scope/data model is that the `data` object being passed in is no longer mutated by default, because all its properties are copied into the scope instead. To sync changes to the scope back to the original data object, you need to now explicitly pass in `syncData: true` in the options. In most cases, this is not necessary, but you do need to be aware of this. +### new option: `events`. + +When events are used extensively for cross-vm communication, the ready hook can get kinda messy. The new `events` option is similar to its Backbone equivalent, where you can declaratiely register a bunch of event listeners. + +### removed options: `id`, `tagName`, `className`, `attributes`, `lazy`. + +Since now a vm must always be provided the `el` option or explicitly mounted to an existing element, the element creation releated options have been removed for simplicity. If you need to modify your element's attributes, simply do so in the new `beforeMount` hook. + +The `lazy` option is removed because this does not belong at the vm level. Users should be able to configure individual `v-model` instances to be lazy or not. + +## Hook changes + +### new hook: `beforeMount` + +This new hook is introduced to accompany the separation of instantiation and DOM mounting. It is called right before the DOM compilation starts and `this.$el` is available, so you can do some pre-processing on the element here. + +### removed hooks: `attached` & `detached` + +These two have caused confusions about when they'd actually fire, and proper use cases seem to be rare. Let me know if you have important use cases for these two hooks. + ## Two Way filters ``` html diff --git a/src/util/option.js b/src/util/option.js index f8c983d49fc..eb50d2685f8 100644 --- a/src/util/option.js +++ b/src/util/option.js @@ -22,8 +22,7 @@ var strats = {} strats.created = strats.ready = -strats.attached = -strats.detached = +strats.beforeMount = strats.beforeDestroy = strats.afterDestroy = strats.paramAttributes = function (parentVal, childVal) { @@ -68,7 +67,12 @@ strats.events = function (parentVal, childVal) { } /** - * Default strategy + * Default strategy. + * Applies to: + * - data + * - el + * - parent + * - replace */ var defaultStrat = function (parentVal, childVal) { From 0ed49727216cd45a942914e115e363f7cab43fc4 Mon Sep 17 00:00:00 2001 From: Evan You Date: Tue, 29 Jul 2014 17:21:06 -0400 Subject: [PATCH 0071/1534] more changes --- changes.md | 8 +++++++- src/instance/data.js | 1 - src/instance/init.js | 8 +++++--- 3 files changed, 12 insertions(+), 5 deletions(-) diff --git a/changes.md b/changes.md index c8e0911e5eb..045be41400f 100644 --- a/changes.md +++ b/changes.md @@ -24,9 +24,15 @@ This is very useful, but it probably should only be available in implicit child ## Option changes +### instance-only options + +`el`, `parent` and `data` are now instance-only options - that means they should not be used and will be ignored in `Vue.extend()`. + +It's probably easy to understand why `el` and `parent` are instance only. But why `data`? Because it's really easy to shoot yourself in the foot when you use `data` in `Vue.extend()`. Non-primitive values will be shared by reference across all instances created from that constructor, and changing it from one instance will affect the state of all the others! It's a bit like shared properties on the prototype. In vanilla javascript, the proper way to initialize instance data is to do so in the constructor: `this.someData = {}`. Similarly in Vue, you can do so in the `created` hook by setting `this.$data.someData = {}`. + ### new option: `syncData`. -A side effect of the new scope/data model is that the `data` object being passed in is no longer mutated by default, because all its properties are copied into the scope instead. To sync changes to the scope back to the original data object, you need to now explicitly pass in `syncData: true` in the options. In most cases, this is not necessary, but you do need to be aware of this. +A side effect of the new scope/data model is that the `data` object being passed in is no longer mutated by default, because all its properties are copied into the scope instead, and the scope is now the source of truth. To sync changes to the scope back to the original data object, you need to now explicitly pass in `syncData: true` in the options. In most cases, this is not necessary, but you do need to be aware of this. ### new option: `events`. diff --git a/src/instance/data.js b/src/instance/data.js index fad8ea353b5..fbeea380920 100644 --- a/src/instance/data.js +++ b/src/instance/data.js @@ -49,7 +49,6 @@ exports._initData = function (data, init) { } // setup sync between scope and new data - this._data = data if (options.syncData) { this._dataObserver = Observer.create(data) this._sync() diff --git a/src/instance/init.js b/src/instance/init.js index 32c20ec6e6d..c8df1d00b41 100644 --- a/src/instance/init.js +++ b/src/instance/init.js @@ -16,8 +16,9 @@ exports._init = function (options) { this.$el = null this.$parent = options.parent + this._data = options.data || {} this._isBlock = false - this._isDestroyed = false + this._isDestroyed = false this._rawContent = null // merge options. @@ -27,13 +28,14 @@ exports._init = function (options) { this ) + // TODO fire created hook + // create scope. // @creates this.$scope this._initScope() // setup initial data. - // @creates this._data - this._initData(options.data || {}, true) + this._initData(this._data, true) // setup property proxying this._initProxy() From 4facaabd1ef1e6cc26a03faced439b0b5d21e1ef Mon Sep 17 00:00:00 2001 From: Evan You Date: Tue, 29 Jul 2014 17:27:33 -0400 Subject: [PATCH 0072/1534] more changes --- changes.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/changes.md b/changes.md index 045be41400f..13ab93f1a21 100644 --- a/changes.md +++ b/changes.md @@ -56,9 +56,7 @@ These two have caused confusions about when they'd actually fire, and proper use ## Two Way filters -``` html - -``` +If a filter is defined as a function, it is treated as a read filter by default - i.e. it is applied when data is read from the model and applied to the DOM. You can now specify write filters as well, which are applied when writing to the model, triggered by user input. Write filters are only triggered on two-way bindings like `v-model`. ``` js Vue.filter('format', { @@ -89,6 +87,8 @@ Vue.filter('format', { ## (Experimental) Validators +This is largely write filters that accept a Boolean return value. Probably should live as a plugin. + ``` html ``` From 6ee31d75ee1dc4235ceca67415c92e81985d4234 Mon Sep 17 00:00:00 2001 From: Evan You Date: Tue, 29 Jul 2014 22:38:28 -0400 Subject: [PATCH 0073/1534] assume has eval for now. CSP build should be in separate branch. --- src/parse/path.js | 4 +--- src/util/env.js | 26 -------------------------- 2 files changed, 1 insertion(+), 29 deletions(-) diff --git a/src/parse/path.js b/src/parse/path.js index fdaf9d02cbd..de28789f3e8 100644 --- a/src/parse/path.js +++ b/src/parse/path.js @@ -257,9 +257,7 @@ exports.parse = function (path) { if (!hit) { hit = parsePath(path) if (hit) { - if (_.hasEval) { - hit.get = exports.compileGetter(hit) - } + hit.get = exports.compileGetter(hit) pathCache.put(path, hit) } } diff --git a/src/util/env.js b/src/util/env.js index 224c6e4ddaa..32ed2caafac 100644 --- a/src/util/env.js +++ b/src/util/env.js @@ -28,32 +28,6 @@ exports.nextTick = function (fn) { return defer(fn, 0) } -/** - * Detect if the environment allows creating - * a function from strings. - * - * @type {Boolean} - */ - -exports.hasEval = (function () { - // chrome apps enforces CSP - if (typeof chrome !== 'undefined' && - chrome.app && - chrome.app.runtime) { - return false - } - // so does Firefox OS apps... - if (inBrowser && navigator.getDeviceStorage) { - return false - } - try { - var f = new Function('', 'return true;') - return f() - } catch (e) { - return false - } -})() - /** * Detect if we are in IE9... * From 8a868b8b17f97357f496afbecf635831e2bbd372 Mon Sep 17 00:00:00 2001 From: Evan You Date: Tue, 29 Jul 2014 23:35:49 -0400 Subject: [PATCH 0074/1534] expression parser generate setter too --- src/parse/expression.js | 54 +++++++++++++++++++++-------- test/unit/expression_parser_spec.js | 14 ++++++-- 2 files changed, 51 insertions(+), 17 deletions(-) diff --git a/src/parse/expression.js b/src/parse/expression.js index 56213c16fb6..1f41e19a295 100644 --- a/src/parse/expression.js +++ b/src/parse/expression.js @@ -2,8 +2,6 @@ var _ = require('../util') var Cache = require('../cache') var expressionCache = new Cache(1000) -function noop () {} - /** * Extract all accessor paths from an expression. * @@ -82,7 +80,7 @@ function save (str) { */ function rewrite (path) { - return path.charAt(0) + 'scope.' + path.slice(1) + return 'scope.' + path } /** @@ -99,6 +97,7 @@ function restore (str, i) { /** * Build a getter function. Requires eval. + * * We isolate the try/catch so it doesn't affect the optimization * of the parse function when it is not called. * @@ -106,20 +105,41 @@ function restore (str, i) { * @return {Function|undefined} */ -function build (body) { +function makeGetter (body) { + try { + return new Function('scope', 'return ' + body + ';') + } catch (e) {} +} + +/** + * Build a setter function. + * + * This is only needed in rare situations like "a[b]" where + * a settable path requires dynamic evaluation. + * + * This setter function may throw error when called if the + * expression body is not a valid left-hand expression in + * assignment. + * + * @param {String} body + * @return {Function|undefined} + */ + +function makeSetter (body) { try { - return new Function('scope', body) + return new Function('scope', 'value', body + ' = value;') } catch (e) {} } /** - * Parse an expression and rewrite into a getter function + * Parse an expression and rewrite into a getter/setter functions * * @param {String} code + * @param {Boolean} needSet * @return {Function} */ -exports.parse = function (code) { +exports.parse = function (code, needSet) { // try cache var hit = expressionCache.get(code) if (hit) { @@ -127,27 +147,31 @@ exports.parse = function (code) { } // extract paths var paths = extractPaths(code) - var body = 'return ' + code + ';' + var body = code // rewrite paths if (paths.length) { var pathRE = new RegExp( - '[^$\\w\\.](' + + '(\\b|\\$)' + paths.map(escapeDollar).join('|') + - ')[^$\\w\\.]', + '(\\b|\\$)', 'g' ) saved.length = 0 - body = body + body = body // pad for regex .replace(PREPARE_RE, save) .replace(pathRE, rewrite) .replace(RESTORE_RE, restore) + .trim() } // generate function - var fn = build(body) - if (fn) { - expressionCache.put(code, fn) + var getter = makeGetter(body) + if (getter) { + if (needSet) { + getter.setter = makeSetter(body) + } + expressionCache.put(code, getter) } else { _.warn('Invalid expression: "' + code + '"\nGenerated function body: ' + body) } - return fn || noop + return getter } \ No newline at end of file diff --git a/test/unit/expression_parser_spec.js b/test/unit/expression_parser_spec.js index 3fc6fc2f0d5..c406edadaab 100644 --- a/test/unit/expression_parser_spec.js +++ b/test/unit/expression_parser_spec.js @@ -8,7 +8,7 @@ function assertExp (testCase) { var testCases = [ { // string concat - exp: 'a + b', + exp: 'a+b', scope: { a: 'hello', b: 'world' @@ -94,10 +94,20 @@ var testCases = [ describe('Expression Parser', function () { - it('parse', function () { + it('parse getter', function () { testCases.forEach(assertExp) }) + it('parse setter', function () { + var setter = expParser.parse('a[b]', true).setter + var scope = { + a: { c: 1 }, + b: 'c' + } + setter(scope, 2) + expect(scope.a.c).toBe(2) + }) + it('cache', function () { var fn1 = expParser.parse('a + b') var fn2 = expParser.parse('a + b') From fe1d9d8cf444443479c4236d29be0a9b5de9bb4b Mon Sep 17 00:00:00 2001 From: Evan You Date: Wed, 30 Jul 2014 00:20:58 -0400 Subject: [PATCH 0075/1534] instantiation bench vs old version & make benches run in browsers only --- benchmarks/bench.js | 3 ++ benchmarks/instantiation.js | 99 ++++++++++++++++++++++++++++++------- benchmarks/observer.js | 52 +++++++++++-------- gruntfile.js | 3 +- src/vue.js | 1 + tasks/bench.js | 31 ------------ 6 files changed, 120 insertions(+), 69 deletions(-) create mode 100644 benchmarks/bench.js delete mode 100644 tasks/bench.js diff --git a/benchmarks/bench.js b/benchmarks/bench.js new file mode 100644 index 00000000000..8c509f9af67 --- /dev/null +++ b/benchmarks/bench.js @@ -0,0 +1,3 @@ +require('./observer').run(function () { + require('./instantiation').run() +}) \ No newline at end of file diff --git a/benchmarks/instantiation.js b/benchmarks/instantiation.js index 0c689bcb309..c4b6e0f14b2 100644 --- a/benchmarks/instantiation.js +++ b/benchmarks/instantiation.js @@ -1,22 +1,20 @@ console.log('\nInstantiation\n') +var done = null +var OldVue = require('../../vue') var Vue = require('../src/vue') var sideEffect = null var parent = new Vue({ data: { a: 1 } }) - -function getNano () { - var hr = process.hrtime() - return hr[0] * 1e9 + hr[1] -} +var oldParent = new OldVue({ + data: { a: 1 } +}) function now () { - return process.hrtime - ? getNano() / 1e6 - : window.performence - ? window.performence.now() - : Date.now() + return window.performence + ? window.performence.now() + : Date.now() } // warm up @@ -24,31 +22,81 @@ for (var i = 0; i < 1000; i++) { sideEffect = new Vue() } +var queue = [] + function bench (desc, n, fn) { - var s = now() - for (var i = 0; i < n; i++) { - fn() + queue.push(function () { + var s = now() + for (var i = 0; i < n; i++) { + fn() + } + var time = now() - s + var opf = (16 / (time / n)).toFixed(2) + console.log(desc + ' ' + n + ' times - ' + opf + ' ops/frame') + }) +} + +function run () { + queue.shift()() + if (queue.length) { + setTimeout(run, 0) + } else { + done && done() } - var time = now() - s - var opf = (16 / (time / n)).toFixed(2) - console.log(desc + ' ' + n + ' times - ' + opf + ' ops/frame') } function simpleInstance () { sideEffect = new Vue({ + el: document.createElement('div'), + data: {a: 1} + }) +} + +function oldSimpleInstance () { + sideEffect = new OldVue({ data: {a: 1} }) } function simpleInstanceWithInheritance () { sideEffect = new Vue({ + el: document.createElement('div'), parent: parent, data: { b:2 } }) } +function oldSimpleInstanceWithInheritance () { + sideEffect = new OldVue({ + parent: oldParent, + data: { b:2 } + }) +} + function complexInstance () { sideEffect = new Vue({ + el: document.createElement('div'), + data: { + a: { + b: { + c: 1 + } + }, + c: { + b: { + c: { a:1 }, + d: 2, + e: 3, + d: 4 + } + }, + e: [{a:1}, {a:2}, {a:3}] + } + }) +} + +function oldComplexInstance () { + sideEffect = new OldVue({ data: { a: { b: { @@ -72,10 +120,27 @@ bench('Simple instance', 10, simpleInstance) bench('Simple instance', 100, simpleInstance) bench('Simple instance', 1000, simpleInstance) +bench('Simple instance (old)', 10, oldSimpleInstance) +bench('Simple instance (old)', 100, oldSimpleInstance) +bench('Simple instance (old)', 1000, oldSimpleInstance) + bench('Simple instance with inheritance', 10, simpleInstanceWithInheritance) bench('Simple instance with inheritance', 100, simpleInstanceWithInheritance) bench('Simple instance with inheritance', 1000, simpleInstanceWithInheritance) +bench('Simple instance with inheritance (old)', 10, oldSimpleInstanceWithInheritance) +bench('Simple instance with inheritance (old)', 100, oldSimpleInstanceWithInheritance) +bench('Simple instance with inheritance (old)', 1000, oldSimpleInstanceWithInheritance) + bench('Complex instance', 10, complexInstance) bench('Complex instance', 100, complexInstance) -bench('Complex instance', 1000, complexInstance) \ No newline at end of file +bench('Complex instance', 1000, complexInstance) + +bench('Complex instance (old)', 10, oldComplexInstance) +bench('Complex instance (old)', 100, oldComplexInstance) +bench('Complex instance (old)', 1000, oldComplexInstance) + +exports.run = function (cb) { + done = cb + run() +} \ No newline at end of file diff --git a/benchmarks/observer.js b/benchmarks/observer.js index bd307765cdf..5dc835170b2 100644 --- a/benchmarks/observer.js +++ b/benchmarks/observer.js @@ -1,5 +1,6 @@ console.log('\nObserver\n') +var done = null var Observer = require('../src/observe/observer') var Emitter = require('../src/emitter') var OldObserver = require('../../vue/src/observer') @@ -9,30 +10,36 @@ function cb () { sideEffect = !sideEffect } -function getNano () { - var hr = process.hrtime() - return hr[0] * 1e9 + hr[1] -} - function now () { - return process.hrtime - ? getNano() / 1e6 - : window.performence - ? window.performence.now() - : Date.now() + return window.performence + ? window.performence.now() + : Date.now() } +var queue = [] + function bench (desc, fac, run) { - var objs = [] - for (var i = 0; i < runs; i++) { - objs.push(fac(i)) - } - var s = now() - for (var i = 0; i < runs; i++) { - run(objs[i]) + queue.push(function () { + var objs = [] + for (var i = 0; i < runs; i++) { + objs.push(fac(i)) + } + var s = now() + for (var i = 0; i < runs; i++) { + run(objs[i]) + } + var passed = now() - s + console.log(desc + ' - ' + (16 / (passed / runs)).toFixed(2) + ' ops/frame') + }) +} + +function run () { + queue.shift()() + if (queue.length) { + setTimeout(run, 0) + } else { + done && done() } - var passed = now() - s - console.log(desc + ' - ' + (16 / (passed / runs)).toFixed(2) + ' ops/frame') } bench( @@ -363,4 +370,9 @@ bench( function (o) { o.reverse() } -) \ No newline at end of file +) + +exports.run = function (cb) { + done = cb + run() +} \ No newline at end of file diff --git a/gruntfile.js b/gruntfile.js index 67bcb698fa4..0d64df4d1be 100644 --- a/gruntfile.js +++ b/gruntfile.js @@ -78,7 +78,7 @@ module.exports = function (grunt) { } }, bench: { - src: ['benchmarks/*.js', '!benchmarks/browser.js'], + src: ['benchmarks/bench.js'], dest: 'benchmarks/browser.js' } }, @@ -116,6 +116,7 @@ module.exports = function (grunt) { grunt.registerTask('unit', ['karma:browsers']) grunt.registerTask('phantom', ['karma:phantom']) + grunt.registerTask('bench', ['browserify:bench']) grunt.registerTask('watch', ['browserify:watch']) grunt.registerTask('build', ['browserify:build', 'uglify:build']) diff --git a/src/vue.js b/src/vue.js index 04ce7f283d6..f835acaa898 100644 --- a/src/vue.js +++ b/src/vue.js @@ -59,6 +59,7 @@ extend(p, require('./instance/scope')) extend(p, require('./instance/data')) extend(p, require('./instance/proxy')) extend(p, require('./instance/bindings')) +extend(p, require('./instance/element')) extend(p, require('./instance/compile')) /** diff --git a/tasks/bench.js b/tasks/bench.js deleted file mode 100644 index 39bd8ca45b0..00000000000 --- a/tasks/bench.js +++ /dev/null @@ -1,31 +0,0 @@ -/** - * Run benchmarks in Node - */ - -module.exports = function (grunt) { - grunt.registerTask('bench', function (target) { - - // polyfill window/document for old Vue - global.window = { - setTimeout: setTimeout, - console: console - } - global.document = { - documentElement: {} - } - - if (target) { - run(target) - } else { - require('fs') - .readdirSync('./benchmarks') - .forEach(run) - } - - function run (mod) { - if (mod === 'browser.js' || mod === 'runner.html') return - require('../benchmarks/' + mod) - } - - }) -} \ No newline at end of file From a5a72ecb3ab1cb3111891bf6489276c3de2bb3cf Mon Sep 17 00:00:00 2001 From: Evan You Date: Wed, 30 Jul 2014 17:03:22 -0400 Subject: [PATCH 0076/1534] observer should ignore accessors --- src/observe/observer.js | 18 ++++++++++-------- test/unit/observer_spec.js | 24 ++++++++++++++++++++++++ 2 files changed, 34 insertions(+), 8 deletions(-) diff --git a/src/observe/observer.js b/src/observe/observer.js index d94d0a02734..107b3ca3112 100644 --- a/src/observe/observer.js +++ b/src/observe/observer.js @@ -95,14 +95,21 @@ Observer.create = function (value, options) { /** * Walk through each property, converting them and adding them as child. * This method should only be called when value type is Object. + * Properties prefixed with `$` or `_` and accessor properties are ignored. * * @param {Object} obj */ p.walk = function (obj) { - var key, val + var key, val, descriptor, prefix for (key in obj) { - if (obj.hasOwnProperty(key)) { + prefix = key.charAt(0) + if (prefix === '$' || prefix === '_') { + continue + } + descriptor = Object.getOwnPropertyDescriptor(obj, key) + // only process own non-accessor properties + if (descriptor && !descriptor.get) { val = obj[key] this.observe(key, val) this.convert(key, val) @@ -172,19 +179,14 @@ p.unobserve = function (val) { /** * Convert a property into getter/setter so we can emit * the events when the property is accessed/changed. - * Properties prefixed with `$` or `_` are ignored. * * @param {String} key * @param {*} val */ p.convert = function (key, val) { - var prefix = key.charAt(0) - if (prefix === '$' || prefix === '_') { - return - } var ob = this - Object.defineProperty(this.value, key, { + Object.defineProperty(ob.value, key, { enumerable: true, configurable: true, get: function () { diff --git a/test/unit/observer_spec.js b/test/unit/observer_spec.js index 2a162414dfd..9cd04cfa442 100644 --- a/test/unit/observer_spec.js +++ b/test/unit/observer_spec.js @@ -69,6 +69,30 @@ describe('Observer', function () { expect(spy.callCount).toBe(3) }) + it('ignore prefix', function () { + var obj = { + _test: 123, + $test: 234 + } + var ob = Observer.create(obj) + ob.on('set', spy) + obj._test = 234 + obj.$test = 345 + expect(spy.callCount).toBe(0) + }) + + it('ignore accessors', function () { + var obj = { + a: 123, + get b () { + return this.a + } + } + var ob = Observer.create(obj) + obj.a = 234 + expect(obj.b).toBe(234) + }) + it('array get', function () { Observer.emitGet = true From 02f1d934653ece7b305e86c5993b814eab6226cd Mon Sep 17 00:00:00 2001 From: Evan You Date: Wed, 30 Jul 2014 17:03:31 -0400 Subject: [PATCH 0077/1534] remove unused require in path --- src/parse/path.js | 1 - 1 file changed, 1 deletion(-) diff --git a/src/parse/path.js b/src/parse/path.js index de28789f3e8..7180ccfe791 100644 --- a/src/parse/path.js +++ b/src/parse/path.js @@ -1,4 +1,3 @@ -var _ = require('../util') var Cache = require('../cache') var pathCache = new Cache(1000) var identStart = '[$_a-zA-Z]' From f22297d574a23cd2d7f67788ff63688b56b6c12c Mon Sep 17 00:00:00 2001 From: Evan You Date: Wed, 30 Jul 2014 23:13:53 -0400 Subject: [PATCH 0078/1534] content transclusion --- src/instance/element.js | 65 ++++++++++++++++++++++++++++++++++++++++- 1 file changed, 64 insertions(+), 1 deletion(-) diff --git a/src/instance/element.js b/src/instance/element.js index f3f18ced375..337a2b75aa6 100644 --- a/src/instance/element.js +++ b/src/instance/element.js @@ -96,5 +96,68 @@ exports._collectRawContent = function () { */ exports._initContent = function () { - // TODO + var outlets = getOutlets(this.$el) + var raw = this._rawContent + var i = outlets.length + var outlet, select, j, main + if (i) { + // first pass, collect corresponding content + // for each outlet. + while (i--) { + outlet = outlets[i] + if (raw) { + select = outlet.getAttribute('select') + if (select) { // select content + outlet.content = _.toArray(raw.querySelectorAll(select)) + } else { // default content + main = outlet + } + } else { // fallback content + outlet.content = _.toArray(outlet.childNodes) + } + } + // second pass, actually insert the contents + for (i = 0, j = outlets.length; i < j; i++) { + outlet = outlets[i] + if (outlet === main) continue + insertContentAt(outlet, outlet.content) + } + // finally insert the main content + if (raw && main) { + insertContentAt(main, _.toArray(raw.childNodes)) + } + } + this._rawContent = null +} + +/** + * Get outlets from the element/list + * + * @param {Element|Array} el + * @return {Array} + */ + +var concat = [].concat + +function getOutlets (el) { + return _.isArray(el) + ? concat.apply([], el.map(getOutlets)) + : _.toArray(el.getElementsByTagName('content')) +} + +/** + * Insert an array of nodes at outlet, then remove the outlet. + * + * @param {Element} outlet + * @param {Array} contents + */ + +function insertContentAt (outlet, contents) { + // not using util DOM methods here because + // parentNode can be cached + var parent = outlet.parentNode + for (var i = 0, j = contents.length; i < j; i++) { + parent.insertBefore(contents[i], outlet) + } + parent.removeChild(outlet) } \ No newline at end of file From 132576b13836615323febaa5cfe3213790c3ca8a Mon Sep 17 00:00:00 2001 From: Evan You Date: Thu, 31 Jul 2014 00:15:06 -0400 Subject: [PATCH 0079/1534] events & hooks --- src/api/events.js | 40 ++++++++++++++++++++++++++++++++++----- src/api/lifecycle.js | 6 +++++- src/instance/events.js | 31 ++++++++++++++++++++++++++++++ src/instance/init.js | 17 +++++++++++++++-- src/util/option.js | 23 ++++++++++++++++++++-- src/vue.js | 1 + test/unit/util_spec.js | 43 +++++++++++++++++++++++++++++++++++++----- 7 files changed, 146 insertions(+), 15 deletions(-) create mode 100644 src/instance/events.js diff --git a/src/api/events.js b/src/api/events.js index ddd750e44bb..20cabbddcbf 100644 --- a/src/api/events.js +++ b/src/api/events.js @@ -1,13 +1,43 @@ +/** + * Proxy basic event methods on the internal emitter. + */ + ;['emit', 'on', 'off', 'once'].forEach(function (method) { - exports[method] = function () { - - } + var realMethod = method === 'emit' + ? 'applyEmit' + : method + exports[method] = function () { + this._emitter[realMethod].apply(this._emitter, arguments) + } }) +/** + * Recursively broadcast an event to all children instances. + * + * @param {String} event + * @param {...*} additional arguments + */ + exports.$broadcast = function () { - + var children = this.children + for (var i = 0, l = children.length; i < l; i++) { + var child = children[i] + child._emitter.applyEmit.apply(child._emitter, arguments) + child.$broadcast.apply(child, arguments) + } } +/** + * Recursively propagate an event up the parent chain. + * + * @param {String} event + * @param {...*} additional arguments + */ + exports.$dispatch = function () { - + this._emitter.applyEmit.apply(this._emitter, arguments) + var parent = this.$parent + if (parent) { + parent.$dispatch.apply(parent, arguments) + } } \ No newline at end of file diff --git a/src/api/lifecycle.js b/src/api/lifecycle.js index 784b049687b..cc68a2a0b95 100644 --- a/src/api/lifecycle.js +++ b/src/api/lifecycle.js @@ -10,8 +10,10 @@ var _ = require('../util') */ exports.$mount = function (el) { + this._callHook('beforeMount') this._initElement(el) this._compile() + this._callHook('ready') } /** @@ -23,5 +25,7 @@ exports.$mount = function (el) { */ exports.$destroy = function (remove) { - + this._callHook('beforeDestroy') + // TODO + this._callHook('afterDestroy') } \ No newline at end of file diff --git a/src/instance/events.js b/src/instance/events.js new file mode 100644 index 00000000000..6f0470b0a98 --- /dev/null +++ b/src/instance/events.js @@ -0,0 +1,31 @@ +/** + * Setup the instance's option events + */ + +exports._initEvents = function () { + var events = this.$options.events + if (events) { + var handlers, e, i, j + for (e in events) { + handlers = events[e] + for (i = 0, j = handlers.length; i < j; i++) { + this.$on(e, handlers[i]) + } + } + } +} + +/** + * Trigger all handlers for a hook + * + * @param {String} hook + */ + +exports._callHook = function (hook) { + var handlers = this.$options[hook] + if (handlers) { + for (var i = 0, j = handlers.length; i < j; i++) { + handlers[i].call(this) + } + } +} \ No newline at end of file diff --git a/src/instance/init.js b/src/instance/init.js index c8df1d00b41..819d1570269 100644 --- a/src/instance/init.js +++ b/src/instance/init.js @@ -1,3 +1,4 @@ +var Emitter = require('../emitter') var mergeOptions = require('../util').mergeOptions /** @@ -15,11 +16,18 @@ exports._init = function (options) { options = options || {} this.$el = null - this.$parent = options.parent this._data = options.data || {} this._isBlock = false this._isDestroyed = false this._rawContent = null + this._emitter = new Emitter(this) + + // setup parent relationship + this.$parent = options.parent + this._children = [] + if (this.$parent) { + this.$parent._children.push(this) + } // merge options. this.$options = mergeOptions( @@ -28,7 +36,12 @@ exports._init = function (options) { this ) - // TODO fire created hook + // the `created` hook is called after basic properties have + // been set up & before data observation happens. + this._callHook('created') + + // setup event system and option events + this._initEvents() // create scope. // @creates this.$scope diff --git a/src/util/option.js b/src/util/option.js index eb50d2685f8..b92ff97a245 100644 --- a/src/util/option.js +++ b/src/util/option.js @@ -52,14 +52,33 @@ strats.components = function (parentVal, childVal, key, vm) { return ret } +/** + * Events + * + * Events should not overwrite one another, so we merge + * them as arrays. + */ + +strats.events = function (parentVal, childVal) { + var ret = Object.create(null) + extend(ret, parentVal) + for (var key in childVal) { + var parent = ret[key] + var child = childVal[key] + ret[key] = parent + ? parent.concat(child) + : [child] + } + return ret +} + /** * Other object hashes. * These are instance-specific and do not inehrit from nested parents. */ strats.methods = -strats.computed = -strats.events = function (parentVal, childVal) { +strats.computed = function (parentVal, childVal) { var ret = Object.create(null) extend(ret, parentVal) extend(ret, childVal) diff --git a/src/vue.js b/src/vue.js index f835acaa898..fefe27ef274 100644 --- a/src/vue.js +++ b/src/vue.js @@ -55,6 +55,7 @@ Object.defineProperty(p, '$data', { */ extend(p, require('./instance/init')) +extend(p, require('./instance/events')) extend(p, require('./instance/scope')) extend(p, require('./instance/data')) extend(p, require('./instance/proxy')) diff --git a/test/unit/util_spec.js b/test/unit/util_spec.js index 4605ee79795..2d69690ec08 100644 --- a/test/unit/util_spec.js +++ b/test/unit/util_spec.js @@ -213,19 +213,52 @@ describe('Util', function () { expect(res[1]).toBe(fn2) }) - it('object hashes', function () { + it('events', function () { + + var fn1 = function () {} + var fn2 = function () {} + var fn3 = function () {} + var parent = { + events: { + 'fn1': [fn1, fn2], + 'fn2': [fn2] + } + } + var child = { + events: { + 'fn1': fn3, + 'fn3': fn3 + } + } + var res = merge(parent, child).events + assertRes(res.fn1, [fn1, fn2, fn3]) + assertRes(res.fn2, [fn2]) + assertRes(res.fn3, [fn3]) + + function assertRes (res, expected) { + expect(Array.isArray(res)).toBe(true) + expect(res.length).toBe(expected.length) + var i = expected.length + while (i--) { + expect(res[i]).toBe(expected[i]) + } + } + + }) + + it('normal object hashes', function () { var fn1 = function () {} var fn2 = function () {} var res // parent undefined - res = merge({}, {events: {test: fn1}}).events + res = merge({}, {methods: {test: fn1}}).methods expect(res.test).toBe(fn1) // child undefined - res = merge({events: {test: fn1}}, {}).events + res = merge({methods: {test: fn1}}, {}).methods expect(res.test).toBe(fn1) // both defined - var parent = {events: {test: fn1}} - res = merge(parent, {events: {test2: fn2}}).events + var parent = {methods: {test: fn1}} + res = merge(parent, {methods: {test2: fn2}}).methods expect(res.test).toBe(fn1) expect(res.test2).toBe(fn2) }) From f80c22fc3b35b6ae295b0dcad94c3e5f664e98bc Mon Sep 17 00:00:00 2001 From: Evan You Date: Thu, 31 Jul 2014 00:30:58 -0400 Subject: [PATCH 0080/1534] move data-related funcs into one file --- changes.md | 13 +++++ src/instance/data.js | 124 ++++++++++++++++++++++++++++++++++++++++++ src/instance/init.js | 3 + src/instance/proxy.js | 33 ----------- src/instance/scope.js | 72 ------------------------ src/vue.js | 2 - 6 files changed, 140 insertions(+), 107 deletions(-) delete mode 100644 src/instance/proxy.js delete mode 100644 src/instance/scope.js diff --git a/changes.md b/changes.md index 13ab93f1a21..886c7fb8590 100644 --- a/changes.md +++ b/changes.md @@ -54,6 +54,19 @@ This new hook is introduced to accompany the separation of instantiation and DOM These two have caused confusions about when they'd actually fire, and proper use cases seem to be rare. Let me know if you have important use cases for these two hooks. +## Computed Properties + +`$get` and `$set` is now simply `get` and `set`: + +``` js +computed: { + fullName: { + get: function () {}, + set: function () {} + } +} +``` + ## Two Way filters If a filter is defined as a function, it is treated as a read filter by default - i.e. it is applied when data is read from the model and applied to the DOM. You can now specify write filters as well, which are applied when writing to the model, triggered by user input. Write filters are only triggered on two-way bindings like `v-model`. diff --git a/src/instance/data.js b/src/instance/data.js index fbeea380920..e304e14a1a0 100644 --- a/src/instance/data.js +++ b/src/instance/data.js @@ -1,4 +1,76 @@ +var _ = require('../util') var Observer = require('../observe/observer') +var scopeEvents = ['set', 'mutate', 'add', 'delete'] + +/** + * Setup instance scope. + * The scope is reponsible for prototypal inheritance of + * parent instance propertiesm abd all binding paths and + * expressions of the current instance are evaluated against its scope. + * + * This should only be called once during _init(). + */ + +exports._initScope = function () { + var parent = this.$parent + var scope = this.$scope = parent + ? Object.create(parent.$scope) + : {} + // create scope observer + this._observer = Observer.create(scope, { + callbackContext: this, + doNotAlterProto: true + }) + + if (!parent) return + + // scope parent accessor + Object.defineProperty(scope, '$parent', { + get: function () { + return parent.$scope + } + }) + + // scope root accessor + var self = this + Object.defineProperty(scope, '$root', { + get: function () { + return self.$root.$scope + } + }) + + // relay change events that sent down from + // the scope prototype chain. + var ob = this._observer + var pob = parent._observer + var listeners = this._scopeListeners = {} + scopeEvents.forEach(function (event) { + var cb = listeners[event] = function (key, a, b) { + // since these events come from upstream, + // we only emit them if we don't have the same keys + // shadowing them in current scope. + if (!scope.hasOwnProperty(key)) { + ob.emit(event, key, a, b) + } + } + pob.on(event, cb) + }) +} + +/** + * Teardown scope and remove listeners attached to parent scope. + * Only called once during $destroy(). + */ + +exports._teardownScope = function () { + this.$scope = null + if (!this.$parent) return + var pob = this.$parent._observer + var listeners = this._scopeListeners + scopeEvents.forEach(function (event) { + pob.off(event, listeners[event]) + }) +} /** * Setup the instances data object. @@ -55,6 +127,58 @@ exports._initData = function (data, init) { } } +/** + * Setup computed properties. + */ + +exports._initComputed = function () { + var computed = this.$options.computed + var scope = this.$scope + if (computed) { + for (var key in computed) { + var def = computed[key] + if (typeof def === 'function') { + def = { get: def } + } + def.enumerable = true + def.configurable = true + Object.defineProperty(scope, key, def) + } + } +} + +/** + * Proxy the scope properties on the instance itself, + * so that vm.a === vm.$scope.a. + * + * Note this only proxies *local* scope properties. We want to + * prevent child instances accidentally modifying properties + * with the same name up in the scope chain because scope + * perperties are all getter/setters. + * + * To access parent properties through prototypal fall through, + * access it on the instance's $scope. + * + * This should only be called once during _init(). + */ + +exports._initProxy = function () { + var scope = this.$scope + for (var key in scope) { + if (scope.hasOwnProperty(key)) { + _.proxy(this, scope, key) + } + } + // keep proxying up-to-date with added/deleted keys. + this._observer + .on('add:self', function (key) { + _.proxy(this, scope, key) + }) + .on('delete:self', function (key) { + delete this[key] + }) +} + /** * Setup two-way sync between the instance scope and * the original data. Requires teardown. diff --git a/src/instance/init.js b/src/instance/init.js index 819d1570269..1ad5757b519 100644 --- a/src/instance/init.js +++ b/src/instance/init.js @@ -50,6 +50,9 @@ exports._init = function (options) { // setup initial data. this._initData(this._data, true) + // setup computed properties + this._initComputed() + // setup property proxying this._initProxy() diff --git a/src/instance/proxy.js b/src/instance/proxy.js deleted file mode 100644 index 1ce0b7120b0..00000000000 --- a/src/instance/proxy.js +++ /dev/null @@ -1,33 +0,0 @@ -var _ = require('../util') - -/** - * Proxy the scope properties on the instance itself, - * so that vm.a === vm.$scope.a. - * - * Note this only proxies *local* scope properties. We want to - * prevent child instances accidentally modifying properties - * with the same name up in the scope chain because scope - * perperties are all getter/setters. - * - * To access parent properties through prototypal fall through, - * access it on the instance's $scope. - * - * This should only be called once during _init(). - */ - -exports._initProxy = function () { - var scope = this.$scope - for (var key in scope) { - if (scope.hasOwnProperty(key)) { - _.proxy(this, scope, key) - } - } - // keep proxying up-to-date with added/deleted keys. - this._observer - .on('add:self', function (key) { - _.proxy(this, scope, key) - }) - .on('delete:self', function (key) { - delete this[key] - }) -} \ No newline at end of file diff --git a/src/instance/scope.js b/src/instance/scope.js deleted file mode 100644 index 62a4d2e5a80..00000000000 --- a/src/instance/scope.js +++ /dev/null @@ -1,72 +0,0 @@ -var Observer = require('../observe/observer') -var scopeEvents = ['set', 'mutate', 'add', 'delete'] - -/** - * Setup instance scope. - * The scope is reponsible for prototypal inheritance of - * parent instance propertiesm abd all binding paths and - * expressions of the current instance are evaluated against its scope. - * - * This should only be called once during _init(). - */ - -exports._initScope = function () { - var parent = this.$parent - var scope = this.$scope = parent - ? Object.create(parent.$scope) - : {} - // create scope observer - this._observer = Observer.create(scope, { - callbackContext: this, - doNotAlterProto: true - }) - - if (!parent) return - - // scope parent accessor - Object.defineProperty(scope, '$parent', { - get: function () { - return parent.$scope - } - }) - - // scope root accessor - var self = this - Object.defineProperty(scope, '$root', { - get: function () { - return self.$root.$scope - } - }) - - // relay change events that sent down from - // the scope prototype chain. - var ob = this._observer - var pob = parent._observer - var listeners = this._scopeListeners = {} - scopeEvents.forEach(function (event) { - var cb = listeners[event] = function (key, a, b) { - // since these events come from upstream, - // we only emit them if we don't have the same keys - // shadowing them in current scope. - if (!scope.hasOwnProperty(key)) { - ob.emit(event, key, a, b) - } - } - pob.on(event, cb) - }) -} - -/** - * Teardown scope and remove listeners attached to parent scope. - * Only called once during $destroy(). - */ - -exports._teardownScope = function () { - this.$scope = null - if (!this.$parent) return - var pob = this.$parent._observer - var listeners = this._scopeListeners - scopeEvents.forEach(function (event) { - pob.off(event, listeners[event]) - }) -} \ No newline at end of file diff --git a/src/vue.js b/src/vue.js index fefe27ef274..d5eec3c05af 100644 --- a/src/vue.js +++ b/src/vue.js @@ -56,9 +56,7 @@ Object.defineProperty(p, '$data', { extend(p, require('./instance/init')) extend(p, require('./instance/events')) -extend(p, require('./instance/scope')) extend(p, require('./instance/data')) -extend(p, require('./instance/proxy')) extend(p, require('./instance/bindings')) extend(p, require('./instance/element')) extend(p, require('./instance/compile')) From 6d8a7dc19d4f93361367a299b0d3dfcbc3eedc68 Mon Sep 17 00:00:00 2001 From: Evan You Date: Thu, 31 Jul 2014 01:16:34 -0400 Subject: [PATCH 0081/1534] make sure computed properties and methods are always called with the right context (istance, not scope) --- src/api/events.js | 2 +- src/instance/data.js | 58 ++++++++++++++++++++++++++++++-------------- src/instance/init.js | 3 +++ src/util/lang.js | 13 ++++++++++ 4 files changed, 57 insertions(+), 19 deletions(-) diff --git a/src/api/events.js b/src/api/events.js index 20cabbddcbf..07ec71685f9 100644 --- a/src/api/events.js +++ b/src/api/events.js @@ -6,7 +6,7 @@ var realMethod = method === 'emit' ? 'applyEmit' : method - exports[method] = function () { + exports['$' + method] = function () { this._emitter[realMethod].apply(this._emitter, arguments) } }) diff --git a/src/instance/data.js b/src/instance/data.js index e304e14a1a0..9445cdbf258 100644 --- a/src/instance/data.js +++ b/src/instance/data.js @@ -24,21 +24,6 @@ exports._initScope = function () { if (!parent) return - // scope parent accessor - Object.defineProperty(scope, '$parent', { - get: function () { - return parent.$scope - } - }) - - // scope root accessor - var self = this - Object.defineProperty(scope, '$root', { - get: function () { - return self.$root.$scope - } - }) - // relay change events that sent down from // the scope prototype chain. var ob = this._observer @@ -133,7 +118,6 @@ exports._initData = function (data, init) { exports._initComputed = function () { var computed = this.$options.computed - var scope = this.$scope if (computed) { for (var key in computed) { var def = computed[key] @@ -142,11 +126,19 @@ exports._initComputed = function () { } def.enumerable = true def.configurable = true - Object.defineProperty(scope, key, def) + Object.defineProperty(this, key, def) } } } +/** + * Setup instance methods. + */ + +exports._initMethods = function () { + _.extend(this, this.$options.methods) +} + /** * Proxy the scope properties on the instance itself, * so that vm.a === vm.$scope.a. @@ -163,8 +155,14 @@ exports._initComputed = function () { */ exports._initProxy = function () { + var key + var options = this.$options var scope = this.$scope - for (var key in scope) { + + // scope --> vm + + // proxy scope data on vm + for (key in scope) { if (scope.hasOwnProperty(key)) { _.proxy(this, scope, key) } @@ -177,6 +175,30 @@ exports._initProxy = function () { .on('delete:self', function (key) { delete this[key] }) + + // vm --> scope + + // proxy vm parent & root on scope + _.proxy(scope, this, '$parent') + _.proxy(scope, this, '$root') + + // proxy computed properties on scope. + // since they are accessors, they are still bound to the vm. + var computed = options.computed + if (computed) { + for (key in computed) { + _.proxy(scope, this, key) + } + } + + // and methods need to be explicitly bound to the vm + // so it actually has all the API methods. + var methods = options.methods + if (methods) { + for (key in methods) { + scope[key] = _.bind(methods[key], this) + } + } } /** diff --git a/src/instance/init.js b/src/instance/init.js index 1ad5757b519..a6a26ec97dd 100644 --- a/src/instance/init.js +++ b/src/instance/init.js @@ -53,6 +53,9 @@ exports._init = function (options) { // setup computed properties this._initComputed() + // setup instance methods + this._initMethods() + // setup property proxying this._initProxy() diff --git a/src/util/lang.js b/src/util/lang.js index a3c7f7bfb1c..5f0101e7c12 100644 --- a/src/util/lang.js +++ b/src/util/lang.js @@ -1,3 +1,16 @@ +/** + * Simple bind, faster than native + * + * @param {Function} fn + * @param {Object} ctx + */ + +exports.bind = function (fn, ctx) { + return function () { + fn.apply(ctx, arguments) + } +} + /** * Convert an Array-like object to a real Array. * From f2798b6924166817bfe7e2321fae482aa2f65bd4 Mon Sep 17 00:00:00 2001 From: Evan You Date: Thu, 31 Jul 2014 01:20:19 -0400 Subject: [PATCH 0082/1534] data -> scope --- src/instance/{data.js => scope.js} | 0 src/vue.js | 2 +- 2 files changed, 1 insertion(+), 1 deletion(-) rename src/instance/{data.js => scope.js} (100%) diff --git a/src/instance/data.js b/src/instance/scope.js similarity index 100% rename from src/instance/data.js rename to src/instance/scope.js diff --git a/src/vue.js b/src/vue.js index d5eec3c05af..55d7964cece 100644 --- a/src/vue.js +++ b/src/vue.js @@ -56,7 +56,7 @@ Object.defineProperty(p, '$data', { extend(p, require('./instance/init')) extend(p, require('./instance/events')) -extend(p, require('./instance/data')) +extend(p, require('./instance/scope')) extend(p, require('./instance/bindings')) extend(p, require('./instance/element')) extend(p, require('./instance/compile')) From 18e50c4570af9722fa7a70617acff6b056b978b7 Mon Sep 17 00:00:00 2001 From: Evan You Date: Thu, 31 Jul 2014 08:43:25 -0400 Subject: [PATCH 0083/1534] test for computed properties and _.bind --- src/util/lang.js | 2 +- test/unit/data_spec.js | 58 ++++++++++++++++++++++++++++++++++++++++++ test/unit/util_spec.js | 10 ++++++++ 3 files changed, 69 insertions(+), 1 deletion(-) diff --git a/src/util/lang.js b/src/util/lang.js index 5f0101e7c12..af441cc12ec 100644 --- a/src/util/lang.js +++ b/src/util/lang.js @@ -7,7 +7,7 @@ exports.bind = function (fn, ctx) { return function () { - fn.apply(ctx, arguments) + return fn.apply(ctx, arguments) } } diff --git a/test/unit/data_spec.js b/test/unit/data_spec.js index c27b07687c7..965aa28676e 100644 --- a/test/unit/data_spec.js +++ b/test/unit/data_spec.js @@ -232,4 +232,62 @@ describe('Scope', function () { }) + describe('computed', function () { + + var vm = new Vue({ + data: { + a: 'a', + b: 'b' + }, + computed: { + c: function () { + expect(this).toBe(vm) + return this.a + this.b + }, + d: { + get: function () { + expect(this).toBe(vm) + return this.a + this.b + }, + set: function (newVal) { + expect(this).toBe(vm) + var vals = newVal.split(' ') + this.a = vals[0] + this.b = vals[1] + } + } + } + }) + + it('get', function () { + expect(vm.c).toBe('ab') + expect(vm.d).toBe('ab') + }) + + it('set', function () { + vm.c = 123 // should do nothing + vm.d = 'c d' + expect(vm.a).toBe('c') + expect(vm.b).toBe('d') + expect(vm.c).toBe('cd') + expect(vm.d).toBe('cd') + }) + + it('inherit', function () { + var child = new Vue({ parent: vm }) + expect(child.$scope.c).toBe('cd') + + child.$scope.d = 'e f' + expect(vm.a).toBe('e') + expect(vm.b).toBe('f') + expect(vm.c).toBe('ef') + expect(vm.d).toBe('ef') + expect(child.$scope.a).toBe('e') + expect(child.$scope.b).toBe('f') + expect(child.$scope.c).toBe('ef') + expect(child.$scope.d).toBe('ef') + }) + + }) + }) \ No newline at end of file diff --git a/test/unit/util_spec.js b/test/unit/util_spec.js index 2d69690ec08..47f8ab98d73 100644 --- a/test/unit/util_spec.js +++ b/test/unit/util_spec.js @@ -5,6 +5,16 @@ config.silent = true describe('Util', function () { describe('Language Enhancement', function () { + + it('bind', function () { + var original = function (a) { + return this.a + a + } + var ctx = { a: 'ctx a ' } + var bound = _.bind(original, ctx) + var res = bound('arg a') + expect(res).toBe('ctx a arg a') + }) it('toArray', function () { // should make a copy of original array From 32318d9b3d00df98f3a1b5a7edc26a036e3507be Mon Sep 17 00:00:00 2001 From: Evan You Date: Thu, 31 Jul 2014 22:19:13 -0400 Subject: [PATCH 0084/1534] template use innerHTML on normal nodes --- src/parse/template.js | 4 +++- test/unit/template.js | 6 +++--- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/src/parse/template.js b/src/parse/template.js index 81ea8033721..fb507df53e3 100644 --- a/src/parse/template.js +++ b/src/parse/template.js @@ -20,6 +20,8 @@ map.colgroup = map.caption = map.tfoot = [1, '', '
'] +map.g = +map.defs = map.text = map.circle = map.ellipse = @@ -96,7 +98,7 @@ function nodeToFragment (node) { } return tag === 'SCRIPT' ? stringToFragment(node.textContent) - : stringToFragment(node.outerHTML) + : stringToFragment(node.innerHTML) } /** diff --git a/test/unit/template.js b/test/unit/template.js index a4548d7c50d..d9aad4da633 100644 --- a/test/unit/template.js +++ b/test/unit/template.js @@ -35,13 +35,13 @@ describe('Template Parser', function () { expect(res.querySelector('.test').textContent).toBe('world') }) - it('should parse outerHTML if argument is a normal node', function () { + it('should parse innerHTML if argument is a normal node', function () { var node = document.createElement('div') node.innerHTML = testString var res = parse(node) expect(res instanceof DocumentFragment).toBeTruthy() - expect(res.childNodes.length).toBe(1) - expect(res.querySelector('div .test').textContent).toBe('world') + expect(res.childNodes.length).toBe(2) + expect(res.querySelector('.test').textContent).toBe('world') }) it('should retrieve and parse if argument is an id selector', function () { From 1c5f1d3e21020109fd666556fbb8c2ae0778c1bd Mon Sep 17 00:00:00 2001 From: Evan You Date: Thu, 31 Jul 2014 22:22:21 -0400 Subject: [PATCH 0085/1534] fix setting get-only property error in phantomjs --- src/instance/scope.js | 7 ++++++- test/unit/{data_spec.js => scope_spec.js} | 0 2 files changed, 6 insertions(+), 1 deletion(-) rename test/unit/{data_spec.js => scope_spec.js} (100%) diff --git a/src/instance/scope.js b/src/instance/scope.js index 9445cdbf258..6df6295e43b 100644 --- a/src/instance/scope.js +++ b/src/instance/scope.js @@ -116,13 +116,18 @@ exports._initData = function (data, init) { * Setup computed properties. */ +function noop () {} + exports._initComputed = function () { var computed = this.$options.computed if (computed) { for (var key in computed) { var def = computed[key] if (typeof def === 'function') { - def = { get: def } + def = { + get: def, + set: noop + } } def.enumerable = true def.configurable = true diff --git a/test/unit/data_spec.js b/test/unit/scope_spec.js similarity index 100% rename from test/unit/data_spec.js rename to test/unit/scope_spec.js From fbe0d05cf26f9a9147f746b983234c44180e3fca Mon Sep 17 00:00:00 2001 From: Evan You Date: Fri, 1 Aug 2014 08:17:30 -0400 Subject: [PATCH 0086/1534] scope refactor + test for methods --- src/instance/init.js | 6 +-- src/instance/scope.js | 87 +++++++++++++++++++++-------------------- test/unit/scope_spec.js | 20 ++++++++++ 3 files changed, 68 insertions(+), 45 deletions(-) diff --git a/src/instance/init.js b/src/instance/init.js index a6a26ec97dd..287b8c0c1a5 100644 --- a/src/instance/init.js +++ b/src/instance/init.js @@ -50,15 +50,15 @@ exports._init = function (options) { // setup initial data. this._initData(this._data, true) + // setup property proxying + this._initProxy() + // setup computed properties this._initComputed() // setup instance methods this._initMethods() - // setup property proxying - this._initProxy() - // setup binding tree. // @creates this._rootBinding this._initBindings() diff --git a/src/instance/scope.js b/src/instance/scope.js index 6df6295e43b..89c093f3317 100644 --- a/src/instance/scope.js +++ b/src/instance/scope.js @@ -78,14 +78,11 @@ exports._teardownScope = function () { exports._initData = function (data, init) { var scope = this.$scope - var options = this.$options var key if (!init) { // teardown old sync listeners - if (options.syncData) { - this._unsync() - } + this._teardownData() // delete keys not present in the new data for (key in scope) { if (scope.hasOwnProperty(key) && !(key in data)) { @@ -106,44 +103,22 @@ exports._initData = function (data, init) { } // setup sync between scope and new data - if (options.syncData) { + if (this.$options.syncData) { this._dataObserver = Observer.create(data) this._sync() } } /** - * Setup computed properties. + * Stop data-syncing. */ -function noop () {} - -exports._initComputed = function () { - var computed = this.$options.computed - if (computed) { - for (var key in computed) { - var def = computed[key] - if (typeof def === 'function') { - def = { - get: def, - set: noop - } - } - def.enumerable = true - def.configurable = true - Object.defineProperty(this, key, def) - } +exports._teardownData = function () { + if (this.$options.syncData) { + this._unsync() } } -/** - * Setup instance methods. - */ - -exports._initMethods = function () { - _.extend(this, this.$options.methods) -} - /** * Proxy the scope properties on the instance itself, * so that vm.a === vm.$scope.a. @@ -160,14 +135,13 @@ exports._initMethods = function () { */ exports._initProxy = function () { - var key var options = this.$options var scope = this.$scope // scope --> vm // proxy scope data on vm - for (key in scope) { + for (var key in scope) { if (scope.hasOwnProperty(key)) { _.proxy(this, scope, key) } @@ -186,22 +160,51 @@ exports._initProxy = function () { // proxy vm parent & root on scope _.proxy(scope, this, '$parent') _.proxy(scope, this, '$root') +} + +/** + * Setup computed properties. + * All computed properties are proxied onto the scope. + * Because they are accessors their `this` context will + * be the instance instead of the scope. + */ + +function noop () {} - // proxy computed properties on scope. - // since they are accessors, they are still bound to the vm. - var computed = options.computed +exports._initComputed = function () { + var computed = this.$options.computed + var scope = this.$scope if (computed) { - for (key in computed) { + for (var key in computed) { + var def = computed[key] + if (typeof def === 'function') { + def = { + get: def, + set: noop + } + } + def.enumerable = true + def.configurable = true + Object.defineProperty(this, key, def) _.proxy(scope, this, key) } } +} - // and methods need to be explicitly bound to the vm - // so it actually has all the API methods. - var methods = options.methods +/** + * Setup instance methods. + * Methods are also copied into scope, but they must + * be bound to the instance. + */ + +exports._initMethods = function () { + var methods = this.$options.methods + var scope = this.$scope if (methods) { - for (key in methods) { - scope[key] = _.bind(methods[key], this) + for (var key in methods) { + var method = methods[key] + this[key] = method + scope[key] = _.bind(method, this) } } } diff --git a/test/unit/scope_spec.js b/test/unit/scope_spec.js index 965aa28676e..bbe2b398ccd 100644 --- a/test/unit/scope_spec.js +++ b/test/unit/scope_spec.js @@ -290,4 +290,24 @@ describe('Scope', function () { }) + describe('methods', function () { + + it('should work and have correct context', function () { + var vm = new Vue({ + data: { + a: 1 + }, + methods: { + test: function () { + expect(this instanceof Vue).toBe(true) + return this.a + } + } + }) + expect(vm.test()).toBe(1) + expect(vm.$scope.test()).toBe(1) + }) + + }) + }) \ No newline at end of file From bdff636065e346491ee3daece3cfa806031de0dd Mon Sep 17 00:00:00 2001 From: Evan You Date: Fri, 1 Aug 2014 17:59:37 -0400 Subject: [PATCH 0087/1534] small fixes for Vue.extend & content insertion --- src/api/global.js | 1 + src/instance/element.js | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/src/api/global.js b/src/api/global.js index 3b7187843de..18025b5aa05 100644 --- a/src/api/global.js +++ b/src/api/global.js @@ -50,6 +50,7 @@ exports.extend = function (extendOptions) { // create asset registers, so extended classes // can have their private assets too. createAssetRegisters(Sub) + return Sub } /** diff --git a/src/instance/element.js b/src/instance/element.js index 337a2b75aa6..be94b1ebebd 100644 --- a/src/instance/element.js +++ b/src/instance/element.js @@ -80,10 +80,10 @@ exports._collectRawContent = function () { var el = this.$el var child if (el.hasChildNodes()) { - this.rawContent = document.createElement('div') + this._rawContent = document.createElement('div') /* jshint boss: true */ while (child = el.firstChild) { - this.rawContent.appendChild(child) + this._rawContent.appendChild(child) } } } From c482824c78dd69672dfd08b9e42b83b40117bfa9 Mon Sep 17 00:00:00 2001 From: Evan You Date: Fri, 1 Aug 2014 23:08:31 -0400 Subject: [PATCH 0088/1534] setTimeout is fine without delay --- src/util/env.js | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/src/util/env.js b/src/util/env.js index 32ed2caafac..289a4ef2113 100644 --- a/src/util/env.js +++ b/src/util/env.js @@ -18,16 +18,12 @@ var inBrowser = exports.inBrowser = * @param {Function} fn */ -var defer = inBrowser +exports.nextTick = inBrowser ? (window.requestAnimationFrame || window.webkitRequestAnimationFrame || setTimeout) : setTimeout -exports.nextTick = function (fn) { - return defer(fn, 0) -} - /** * Detect if we are in IE9... * From 2b1794c7afecb3cc306872332ded9acb846239f6 Mon Sep 17 00:00:00 2001 From: Evan You Date: Sat, 2 Aug 2014 00:18:16 -0400 Subject: [PATCH 0089/1534] add unit runner page, fix _.inBrowser in IE9 --- .gitignore | 2 +- gruntfile.js | 10 +- src/util/env.js | 4 +- test/unit/lib/MIT.LICENSE | 20 + test/unit/lib/jasmine-html.js | 681 +++++ test/unit/lib/jasmine.css | 82 + test/unit/lib/jasmine.js | 2600 +++++++++++++++++ test/unit/runner.html | 28 + test/unit/{ => specs}/binding_spec.js | 0 test/unit/{ => specs}/cache_spec.js | 2 +- .../unit/{ => specs}/directive_parser_spec.js | 2 +- .../{ => specs}/expression_parser_spec.js | 2 +- test/unit/{ => specs}/observer_spec.js | 2 +- test/unit/{ => specs}/path_parser_spec.js | 2 +- test/unit/{ => specs}/scope_spec.js | 4 +- test/unit/{ => specs}/template.js | 2 +- test/unit/{ => specs}/util_spec.js | 4 +- 17 files changed, 3431 insertions(+), 16 deletions(-) create mode 100644 test/unit/lib/MIT.LICENSE create mode 100644 test/unit/lib/jasmine-html.js create mode 100644 test/unit/lib/jasmine.css create mode 100644 test/unit/lib/jasmine.js create mode 100644 test/unit/runner.html rename test/unit/{ => specs}/binding_spec.js (100%) rename test/unit/{ => specs}/cache_spec.js (96%) rename test/unit/{ => specs}/directive_parser_spec.js (98%) rename test/unit/{ => specs}/expression_parser_spec.js (97%) rename test/unit/{ => specs}/observer_spec.js (99%) rename test/unit/{ => specs}/path_parser_spec.js (98%) rename test/unit/{ => specs}/scope_spec.js (98%) rename test/unit/{ => specs}/template.js (98%) rename test/unit/{ => specs}/util_spec.js (99%) diff --git a/.gitignore b/.gitignore index eb4535c1720..70310172ea6 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,5 @@ dist/vue.min.js.gz -test/vue.test.js +test/unit/specs.js explorations node_modules .DS_Store diff --git a/gruntfile.js b/gruntfile.js index 0d64df4d1be..6dcfd962f0e 100644 --- a/gruntfile.js +++ b/gruntfile.js @@ -31,11 +31,11 @@ module.exports = function (grunt) { frameworks: ['jasmine', 'commonjs'], files: [ 'src/**/*.js', - 'test/unit/**/*.js' + 'test/unit/specs/*.js' ], preprocessors: { 'src/**/*.js': ['commonjs'], - 'test/unit/**/*.js': ['commonjs'] + 'test/unit/specs/*.js': ['commonjs'] }, singleRun: true }, @@ -80,6 +80,10 @@ module.exports = function (grunt) { bench: { src: ['benchmarks/bench.js'], dest: 'benchmarks/browser.js' + }, + test: { + src: ['test/unit/specs/*.js'], + dest: 'test/unit/specs.js' } }, @@ -118,6 +122,6 @@ module.exports = function (grunt) { grunt.registerTask('phantom', ['karma:phantom']) grunt.registerTask('bench', ['browserify:bench']) grunt.registerTask('watch', ['browserify:watch']) - grunt.registerTask('build', ['browserify:build', 'uglify:build']) + grunt.registerTask('build', ['browserify:test', 'browserify:build', 'uglify:build']) } \ No newline at end of file diff --git a/src/util/env.js b/src/util/env.js index 289a4ef2113..e8413738136 100644 --- a/src/util/env.js +++ b/src/util/env.js @@ -9,8 +9,8 @@ */ var inBrowser = exports.inBrowser = - typeof document !== 'undefined' && - Object.prototype.toString.call(document) === '[object HTMLDocument]' + typeof window !== 'undefined' && + Object.prototype.toString.call(window) !== '[object Object]' /** * Defer a task to the start of the next event loop diff --git a/test/unit/lib/MIT.LICENSE b/test/unit/lib/MIT.LICENSE new file mode 100644 index 00000000000..7c435baaec8 --- /dev/null +++ b/test/unit/lib/MIT.LICENSE @@ -0,0 +1,20 @@ +Copyright (c) 2008-2011 Pivotal Labs + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/test/unit/lib/jasmine-html.js b/test/unit/lib/jasmine-html.js new file mode 100644 index 00000000000..543d56963eb --- /dev/null +++ b/test/unit/lib/jasmine-html.js @@ -0,0 +1,681 @@ +jasmine.HtmlReporterHelpers = {}; + +jasmine.HtmlReporterHelpers.createDom = function(type, attrs, childrenVarArgs) { + var el = document.createElement(type); + + for (var i = 2; i < arguments.length; i++) { + var child = arguments[i]; + + if (typeof child === 'string') { + el.appendChild(document.createTextNode(child)); + } else { + if (child) { + el.appendChild(child); + } + } + } + + for (var attr in attrs) { + if (attr == "className") { + el[attr] = attrs[attr]; + } else { + el.setAttribute(attr, attrs[attr]); + } + } + + return el; +}; + +jasmine.HtmlReporterHelpers.getSpecStatus = function(child) { + var results = child.results(); + var status = results.passed() ? 'passed' : 'failed'; + if (results.skipped) { + status = 'skipped'; + } + + return status; +}; + +jasmine.HtmlReporterHelpers.appendToSummary = function(child, childElement) { + var parentDiv = this.dom.summary; + var parentSuite = (typeof child.parentSuite == 'undefined') ? 'suite' : 'parentSuite'; + var parent = child[parentSuite]; + + if (parent) { + if (typeof this.views.suites[parent.id] == 'undefined') { + this.views.suites[parent.id] = new jasmine.HtmlReporter.SuiteView(parent, this.dom, this.views); + } + parentDiv = this.views.suites[parent.id].element; + } + + parentDiv.appendChild(childElement); +}; + + +jasmine.HtmlReporterHelpers.addHelpers = function(ctor) { + for(var fn in jasmine.HtmlReporterHelpers) { + ctor.prototype[fn] = jasmine.HtmlReporterHelpers[fn]; + } +}; + +jasmine.HtmlReporter = function(_doc) { + var self = this; + var doc = _doc || window.document; + + var reporterView; + + var dom = {}; + + // Jasmine Reporter Public Interface + self.logRunningSpecs = false; + + self.reportRunnerStarting = function(runner) { + var specs = runner.specs() || []; + + if (specs.length == 0) { + return; + } + + createReporterDom(runner.env.versionString()); + doc.body.appendChild(dom.reporter); + setExceptionHandling(); + + reporterView = new jasmine.HtmlReporter.ReporterView(dom); + reporterView.addSpecs(specs, self.specFilter); + }; + + self.reportRunnerResults = function(runner) { + reporterView && reporterView.complete(); + }; + + self.reportSuiteResults = function(suite) { + reporterView.suiteComplete(suite); + }; + + self.reportSpecStarting = function(spec) { + if (self.logRunningSpecs) { + self.log('>> Jasmine Running ' + spec.suite.description + ' ' + spec.description + '...'); + } + }; + + self.reportSpecResults = function(spec) { + reporterView.specComplete(spec); + }; + + self.log = function() { + var console = jasmine.getGlobal().console; + if (console && console.log) { + if (console.log.apply) { + console.log.apply(console, arguments); + } else { + console.log(arguments); // ie fix: console.log.apply doesn't exist on ie + } + } + }; + + self.specFilter = function(spec) { + if (!focusedSpecName()) { + return true; + } + + return spec.getFullName().indexOf(focusedSpecName()) === 0; + }; + + return self; + + function focusedSpecName() { + var specName; + + (function memoizeFocusedSpec() { + if (specName) { + return; + } + + var paramMap = []; + var params = jasmine.HtmlReporter.parameters(doc); + + for (var i = 0; i < params.length; i++) { + var p = params[i].split('='); + paramMap[decodeURIComponent(p[0])] = decodeURIComponent(p[1]); + } + + specName = paramMap.spec; + })(); + + return specName; + } + + function createReporterDom(version) { + dom.reporter = self.createDom('div', { id: 'HTMLReporter', className: 'jasmine_reporter' }, + dom.banner = self.createDom('div', { className: 'banner' }, + self.createDom('span', { className: 'title' }, "Jasmine "), + self.createDom('span', { className: 'version' }, version)), + + dom.symbolSummary = self.createDom('ul', {className: 'symbolSummary'}), + dom.alert = self.createDom('div', {className: 'alert'}, + self.createDom('span', { className: 'exceptions' }, + self.createDom('label', { className: 'label', 'for': 'no_try_catch' }, 'No try/catch'), + self.createDom('input', { id: 'no_try_catch', type: 'checkbox' }))), + dom.results = self.createDom('div', {className: 'results'}, + dom.summary = self.createDom('div', { className: 'summary' }), + dom.details = self.createDom('div', { id: 'details' })) + ); + } + + function noTryCatch() { + return window.location.search.match(/catch=false/); + } + + function searchWithCatch() { + var params = jasmine.HtmlReporter.parameters(window.document); + var removed = false; + var i = 0; + + while (!removed && i < params.length) { + if (params[i].match(/catch=/)) { + params.splice(i, 1); + removed = true; + } + i++; + } + if (jasmine.CATCH_EXCEPTIONS) { + params.push("catch=false"); + } + + return params.join("&"); + } + + function setExceptionHandling() { + var chxCatch = document.getElementById('no_try_catch'); + + if (noTryCatch()) { + chxCatch.setAttribute('checked', true); + jasmine.CATCH_EXCEPTIONS = false; + } + chxCatch.onclick = function() { + window.location.search = searchWithCatch(); + }; + } +}; +jasmine.HtmlReporter.parameters = function(doc) { + var paramStr = doc.location.search.substring(1); + var params = []; + + if (paramStr.length > 0) { + params = paramStr.split('&'); + } + return params; +} +jasmine.HtmlReporter.sectionLink = function(sectionName) { + var link = '?'; + var params = []; + + if (sectionName) { + params.push('spec=' + encodeURIComponent(sectionName)); + } + if (!jasmine.CATCH_EXCEPTIONS) { + params.push("catch=false"); + } + if (params.length > 0) { + link += params.join("&"); + } + + return link; +}; +jasmine.HtmlReporterHelpers.addHelpers(jasmine.HtmlReporter); +jasmine.HtmlReporter.ReporterView = function(dom) { + this.startedAt = new Date(); + this.runningSpecCount = 0; + this.completeSpecCount = 0; + this.passedCount = 0; + this.failedCount = 0; + this.skippedCount = 0; + + this.createResultsMenu = function() { + this.resultsMenu = this.createDom('span', {className: 'resultsMenu bar'}, + this.summaryMenuItem = this.createDom('a', {className: 'summaryMenuItem', href: "#"}, '0 specs'), + ' | ', + this.detailsMenuItem = this.createDom('a', {className: 'detailsMenuItem', href: "#"}, '0 failing')); + + this.summaryMenuItem.onclick = function() { + dom.reporter.className = dom.reporter.className.replace(/ showDetails/g, ''); + }; + + this.detailsMenuItem.onclick = function() { + showDetails(); + }; + }; + + this.addSpecs = function(specs, specFilter) { + this.totalSpecCount = specs.length; + + this.views = { + specs: {}, + suites: {} + }; + + for (var i = 0; i < specs.length; i++) { + var spec = specs[i]; + this.views.specs[spec.id] = new jasmine.HtmlReporter.SpecView(spec, dom, this.views); + if (specFilter(spec)) { + this.runningSpecCount++; + } + } + }; + + this.specComplete = function(spec) { + this.completeSpecCount++; + + if (isUndefined(this.views.specs[spec.id])) { + this.views.specs[spec.id] = new jasmine.HtmlReporter.SpecView(spec, dom); + } + + var specView = this.views.specs[spec.id]; + + switch (specView.status()) { + case 'passed': + this.passedCount++; + break; + + case 'failed': + this.failedCount++; + break; + + case 'skipped': + this.skippedCount++; + break; + } + + specView.refresh(); + this.refresh(); + }; + + this.suiteComplete = function(suite) { + var suiteView = this.views.suites[suite.id]; + if (isUndefined(suiteView)) { + return; + } + suiteView.refresh(); + }; + + this.refresh = function() { + + if (isUndefined(this.resultsMenu)) { + this.createResultsMenu(); + } + + // currently running UI + if (isUndefined(this.runningAlert)) { + this.runningAlert = this.createDom('a', { href: jasmine.HtmlReporter.sectionLink(), className: "runningAlert bar" }); + dom.alert.appendChild(this.runningAlert); + } + this.runningAlert.innerHTML = "Running " + this.completeSpecCount + " of " + specPluralizedFor(this.totalSpecCount); + + // skipped specs UI + if (isUndefined(this.skippedAlert)) { + this.skippedAlert = this.createDom('a', { href: jasmine.HtmlReporter.sectionLink(), className: "skippedAlert bar" }); + } + + this.skippedAlert.innerHTML = "Skipping " + this.skippedCount + " of " + specPluralizedFor(this.totalSpecCount) + " - run all"; + + if (this.skippedCount === 1 && isDefined(dom.alert)) { + dom.alert.appendChild(this.skippedAlert); + } + + // passing specs UI + if (isUndefined(this.passedAlert)) { + this.passedAlert = this.createDom('span', { href: jasmine.HtmlReporter.sectionLink(), className: "passingAlert bar" }); + } + this.passedAlert.innerHTML = "Passing " + specPluralizedFor(this.passedCount); + + // failing specs UI + if (isUndefined(this.failedAlert)) { + this.failedAlert = this.createDom('span', {href: "?", className: "failingAlert bar"}); + } + this.failedAlert.innerHTML = "Failing " + specPluralizedFor(this.failedCount); + + if (this.failedCount === 1 && isDefined(dom.alert)) { + dom.alert.appendChild(this.failedAlert); + dom.alert.appendChild(this.resultsMenu); + } + + // summary info + this.summaryMenuItem.innerHTML = "" + specPluralizedFor(this.runningSpecCount); + this.detailsMenuItem.innerHTML = "" + this.failedCount + " failing"; + }; + + this.complete = function() { + dom.alert.removeChild(this.runningAlert); + + this.skippedAlert.innerHTML = "Ran " + this.runningSpecCount + " of " + specPluralizedFor(this.totalSpecCount) + " - run all"; + + if (this.failedCount === 0) { + dom.alert.appendChild(this.createDom('span', {className: 'passingAlert bar'}, "Passing " + specPluralizedFor(this.passedCount))); + } else { + showDetails(); + } + + dom.banner.appendChild(this.createDom('span', {className: 'duration'}, "finished in " + ((new Date().getTime() - this.startedAt.getTime()) / 1000) + "s")); + }; + + return this; + + function showDetails() { + if (dom.reporter.className.search(/showDetails/) === -1) { + dom.reporter.className += " showDetails"; + } + } + + function isUndefined(obj) { + return typeof obj === 'undefined'; + } + + function isDefined(obj) { + return !isUndefined(obj); + } + + function specPluralizedFor(count) { + var str = count + " spec"; + if (count > 1) { + str += "s" + } + return str; + } + +}; + +jasmine.HtmlReporterHelpers.addHelpers(jasmine.HtmlReporter.ReporterView); + + +jasmine.HtmlReporter.SpecView = function(spec, dom, views) { + this.spec = spec; + this.dom = dom; + this.views = views; + + this.symbol = this.createDom('li', { className: 'pending' }); + this.dom.symbolSummary.appendChild(this.symbol); + + this.summary = this.createDom('div', { className: 'specSummary' }, + this.createDom('a', { + className: 'description', + href: jasmine.HtmlReporter.sectionLink(this.spec.getFullName()), + title: this.spec.getFullName() + }, this.spec.description) + ); + + this.detail = this.createDom('div', { className: 'specDetail' }, + this.createDom('a', { + className: 'description', + href: '?spec=' + encodeURIComponent(this.spec.getFullName()), + title: this.spec.getFullName() + }, this.spec.getFullName()) + ); +}; + +jasmine.HtmlReporter.SpecView.prototype.status = function() { + return this.getSpecStatus(this.spec); +}; + +jasmine.HtmlReporter.SpecView.prototype.refresh = function() { + this.symbol.className = this.status(); + + switch (this.status()) { + case 'skipped': + break; + + case 'passed': + this.appendSummaryToSuiteDiv(); + break; + + case 'failed': + this.appendSummaryToSuiteDiv(); + this.appendFailureDetail(); + break; + } +}; + +jasmine.HtmlReporter.SpecView.prototype.appendSummaryToSuiteDiv = function() { + this.summary.className += ' ' + this.status(); + this.appendToSummary(this.spec, this.summary); +}; + +jasmine.HtmlReporter.SpecView.prototype.appendFailureDetail = function() { + this.detail.className += ' ' + this.status(); + + var resultItems = this.spec.results().getItems(); + var messagesDiv = this.createDom('div', { className: 'messages' }); + + for (var i = 0; i < resultItems.length; i++) { + var result = resultItems[i]; + + if (result.type == 'log') { + messagesDiv.appendChild(this.createDom('div', {className: 'resultMessage log'}, result.toString())); + } else if (result.type == 'expect' && result.passed && !result.passed()) { + messagesDiv.appendChild(this.createDom('div', {className: 'resultMessage fail'}, result.message)); + + if (result.trace.stack) { + messagesDiv.appendChild(this.createDom('div', {className: 'stackTrace'}, result.trace.stack)); + } + } + } + + if (messagesDiv.childNodes.length > 0) { + this.detail.appendChild(messagesDiv); + this.dom.details.appendChild(this.detail); + } +}; + +jasmine.HtmlReporterHelpers.addHelpers(jasmine.HtmlReporter.SpecView);jasmine.HtmlReporter.SuiteView = function(suite, dom, views) { + this.suite = suite; + this.dom = dom; + this.views = views; + + this.element = this.createDom('div', { className: 'suite' }, + this.createDom('a', { className: 'description', href: jasmine.HtmlReporter.sectionLink(this.suite.getFullName()) }, this.suite.description) + ); + + this.appendToSummary(this.suite, this.element); +}; + +jasmine.HtmlReporter.SuiteView.prototype.status = function() { + return this.getSpecStatus(this.suite); +}; + +jasmine.HtmlReporter.SuiteView.prototype.refresh = function() { + this.element.className += " " + this.status(); +}; + +jasmine.HtmlReporterHelpers.addHelpers(jasmine.HtmlReporter.SuiteView); + +/* @deprecated Use jasmine.HtmlReporter instead + */ +jasmine.TrivialReporter = function(doc) { + this.document = doc || document; + this.suiteDivs = {}; + this.logRunningSpecs = false; +}; + +jasmine.TrivialReporter.prototype.createDom = function(type, attrs, childrenVarArgs) { + var el = document.createElement(type); + + for (var i = 2; i < arguments.length; i++) { + var child = arguments[i]; + + if (typeof child === 'string') { + el.appendChild(document.createTextNode(child)); + } else { + if (child) { el.appendChild(child); } + } + } + + for (var attr in attrs) { + if (attr == "className") { + el[attr] = attrs[attr]; + } else { + el.setAttribute(attr, attrs[attr]); + } + } + + return el; +}; + +jasmine.TrivialReporter.prototype.reportRunnerStarting = function(runner) { + var showPassed, showSkipped; + + this.outerDiv = this.createDom('div', { id: 'TrivialReporter', className: 'jasmine_reporter' }, + this.createDom('div', { className: 'banner' }, + this.createDom('div', { className: 'logo' }, + this.createDom('span', { className: 'title' }, "Jasmine"), + this.createDom('span', { className: 'version' }, runner.env.versionString())), + this.createDom('div', { className: 'options' }, + "Show ", + showPassed = this.createDom('input', { id: "__jasmine_TrivialReporter_showPassed__", type: 'checkbox' }), + this.createDom('label', { "for": "__jasmine_TrivialReporter_showPassed__" }, " passed "), + showSkipped = this.createDom('input', { id: "__jasmine_TrivialReporter_showSkipped__", type: 'checkbox' }), + this.createDom('label', { "for": "__jasmine_TrivialReporter_showSkipped__" }, " skipped") + ) + ), + + this.runnerDiv = this.createDom('div', { className: 'runner running' }, + this.createDom('a', { className: 'run_spec', href: '?' }, "run all"), + this.runnerMessageSpan = this.createDom('span', {}, "Running..."), + this.finishedAtSpan = this.createDom('span', { className: 'finished-at' }, "")) + ); + + this.document.body.appendChild(this.outerDiv); + + var suites = runner.suites(); + for (var i = 0; i < suites.length; i++) { + var suite = suites[i]; + var suiteDiv = this.createDom('div', { className: 'suite' }, + this.createDom('a', { className: 'run_spec', href: '?spec=' + encodeURIComponent(suite.getFullName()) }, "run"), + this.createDom('a', { className: 'description', href: '?spec=' + encodeURIComponent(suite.getFullName()) }, suite.description)); + this.suiteDivs[suite.id] = suiteDiv; + var parentDiv = this.outerDiv; + if (suite.parentSuite) { + parentDiv = this.suiteDivs[suite.parentSuite.id]; + } + parentDiv.appendChild(suiteDiv); + } + + this.startedAt = new Date(); + + var self = this; + showPassed.onclick = function(evt) { + if (showPassed.checked) { + self.outerDiv.className += ' show-passed'; + } else { + self.outerDiv.className = self.outerDiv.className.replace(/ show-passed/, ''); + } + }; + + showSkipped.onclick = function(evt) { + if (showSkipped.checked) { + self.outerDiv.className += ' show-skipped'; + } else { + self.outerDiv.className = self.outerDiv.className.replace(/ show-skipped/, ''); + } + }; +}; + +jasmine.TrivialReporter.prototype.reportRunnerResults = function(runner) { + var results = runner.results(); + var className = (results.failedCount > 0) ? "runner failed" : "runner passed"; + this.runnerDiv.setAttribute("class", className); + //do it twice for IE + this.runnerDiv.setAttribute("className", className); + var specs = runner.specs(); + var specCount = 0; + for (var i = 0; i < specs.length; i++) { + if (this.specFilter(specs[i])) { + specCount++; + } + } + var message = "" + specCount + " spec" + (specCount == 1 ? "" : "s" ) + ", " + results.failedCount + " failure" + ((results.failedCount == 1) ? "" : "s"); + message += " in " + ((new Date().getTime() - this.startedAt.getTime()) / 1000) + "s"; + this.runnerMessageSpan.replaceChild(this.createDom('a', { className: 'description', href: '?'}, message), this.runnerMessageSpan.firstChild); + + this.finishedAtSpan.appendChild(document.createTextNode("Finished at " + new Date().toString())); +}; + +jasmine.TrivialReporter.prototype.reportSuiteResults = function(suite) { + var results = suite.results(); + var status = results.passed() ? 'passed' : 'failed'; + if (results.totalCount === 0) { // todo: change this to check results.skipped + status = 'skipped'; + } + this.suiteDivs[suite.id].className += " " + status; +}; + +jasmine.TrivialReporter.prototype.reportSpecStarting = function(spec) { + if (this.logRunningSpecs) { + this.log('>> Jasmine Running ' + spec.suite.description + ' ' + spec.description + '...'); + } +}; + +jasmine.TrivialReporter.prototype.reportSpecResults = function(spec) { + var results = spec.results(); + var status = results.passed() ? 'passed' : 'failed'; + if (results.skipped) { + status = 'skipped'; + } + var specDiv = this.createDom('div', { className: 'spec ' + status }, + this.createDom('a', { className: 'run_spec', href: '?spec=' + encodeURIComponent(spec.getFullName()) }, "run"), + this.createDom('a', { + className: 'description', + href: '?spec=' + encodeURIComponent(spec.getFullName()), + title: spec.getFullName() + }, spec.description)); + + + var resultItems = results.getItems(); + var messagesDiv = this.createDom('div', { className: 'messages' }); + for (var i = 0; i < resultItems.length; i++) { + var result = resultItems[i]; + + if (result.type == 'log') { + messagesDiv.appendChild(this.createDom('div', {className: 'resultMessage log'}, result.toString())); + } else if (result.type == 'expect' && result.passed && !result.passed()) { + messagesDiv.appendChild(this.createDom('div', {className: 'resultMessage fail'}, result.message)); + + if (result.trace.stack) { + messagesDiv.appendChild(this.createDom('div', {className: 'stackTrace'}, result.trace.stack)); + } + } + } + + if (messagesDiv.childNodes.length > 0) { + specDiv.appendChild(messagesDiv); + } + + this.suiteDivs[spec.suite.id].appendChild(specDiv); +}; + +jasmine.TrivialReporter.prototype.log = function() { + var console = jasmine.getGlobal().console; + if (console && console.log) { + if (console.log.apply) { + console.log.apply(console, arguments); + } else { + console.log(arguments); // ie fix: console.log.apply doesn't exist on ie + } + } +}; + +jasmine.TrivialReporter.prototype.getLocation = function() { + return this.document.location; +}; + +jasmine.TrivialReporter.prototype.specFilter = function(spec) { + var paramMap = {}; + var params = this.getLocation().search.substring(1).split('&'); + for (var i = 0; i < params.length; i++) { + var p = params[i].split('='); + paramMap[decodeURIComponent(p[0])] = decodeURIComponent(p[1]); + } + + if (!paramMap.spec) { + return true; + } + return spec.getFullName().indexOf(paramMap.spec) === 0; +}; diff --git a/test/unit/lib/jasmine.css b/test/unit/lib/jasmine.css new file mode 100644 index 00000000000..8c008dc7221 --- /dev/null +++ b/test/unit/lib/jasmine.css @@ -0,0 +1,82 @@ +body { background-color: #eeeeee; padding: 0; margin: 5px; overflow-y: scroll; } + +#HTMLReporter { font-size: 11px; font-family: Monaco, "Lucida Console", monospace; line-height: 14px; color: #333333; } +#HTMLReporter a { text-decoration: none; } +#HTMLReporter a:hover { text-decoration: underline; } +#HTMLReporter p, #HTMLReporter h1, #HTMLReporter h2, #HTMLReporter h3, #HTMLReporter h4, #HTMLReporter h5, #HTMLReporter h6 { margin: 0; line-height: 14px; } +#HTMLReporter .banner, #HTMLReporter .symbolSummary, #HTMLReporter .summary, #HTMLReporter .resultMessage, #HTMLReporter .specDetail .description, #HTMLReporter .alert .bar, #HTMLReporter .stackTrace { padding-left: 9px; padding-right: 9px; } +#HTMLReporter #jasmine_content { position: fixed; right: 100%; } +#HTMLReporter .version { color: #aaaaaa; } +#HTMLReporter .banner { margin-top: 14px; } +#HTMLReporter .duration { color: #aaaaaa; float: right; } +#HTMLReporter .symbolSummary { overflow: hidden; *zoom: 1; margin: 14px 0; } +#HTMLReporter .symbolSummary li { display: block; float: left; height: 7px; width: 14px; margin-bottom: 7px; font-size: 16px; } +#HTMLReporter .symbolSummary li.passed { font-size: 14px; } +#HTMLReporter .symbolSummary li.passed:before { color: #5e7d00; content: "\02022"; } +#HTMLReporter .symbolSummary li.failed { line-height: 9px; } +#HTMLReporter .symbolSummary li.failed:before { color: #b03911; content: "x"; font-weight: bold; margin-left: -1px; } +#HTMLReporter .symbolSummary li.skipped { font-size: 14px; } +#HTMLReporter .symbolSummary li.skipped:before { color: #bababa; content: "\02022"; } +#HTMLReporter .symbolSummary li.pending { line-height: 11px; } +#HTMLReporter .symbolSummary li.pending:before { color: #aaaaaa; content: "-"; } +#HTMLReporter .exceptions { color: #fff; float: right; margin-top: 5px; margin-right: 5px; } +#HTMLReporter .bar { line-height: 28px; font-size: 14px; display: block; color: #eee; } +#HTMLReporter .runningAlert { background-color: #666666; } +#HTMLReporter .skippedAlert { background-color: #aaaaaa; } +#HTMLReporter .skippedAlert:first-child { background-color: #333333; } +#HTMLReporter .skippedAlert:hover { text-decoration: none; color: white; text-decoration: underline; } +#HTMLReporter .passingAlert { background-color: #a6b779; } +#HTMLReporter .passingAlert:first-child { background-color: #5e7d00; } +#HTMLReporter .failingAlert { background-color: #cf867e; } +#HTMLReporter .failingAlert:first-child { background-color: #b03911; } +#HTMLReporter .results { margin-top: 14px; } +#HTMLReporter #details { display: none; } +#HTMLReporter .resultsMenu, #HTMLReporter .resultsMenu a { background-color: #fff; color: #333333; } +#HTMLReporter.showDetails .summaryMenuItem { font-weight: normal; text-decoration: inherit; } +#HTMLReporter.showDetails .summaryMenuItem:hover { text-decoration: underline; } +#HTMLReporter.showDetails .detailsMenuItem { font-weight: bold; text-decoration: underline; } +#HTMLReporter.showDetails .summary { display: none; } +#HTMLReporter.showDetails #details { display: block; } +#HTMLReporter .summaryMenuItem { font-weight: bold; text-decoration: underline; } +#HTMLReporter .summary { margin-top: 14px; } +#HTMLReporter .summary .suite .suite, #HTMLReporter .summary .specSummary { margin-left: 14px; } +#HTMLReporter .summary .specSummary.passed a { color: #5e7d00; } +#HTMLReporter .summary .specSummary.failed a { color: #b03911; } +#HTMLReporter .description + .suite { margin-top: 0; } +#HTMLReporter .suite { margin-top: 14px; } +#HTMLReporter .suite a { color: #333333; } +#HTMLReporter #details .specDetail { margin-bottom: 28px; } +#HTMLReporter #details .specDetail .description { display: block; color: white; background-color: #b03911; } +#HTMLReporter .resultMessage { padding-top: 14px; color: #333333; } +#HTMLReporter .resultMessage span.result { display: block; } +#HTMLReporter .stackTrace { margin: 5px 0 0 0; max-height: 224px; overflow: auto; line-height: 18px; color: #666666; border: 1px solid #ddd; background: white; white-space: pre; } + +#TrivialReporter { padding: 8px 13px; position: absolute; top: 0; bottom: 0; left: 0; right: 0; overflow-y: scroll; background-color: white; font-family: "Helvetica Neue Light", "Lucida Grande", "Calibri", "Arial", sans-serif; /*.resultMessage {*/ /*white-space: pre;*/ /*}*/ } +#TrivialReporter a:visited, #TrivialReporter a { color: #303; } +#TrivialReporter a:hover, #TrivialReporter a:active { color: blue; } +#TrivialReporter .run_spec { float: right; padding-right: 5px; font-size: .8em; text-decoration: none; } +#TrivialReporter .banner { color: #303; background-color: #fef; padding: 5px; } +#TrivialReporter .logo { float: left; font-size: 1.1em; padding-left: 5px; } +#TrivialReporter .logo .version { font-size: .6em; padding-left: 1em; } +#TrivialReporter .runner.running { background-color: yellow; } +#TrivialReporter .options { text-align: right; font-size: .8em; } +#TrivialReporter .suite { border: 1px outset gray; margin: 5px 0; padding-left: 1em; } +#TrivialReporter .suite .suite { margin: 5px; } +#TrivialReporter .suite.passed { background-color: #dfd; } +#TrivialReporter .suite.failed { background-color: #fdd; } +#TrivialReporter .spec { margin: 5px; padding-left: 1em; clear: both; } +#TrivialReporter .spec.failed, #TrivialReporter .spec.passed, #TrivialReporter .spec.skipped { padding-bottom: 5px; border: 1px solid gray; } +#TrivialReporter .spec.failed { background-color: #fbb; border-color: red; } +#TrivialReporter .spec.passed { background-color: #bfb; border-color: green; } +#TrivialReporter .spec.skipped { background-color: #bbb; } +#TrivialReporter .messages { border-left: 1px dashed gray; padding-left: 1em; padding-right: 1em; } +#TrivialReporter .passed { background-color: #cfc; display: none; } +#TrivialReporter .failed { background-color: #fbb; } +#TrivialReporter .skipped { color: #777; background-color: #eee; display: none; } +#TrivialReporter .resultMessage span.result { display: block; line-height: 2em; color: black; } +#TrivialReporter .resultMessage .mismatch { color: black; } +#TrivialReporter .stackTrace { white-space: pre; font-size: .8em; margin-left: 10px; max-height: 5em; overflow: auto; border: 1px inset red; padding: 1em; background: #eef; } +#TrivialReporter .finished-at { padding-left: 1em; font-size: .6em; } +#TrivialReporter.show-passed .passed, #TrivialReporter.show-skipped .skipped { display: block; } +#TrivialReporter #jasmine_content { position: fixed; right: 100%; } +#TrivialReporter .runner { border: 1px solid gray; display: block; margin: 5px 0; padding: 2px 0 2px 10px; } diff --git a/test/unit/lib/jasmine.js b/test/unit/lib/jasmine.js new file mode 100644 index 00000000000..6b3459b913f --- /dev/null +++ b/test/unit/lib/jasmine.js @@ -0,0 +1,2600 @@ +var isCommonJS = typeof window == "undefined" && typeof exports == "object"; + +/** + * Top level namespace for Jasmine, a lightweight JavaScript BDD/spec/testing framework. + * + * @namespace + */ +var jasmine = {}; +if (isCommonJS) exports.jasmine = jasmine; +/** + * @private + */ +jasmine.unimplementedMethod_ = function() { + throw new Error("unimplemented method"); +}; + +/** + * Use jasmine.undefined instead of undefined, since undefined is just + * a plain old variable and may be redefined by somebody else. + * + * @private + */ +jasmine.undefined = jasmine.___undefined___; + +/** + * Show diagnostic messages in the console if set to true + * + */ +jasmine.VERBOSE = false; + +/** + * Default interval in milliseconds for event loop yields (e.g. to allow network activity or to refresh the screen with the HTML-based runner). Small values here may result in slow test running. Zero means no updates until all tests have completed. + * + */ +jasmine.DEFAULT_UPDATE_INTERVAL = 250; + +/** + * Maximum levels of nesting that will be included when an object is pretty-printed + */ +jasmine.MAX_PRETTY_PRINT_DEPTH = 40; + +/** + * Default timeout interval in milliseconds for waitsFor() blocks. + */ +jasmine.DEFAULT_TIMEOUT_INTERVAL = 5000; + +/** + * By default exceptions thrown in the context of a test are caught by jasmine so that it can run the remaining tests in the suite. + * Set to false to let the exception bubble up in the browser. + * + */ +jasmine.CATCH_EXCEPTIONS = true; + +jasmine.getGlobal = function() { + function getGlobal() { + return this; + } + + return getGlobal(); +}; + +/** + * Allows for bound functions to be compared. Internal use only. + * + * @ignore + * @private + * @param base {Object} bound 'this' for the function + * @param name {Function} function to find + */ +jasmine.bindOriginal_ = function(base, name) { + var original = base[name]; + if (original.apply) { + return function() { + return original.apply(base, arguments); + }; + } else { + // IE support + return jasmine.getGlobal()[name]; + } +}; + +jasmine.setTimeout = jasmine.bindOriginal_(jasmine.getGlobal(), 'setTimeout'); +jasmine.clearTimeout = jasmine.bindOriginal_(jasmine.getGlobal(), 'clearTimeout'); +jasmine.setInterval = jasmine.bindOriginal_(jasmine.getGlobal(), 'setInterval'); +jasmine.clearInterval = jasmine.bindOriginal_(jasmine.getGlobal(), 'clearInterval'); + +jasmine.MessageResult = function(values) { + this.type = 'log'; + this.values = values; + this.trace = new Error(); // todo: test better +}; + +jasmine.MessageResult.prototype.toString = function() { + var text = ""; + for (var i = 0; i < this.values.length; i++) { + if (i > 0) text += " "; + if (jasmine.isString_(this.values[i])) { + text += this.values[i]; + } else { + text += jasmine.pp(this.values[i]); + } + } + return text; +}; + +jasmine.ExpectationResult = function(params) { + this.type = 'expect'; + this.matcherName = params.matcherName; + this.passed_ = params.passed; + this.expected = params.expected; + this.actual = params.actual; + this.message = this.passed_ ? 'Passed.' : params.message; + + var trace = (params.trace || new Error(this.message)); + this.trace = this.passed_ ? '' : trace; +}; + +jasmine.ExpectationResult.prototype.toString = function () { + return this.message; +}; + +jasmine.ExpectationResult.prototype.passed = function () { + return this.passed_; +}; + +/** + * Getter for the Jasmine environment. Ensures one gets created + */ +jasmine.getEnv = function() { + var env = jasmine.currentEnv_ = jasmine.currentEnv_ || new jasmine.Env(); + return env; +}; + +/** + * @ignore + * @private + * @param value + * @returns {Boolean} + */ +jasmine.isArray_ = function(value) { + return jasmine.isA_("Array", value); +}; + +/** + * @ignore + * @private + * @param value + * @returns {Boolean} + */ +jasmine.isString_ = function(value) { + return jasmine.isA_("String", value); +}; + +/** + * @ignore + * @private + * @param value + * @returns {Boolean} + */ +jasmine.isNumber_ = function(value) { + return jasmine.isA_("Number", value); +}; + +/** + * @ignore + * @private + * @param {String} typeName + * @param value + * @returns {Boolean} + */ +jasmine.isA_ = function(typeName, value) { + return Object.prototype.toString.apply(value) === '[object ' + typeName + ']'; +}; + +/** + * Pretty printer for expecations. Takes any object and turns it into a human-readable string. + * + * @param value {Object} an object to be outputted + * @returns {String} + */ +jasmine.pp = function(value) { + var stringPrettyPrinter = new jasmine.StringPrettyPrinter(); + stringPrettyPrinter.format(value); + return stringPrettyPrinter.string; +}; + +/** + * Returns true if the object is a DOM Node. + * + * @param {Object} obj object to check + * @returns {Boolean} + */ +jasmine.isDomNode = function(obj) { + return obj.nodeType > 0; +}; + +/** + * Returns a matchable 'generic' object of the class type. For use in expecations of type when values don't matter. + * + * @example + * // don't care about which function is passed in, as long as it's a function + * expect(mySpy).toHaveBeenCalledWith(jasmine.any(Function)); + * + * @param {Class} clazz + * @returns matchable object of the type clazz + */ +jasmine.any = function(clazz) { + return new jasmine.Matchers.Any(clazz); +}; + +/** + * Returns a matchable subset of a JSON object. For use in expectations when you don't care about all of the + * attributes on the object. + * + * @example + * // don't care about any other attributes than foo. + * expect(mySpy).toHaveBeenCalledWith(jasmine.objectContaining({foo: "bar"}); + * + * @param sample {Object} sample + * @returns matchable object for the sample + */ +jasmine.objectContaining = function (sample) { + return new jasmine.Matchers.ObjectContaining(sample); +}; + +/** + * Jasmine Spies are test doubles that can act as stubs, spies, fakes or when used in an expecation, mocks. + * + * Spies should be created in test setup, before expectations. They can then be checked, using the standard Jasmine + * expectation syntax. Spies can be checked if they were called or not and what the calling params were. + * + * A Spy has the following fields: wasCalled, callCount, mostRecentCall, and argsForCall (see docs). + * + * Spies are torn down at the end of every spec. + * + * Note: Do not call new jasmine.Spy() directly - a spy must be created using spyOn, jasmine.createSpy or jasmine.createSpyObj. + * + * @example + * // a stub + * var myStub = jasmine.createSpy('myStub'); // can be used anywhere + * + * // spy example + * var foo = { + * not: function(bool) { return !bool; } + * } + * + * // actual foo.not will not be called, execution stops + * spyOn(foo, 'not'); + + // foo.not spied upon, execution will continue to implementation + * spyOn(foo, 'not').andCallThrough(); + * + * // fake example + * var foo = { + * not: function(bool) { return !bool; } + * } + * + * // foo.not(val) will return val + * spyOn(foo, 'not').andCallFake(function(value) {return value;}); + * + * // mock example + * foo.not(7 == 7); + * expect(foo.not).toHaveBeenCalled(); + * expect(foo.not).toHaveBeenCalledWith(true); + * + * @constructor + * @see spyOn, jasmine.createSpy, jasmine.createSpyObj + * @param {String} name + */ +jasmine.Spy = function(name) { + /** + * The name of the spy, if provided. + */ + this.identity = name || 'unknown'; + /** + * Is this Object a spy? + */ + this.isSpy = true; + /** + * The actual function this spy stubs. + */ + this.plan = function() { + }; + /** + * Tracking of the most recent call to the spy. + * @example + * var mySpy = jasmine.createSpy('foo'); + * mySpy(1, 2); + * mySpy.mostRecentCall.args = [1, 2]; + */ + this.mostRecentCall = {}; + + /** + * Holds arguments for each call to the spy, indexed by call count + * @example + * var mySpy = jasmine.createSpy('foo'); + * mySpy(1, 2); + * mySpy(7, 8); + * mySpy.mostRecentCall.args = [7, 8]; + * mySpy.argsForCall[0] = [1, 2]; + * mySpy.argsForCall[1] = [7, 8]; + */ + this.argsForCall = []; + this.calls = []; +}; + +/** + * Tells a spy to call through to the actual implemenatation. + * + * @example + * var foo = { + * bar: function() { // do some stuff } + * } + * + * // defining a spy on an existing property: foo.bar + * spyOn(foo, 'bar').andCallThrough(); + */ +jasmine.Spy.prototype.andCallThrough = function() { + this.plan = this.originalValue; + return this; +}; + +/** + * For setting the return value of a spy. + * + * @example + * // defining a spy from scratch: foo() returns 'baz' + * var foo = jasmine.createSpy('spy on foo').andReturn('baz'); + * + * // defining a spy on an existing property: foo.bar() returns 'baz' + * spyOn(foo, 'bar').andReturn('baz'); + * + * @param {Object} value + */ +jasmine.Spy.prototype.andReturn = function(value) { + this.plan = function() { + return value; + }; + return this; +}; + +/** + * For throwing an exception when a spy is called. + * + * @example + * // defining a spy from scratch: foo() throws an exception w/ message 'ouch' + * var foo = jasmine.createSpy('spy on foo').andThrow('baz'); + * + * // defining a spy on an existing property: foo.bar() throws an exception w/ message 'ouch' + * spyOn(foo, 'bar').andThrow('baz'); + * + * @param {String} exceptionMsg + */ +jasmine.Spy.prototype.andThrow = function(exceptionMsg) { + this.plan = function() { + throw exceptionMsg; + }; + return this; +}; + +/** + * Calls an alternate implementation when a spy is called. + * + * @example + * var baz = function() { + * // do some stuff, return something + * } + * // defining a spy from scratch: foo() calls the function baz + * var foo = jasmine.createSpy('spy on foo').andCall(baz); + * + * // defining a spy on an existing property: foo.bar() calls an anonymnous function + * spyOn(foo, 'bar').andCall(function() { return 'baz';} ); + * + * @param {Function} fakeFunc + */ +jasmine.Spy.prototype.andCallFake = function(fakeFunc) { + this.plan = fakeFunc; + return this; +}; + +/** + * Resets all of a spy's the tracking variables so that it can be used again. + * + * @example + * spyOn(foo, 'bar'); + * + * foo.bar(); + * + * expect(foo.bar.callCount).toEqual(1); + * + * foo.bar.reset(); + * + * expect(foo.bar.callCount).toEqual(0); + */ +jasmine.Spy.prototype.reset = function() { + this.wasCalled = false; + this.callCount = 0; + this.argsForCall = []; + this.calls = []; + this.mostRecentCall = {}; +}; + +jasmine.createSpy = function(name) { + + var spyObj = function() { + spyObj.wasCalled = true; + spyObj.callCount++; + var args = jasmine.util.argsToArray(arguments); + spyObj.mostRecentCall.object = this; + spyObj.mostRecentCall.args = args; + spyObj.argsForCall.push(args); + spyObj.calls.push({object: this, args: args}); + return spyObj.plan.apply(this, arguments); + }; + + var spy = new jasmine.Spy(name); + + for (var prop in spy) { + spyObj[prop] = spy[prop]; + } + + spyObj.reset(); + + return spyObj; +}; + +/** + * Determines whether an object is a spy. + * + * @param {jasmine.Spy|Object} putativeSpy + * @returns {Boolean} + */ +jasmine.isSpy = function(putativeSpy) { + return putativeSpy && putativeSpy.isSpy; +}; + +/** + * Creates a more complicated spy: an Object that has every property a function that is a spy. Used for stubbing something + * large in one call. + * + * @param {String} baseName name of spy class + * @param {Array} methodNames array of names of methods to make spies + */ +jasmine.createSpyObj = function(baseName, methodNames) { + if (!jasmine.isArray_(methodNames) || methodNames.length === 0) { + throw new Error('createSpyObj requires a non-empty array of method names to create spies for'); + } + var obj = {}; + for (var i = 0; i < methodNames.length; i++) { + obj[methodNames[i]] = jasmine.createSpy(baseName + '.' + methodNames[i]); + } + return obj; +}; + +/** + * All parameters are pretty-printed and concatenated together, then written to the current spec's output. + * + * Be careful not to leave calls to jasmine.log in production code. + */ +jasmine.log = function() { + var spec = jasmine.getEnv().currentSpec; + spec.log.apply(spec, arguments); +}; + +/** + * Function that installs a spy on an existing object's method name. Used within a Spec to create a spy. + * + * @example + * // spy example + * var foo = { + * not: function(bool) { return !bool; } + * } + * spyOn(foo, 'not'); // actual foo.not will not be called, execution stops + * + * @see jasmine.createSpy + * @param obj + * @param methodName + * @return {jasmine.Spy} a Jasmine spy that can be chained with all spy methods + */ +var spyOn = function(obj, methodName) { + return jasmine.getEnv().currentSpec.spyOn(obj, methodName); +}; +if (isCommonJS) exports.spyOn = spyOn; + +/** + * Creates a Jasmine spec that will be added to the current suite. + * + * // TODO: pending tests + * + * @example + * it('should be true', function() { + * expect(true).toEqual(true); + * }); + * + * @param {String} desc description of this specification + * @param {Function} func defines the preconditions and expectations of the spec + */ +var it = function(desc, func) { + return jasmine.getEnv().it(desc, func); +}; +if (isCommonJS) exports.it = it; + +/** + * Creates a disabled Jasmine spec. + * + * A convenience method that allows existing specs to be disabled temporarily during development. + * + * @param {String} desc description of this specification + * @param {Function} func defines the preconditions and expectations of the spec + */ +var xit = function(desc, func) { + return jasmine.getEnv().xit(desc, func); +}; +if (isCommonJS) exports.xit = xit; + +/** + * Starts a chain for a Jasmine expectation. + * + * It is passed an Object that is the actual value and should chain to one of the many + * jasmine.Matchers functions. + * + * @param {Object} actual Actual value to test against and expected value + * @return {jasmine.Matchers} + */ +var expect = function(actual) { + return jasmine.getEnv().currentSpec.expect(actual); +}; +if (isCommonJS) exports.expect = expect; + +/** + * Defines part of a jasmine spec. Used in cominbination with waits or waitsFor in asynchrnous specs. + * + * @param {Function} func Function that defines part of a jasmine spec. + */ +var runs = function(func) { + jasmine.getEnv().currentSpec.runs(func); +}; +if (isCommonJS) exports.runs = runs; + +/** + * Waits a fixed time period before moving to the next block. + * + * @deprecated Use waitsFor() instead + * @param {Number} timeout milliseconds to wait + */ +var waits = function(timeout) { + jasmine.getEnv().currentSpec.waits(timeout); +}; +if (isCommonJS) exports.waits = waits; + +/** + * Waits for the latchFunction to return true before proceeding to the next block. + * + * @param {Function} latchFunction + * @param {String} optional_timeoutMessage + * @param {Number} optional_timeout + */ +var waitsFor = function(latchFunction, optional_timeoutMessage, optional_timeout) { + jasmine.getEnv().currentSpec.waitsFor.apply(jasmine.getEnv().currentSpec, arguments); +}; +if (isCommonJS) exports.waitsFor = waitsFor; + +/** + * A function that is called before each spec in a suite. + * + * Used for spec setup, including validating assumptions. + * + * @param {Function} beforeEachFunction + */ +var beforeEach = function(beforeEachFunction) { + jasmine.getEnv().beforeEach(beforeEachFunction); +}; +if (isCommonJS) exports.beforeEach = beforeEach; + +/** + * A function that is called after each spec in a suite. + * + * Used for restoring any state that is hijacked during spec execution. + * + * @param {Function} afterEachFunction + */ +var afterEach = function(afterEachFunction) { + jasmine.getEnv().afterEach(afterEachFunction); +}; +if (isCommonJS) exports.afterEach = afterEach; + +/** + * Defines a suite of specifications. + * + * Stores the description and all defined specs in the Jasmine environment as one suite of specs. Variables declared + * are accessible by calls to beforeEach, it, and afterEach. Describe blocks can be nested, allowing for specialization + * of setup in some tests. + * + * @example + * // TODO: a simple suite + * + * // TODO: a simple suite with a nested describe block + * + * @param {String} description A string, usually the class under test. + * @param {Function} specDefinitions function that defines several specs. + */ +var describe = function(description, specDefinitions) { + return jasmine.getEnv().describe(description, specDefinitions); +}; +if (isCommonJS) exports.describe = describe; + +/** + * Disables a suite of specifications. Used to disable some suites in a file, or files, temporarily during development. + * + * @param {String} description A string, usually the class under test. + * @param {Function} specDefinitions function that defines several specs. + */ +var xdescribe = function(description, specDefinitions) { + return jasmine.getEnv().xdescribe(description, specDefinitions); +}; +if (isCommonJS) exports.xdescribe = xdescribe; + + +// Provide the XMLHttpRequest class for IE 5.x-6.x: +jasmine.XmlHttpRequest = (typeof XMLHttpRequest == "undefined") ? function() { + function tryIt(f) { + try { + return f(); + } catch(e) { + } + return null; + } + + var xhr = tryIt(function() { + return new ActiveXObject("Msxml2.XMLHTTP.6.0"); + }) || + tryIt(function() { + return new ActiveXObject("Msxml2.XMLHTTP.3.0"); + }) || + tryIt(function() { + return new ActiveXObject("Msxml2.XMLHTTP"); + }) || + tryIt(function() { + return new ActiveXObject("Microsoft.XMLHTTP"); + }); + + if (!xhr) throw new Error("This browser does not support XMLHttpRequest."); + + return xhr; +} : XMLHttpRequest; +/** + * @namespace + */ +jasmine.util = {}; + +/** + * Declare that a child class inherit it's prototype from the parent class. + * + * @private + * @param {Function} childClass + * @param {Function} parentClass + */ +jasmine.util.inherit = function(childClass, parentClass) { + /** + * @private + */ + var subclass = function() { + }; + subclass.prototype = parentClass.prototype; + childClass.prototype = new subclass(); +}; + +jasmine.util.formatException = function(e) { + var lineNumber; + if (e.line) { + lineNumber = e.line; + } + else if (e.lineNumber) { + lineNumber = e.lineNumber; + } + + var file; + + if (e.sourceURL) { + file = e.sourceURL; + } + else if (e.fileName) { + file = e.fileName; + } + + var message = (e.name && e.message) ? (e.name + ': ' + e.message) : e.toString(); + + if (file && lineNumber) { + message += ' in ' + file + ' (line ' + lineNumber + ')'; + } + + return message; +}; + +jasmine.util.htmlEscape = function(str) { + if (!str) return str; + return str.replace(/&/g, '&') + .replace(//g, '>'); +}; + +jasmine.util.argsToArray = function(args) { + var arrayOfArgs = []; + for (var i = 0; i < args.length; i++) arrayOfArgs.push(args[i]); + return arrayOfArgs; +}; + +jasmine.util.extend = function(destination, source) { + for (var property in source) destination[property] = source[property]; + return destination; +}; + +/** + * Environment for Jasmine + * + * @constructor + */ +jasmine.Env = function() { + this.currentSpec = null; + this.currentSuite = null; + this.currentRunner_ = new jasmine.Runner(this); + + this.reporter = new jasmine.MultiReporter(); + + this.updateInterval = jasmine.DEFAULT_UPDATE_INTERVAL; + this.defaultTimeoutInterval = jasmine.DEFAULT_TIMEOUT_INTERVAL; + this.lastUpdate = 0; + this.specFilter = function() { + return true; + }; + + this.nextSpecId_ = 0; + this.nextSuiteId_ = 0; + this.equalityTesters_ = []; + + // wrap matchers + this.matchersClass = function() { + jasmine.Matchers.apply(this, arguments); + }; + jasmine.util.inherit(this.matchersClass, jasmine.Matchers); + + jasmine.Matchers.wrapInto_(jasmine.Matchers.prototype, this.matchersClass); +}; + + +jasmine.Env.prototype.setTimeout = jasmine.setTimeout; +jasmine.Env.prototype.clearTimeout = jasmine.clearTimeout; +jasmine.Env.prototype.setInterval = jasmine.setInterval; +jasmine.Env.prototype.clearInterval = jasmine.clearInterval; + +/** + * @returns an object containing jasmine version build info, if set. + */ +jasmine.Env.prototype.version = function () { + if (jasmine.version_) { + return jasmine.version_; + } else { + throw new Error('Version not set'); + } +}; + +/** + * @returns string containing jasmine version build info, if set. + */ +jasmine.Env.prototype.versionString = function() { + if (!jasmine.version_) { + return "version unknown"; + } + + var version = this.version(); + var versionString = version.major + "." + version.minor + "." + version.build; + if (version.release_candidate) { + versionString += ".rc" + version.release_candidate; + } + versionString += " revision " + version.revision; + return versionString; +}; + +/** + * @returns a sequential integer starting at 0 + */ +jasmine.Env.prototype.nextSpecId = function () { + return this.nextSpecId_++; +}; + +/** + * @returns a sequential integer starting at 0 + */ +jasmine.Env.prototype.nextSuiteId = function () { + return this.nextSuiteId_++; +}; + +/** + * Register a reporter to receive status updates from Jasmine. + * @param {jasmine.Reporter} reporter An object which will receive status updates. + */ +jasmine.Env.prototype.addReporter = function(reporter) { + this.reporter.addReporter(reporter); +}; + +jasmine.Env.prototype.execute = function() { + this.currentRunner_.execute(); +}; + +jasmine.Env.prototype.describe = function(description, specDefinitions) { + var suite = new jasmine.Suite(this, description, specDefinitions, this.currentSuite); + + var parentSuite = this.currentSuite; + if (parentSuite) { + parentSuite.add(suite); + } else { + this.currentRunner_.add(suite); + } + + this.currentSuite = suite; + + var declarationError = null; + try { + specDefinitions.call(suite); + } catch(e) { + declarationError = e; + } + + if (declarationError) { + this.it("encountered a declaration exception", function() { + throw declarationError; + }); + } + + this.currentSuite = parentSuite; + + return suite; +}; + +jasmine.Env.prototype.beforeEach = function(beforeEachFunction) { + if (this.currentSuite) { + this.currentSuite.beforeEach(beforeEachFunction); + } else { + this.currentRunner_.beforeEach(beforeEachFunction); + } +}; + +jasmine.Env.prototype.currentRunner = function () { + return this.currentRunner_; +}; + +jasmine.Env.prototype.afterEach = function(afterEachFunction) { + if (this.currentSuite) { + this.currentSuite.afterEach(afterEachFunction); + } else { + this.currentRunner_.afterEach(afterEachFunction); + } + +}; + +jasmine.Env.prototype.xdescribe = function(desc, specDefinitions) { + return { + execute: function() { + } + }; +}; + +jasmine.Env.prototype.it = function(description, func) { + var spec = new jasmine.Spec(this, this.currentSuite, description); + this.currentSuite.add(spec); + this.currentSpec = spec; + + if (func) { + spec.runs(func); + } + + return spec; +}; + +jasmine.Env.prototype.xit = function(desc, func) { + return { + id: this.nextSpecId(), + runs: function() { + } + }; +}; + +jasmine.Env.prototype.compareRegExps_ = function(a, b, mismatchKeys, mismatchValues) { + if (a.source != b.source) + mismatchValues.push("expected pattern /" + b.source + "/ is not equal to the pattern /" + a.source + "/"); + + if (a.ignoreCase != b.ignoreCase) + mismatchValues.push("expected modifier i was" + (b.ignoreCase ? " " : " not ") + "set and does not equal the origin modifier"); + + if (a.global != b.global) + mismatchValues.push("expected modifier g was" + (b.global ? " " : " not ") + "set and does not equal the origin modifier"); + + if (a.multiline != b.multiline) + mismatchValues.push("expected modifier m was" + (b.multiline ? " " : " not ") + "set and does not equal the origin modifier"); + + if (a.sticky != b.sticky) + mismatchValues.push("expected modifier y was" + (b.sticky ? " " : " not ") + "set and does not equal the origin modifier"); + + return (mismatchValues.length === 0); +}; + +jasmine.Env.prototype.compareObjects_ = function(a, b, mismatchKeys, mismatchValues) { + if (a.__Jasmine_been_here_before__ === b && b.__Jasmine_been_here_before__ === a) { + return true; + } + + a.__Jasmine_been_here_before__ = b; + b.__Jasmine_been_here_before__ = a; + + var hasKey = function(obj, keyName) { + return obj !== null && obj[keyName] !== jasmine.undefined; + }; + + for (var property in b) { + if (!hasKey(a, property) && hasKey(b, property)) { + mismatchKeys.push("expected has key '" + property + "', but missing from actual."); + } + } + for (property in a) { + if (!hasKey(b, property) && hasKey(a, property)) { + mismatchKeys.push("expected missing key '" + property + "', but present in actual."); + } + } + for (property in b) { + if (property == '__Jasmine_been_here_before__') continue; + if (!this.equals_(a[property], b[property], mismatchKeys, mismatchValues)) { + mismatchValues.push("'" + property + "' was '" + (b[property] ? jasmine.util.htmlEscape(b[property].toString()) : b[property]) + "' in expected, but was '" + (a[property] ? jasmine.util.htmlEscape(a[property].toString()) : a[property]) + "' in actual."); + } + } + + if (jasmine.isArray_(a) && jasmine.isArray_(b) && a.length != b.length) { + mismatchValues.push("arrays were not the same length"); + } + + delete a.__Jasmine_been_here_before__; + delete b.__Jasmine_been_here_before__; + return (mismatchKeys.length === 0 && mismatchValues.length === 0); +}; + +jasmine.Env.prototype.equals_ = function(a, b, mismatchKeys, mismatchValues) { + mismatchKeys = mismatchKeys || []; + mismatchValues = mismatchValues || []; + + for (var i = 0; i < this.equalityTesters_.length; i++) { + var equalityTester = this.equalityTesters_[i]; + var result = equalityTester(a, b, this, mismatchKeys, mismatchValues); + if (result !== jasmine.undefined) return result; + } + + if (a === b) return true; + + if (a === jasmine.undefined || a === null || b === jasmine.undefined || b === null) { + return (a == jasmine.undefined && b == jasmine.undefined); + } + + if (jasmine.isDomNode(a) && jasmine.isDomNode(b)) { + return a === b; + } + + if (a instanceof Date && b instanceof Date) { + return a.getTime() == b.getTime(); + } + + if (a.jasmineMatches) { + return a.jasmineMatches(b); + } + + if (b.jasmineMatches) { + return b.jasmineMatches(a); + } + + if (a instanceof jasmine.Matchers.ObjectContaining) { + return a.matches(b); + } + + if (b instanceof jasmine.Matchers.ObjectContaining) { + return b.matches(a); + } + + if (jasmine.isString_(a) && jasmine.isString_(b)) { + return (a == b); + } + + if (jasmine.isNumber_(a) && jasmine.isNumber_(b)) { + return (a == b); + } + + if (a instanceof RegExp && b instanceof RegExp) { + return this.compareRegExps_(a, b, mismatchKeys, mismatchValues); + } + + if (typeof a === "object" && typeof b === "object") { + return this.compareObjects_(a, b, mismatchKeys, mismatchValues); + } + + //Straight check + return (a === b); +}; + +jasmine.Env.prototype.contains_ = function(haystack, needle) { + if (jasmine.isArray_(haystack)) { + for (var i = 0; i < haystack.length; i++) { + if (this.equals_(haystack[i], needle)) return true; + } + return false; + } + return haystack.indexOf(needle) >= 0; +}; + +jasmine.Env.prototype.addEqualityTester = function(equalityTester) { + this.equalityTesters_.push(equalityTester); +}; +/** No-op base class for Jasmine reporters. + * + * @constructor + */ +jasmine.Reporter = function() { +}; + +//noinspection JSUnusedLocalSymbols +jasmine.Reporter.prototype.reportRunnerStarting = function(runner) { +}; + +//noinspection JSUnusedLocalSymbols +jasmine.Reporter.prototype.reportRunnerResults = function(runner) { +}; + +//noinspection JSUnusedLocalSymbols +jasmine.Reporter.prototype.reportSuiteResults = function(suite) { +}; + +//noinspection JSUnusedLocalSymbols +jasmine.Reporter.prototype.reportSpecStarting = function(spec) { +}; + +//noinspection JSUnusedLocalSymbols +jasmine.Reporter.prototype.reportSpecResults = function(spec) { +}; + +//noinspection JSUnusedLocalSymbols +jasmine.Reporter.prototype.log = function(str) { +}; + +/** + * Blocks are functions with executable code that make up a spec. + * + * @constructor + * @param {jasmine.Env} env + * @param {Function} func + * @param {jasmine.Spec} spec + */ +jasmine.Block = function(env, func, spec) { + this.env = env; + this.func = func; + this.spec = spec; +}; + +jasmine.Block.prototype.execute = function(onComplete) { + if (!jasmine.CATCH_EXCEPTIONS) { + this.func.apply(this.spec); + } + else { + try { + this.func.apply(this.spec); + } catch (e) { + this.spec.fail(e); + } + } + onComplete(); +}; +/** JavaScript API reporter. + * + * @constructor + */ +jasmine.JsApiReporter = function() { + this.started = false; + this.finished = false; + this.suites_ = []; + this.results_ = {}; +}; + +jasmine.JsApiReporter.prototype.reportRunnerStarting = function(runner) { + this.started = true; + var suites = runner.topLevelSuites(); + for (var i = 0; i < suites.length; i++) { + var suite = suites[i]; + this.suites_.push(this.summarize_(suite)); + } +}; + +jasmine.JsApiReporter.prototype.suites = function() { + return this.suites_; +}; + +jasmine.JsApiReporter.prototype.summarize_ = function(suiteOrSpec) { + var isSuite = suiteOrSpec instanceof jasmine.Suite; + var summary = { + id: suiteOrSpec.id, + name: suiteOrSpec.description, + type: isSuite ? 'suite' : 'spec', + children: [] + }; + + if (isSuite) { + var children = suiteOrSpec.children(); + for (var i = 0; i < children.length; i++) { + summary.children.push(this.summarize_(children[i])); + } + } + return summary; +}; + +jasmine.JsApiReporter.prototype.results = function() { + return this.results_; +}; + +jasmine.JsApiReporter.prototype.resultsForSpec = function(specId) { + return this.results_[specId]; +}; + +//noinspection JSUnusedLocalSymbols +jasmine.JsApiReporter.prototype.reportRunnerResults = function(runner) { + this.finished = true; +}; + +//noinspection JSUnusedLocalSymbols +jasmine.JsApiReporter.prototype.reportSuiteResults = function(suite) { +}; + +//noinspection JSUnusedLocalSymbols +jasmine.JsApiReporter.prototype.reportSpecResults = function(spec) { + this.results_[spec.id] = { + messages: spec.results().getItems(), + result: spec.results().failedCount > 0 ? "failed" : "passed" + }; +}; + +//noinspection JSUnusedLocalSymbols +jasmine.JsApiReporter.prototype.log = function(str) { +}; + +jasmine.JsApiReporter.prototype.resultsForSpecs = function(specIds){ + var results = {}; + for (var i = 0; i < specIds.length; i++) { + var specId = specIds[i]; + results[specId] = this.summarizeResult_(this.results_[specId]); + } + return results; +}; + +jasmine.JsApiReporter.prototype.summarizeResult_ = function(result){ + var summaryMessages = []; + var messagesLength = result.messages.length; + for (var messageIndex = 0; messageIndex < messagesLength; messageIndex++) { + var resultMessage = result.messages[messageIndex]; + summaryMessages.push({ + text: resultMessage.type == 'log' ? resultMessage.toString() : jasmine.undefined, + passed: resultMessage.passed ? resultMessage.passed() : true, + type: resultMessage.type, + message: resultMessage.message, + trace: { + stack: resultMessage.passed && !resultMessage.passed() ? resultMessage.trace.stack : jasmine.undefined + } + }); + } + + return { + result : result.result, + messages : summaryMessages + }; +}; + +/** + * @constructor + * @param {jasmine.Env} env + * @param actual + * @param {jasmine.Spec} spec + */ +jasmine.Matchers = function(env, actual, spec, opt_isNot) { + this.env = env; + this.actual = actual; + this.spec = spec; + this.isNot = opt_isNot || false; + this.reportWasCalled_ = false; +}; + +// todo: @deprecated as of Jasmine 0.11, remove soon [xw] +jasmine.Matchers.pp = function(str) { + throw new Error("jasmine.Matchers.pp() is no longer supported, please use jasmine.pp() instead!"); +}; + +// todo: @deprecated Deprecated as of Jasmine 0.10. Rewrite your custom matchers to return true or false. [xw] +jasmine.Matchers.prototype.report = function(result, failing_message, details) { + throw new Error("As of jasmine 0.11, custom matchers must be implemented differently -- please see jasmine docs"); +}; + +jasmine.Matchers.wrapInto_ = function(prototype, matchersClass) { + for (var methodName in prototype) { + if (methodName == 'report') continue; + var orig = prototype[methodName]; + matchersClass.prototype[methodName] = jasmine.Matchers.matcherFn_(methodName, orig); + } +}; + +jasmine.Matchers.matcherFn_ = function(matcherName, matcherFunction) { + return function() { + var matcherArgs = jasmine.util.argsToArray(arguments); + var result = matcherFunction.apply(this, arguments); + + if (this.isNot) { + result = !result; + } + + if (this.reportWasCalled_) return result; + + var message; + if (!result) { + if (this.message) { + message = this.message.apply(this, arguments); + if (jasmine.isArray_(message)) { + message = message[this.isNot ? 1 : 0]; + } + } else { + var englishyPredicate = matcherName.replace(/[A-Z]/g, function(s) { return ' ' + s.toLowerCase(); }); + message = "Expected " + jasmine.pp(this.actual) + (this.isNot ? " not " : " ") + englishyPredicate; + if (matcherArgs.length > 0) { + for (var i = 0; i < matcherArgs.length; i++) { + if (i > 0) message += ","; + message += " " + jasmine.pp(matcherArgs[i]); + } + } + message += "."; + } + } + var expectationResult = new jasmine.ExpectationResult({ + matcherName: matcherName, + passed: result, + expected: matcherArgs.length > 1 ? matcherArgs : matcherArgs[0], + actual: this.actual, + message: message + }); + this.spec.addMatcherResult(expectationResult); + return jasmine.undefined; + }; +}; + + + + +/** + * toBe: compares the actual to the expected using === + * @param expected + */ +jasmine.Matchers.prototype.toBe = function(expected) { + return this.actual === expected; +}; + +/** + * toNotBe: compares the actual to the expected using !== + * @param expected + * @deprecated as of 1.0. Use not.toBe() instead. + */ +jasmine.Matchers.prototype.toNotBe = function(expected) { + return this.actual !== expected; +}; + +/** + * toEqual: compares the actual to the expected using common sense equality. Handles Objects, Arrays, etc. + * + * @param expected + */ +jasmine.Matchers.prototype.toEqual = function(expected) { + return this.env.equals_(this.actual, expected); +}; + +/** + * toNotEqual: compares the actual to the expected using the ! of jasmine.Matchers.toEqual + * @param expected + * @deprecated as of 1.0. Use not.toEqual() instead. + */ +jasmine.Matchers.prototype.toNotEqual = function(expected) { + return !this.env.equals_(this.actual, expected); +}; + +/** + * Matcher that compares the actual to the expected using a regular expression. Constructs a RegExp, so takes + * a pattern or a String. + * + * @param expected + */ +jasmine.Matchers.prototype.toMatch = function(expected) { + return new RegExp(expected).test(this.actual); +}; + +/** + * Matcher that compares the actual to the expected using the boolean inverse of jasmine.Matchers.toMatch + * @param expected + * @deprecated as of 1.0. Use not.toMatch() instead. + */ +jasmine.Matchers.prototype.toNotMatch = function(expected) { + return !(new RegExp(expected).test(this.actual)); +}; + +/** + * Matcher that compares the actual to jasmine.undefined. + */ +jasmine.Matchers.prototype.toBeDefined = function() { + return (this.actual !== jasmine.undefined); +}; + +/** + * Matcher that compares the actual to jasmine.undefined. + */ +jasmine.Matchers.prototype.toBeUndefined = function() { + return (this.actual === jasmine.undefined); +}; + +/** + * Matcher that compares the actual to null. + */ +jasmine.Matchers.prototype.toBeNull = function() { + return (this.actual === null); +}; + +/** + * Matcher that compares the actual to NaN. + */ +jasmine.Matchers.prototype.toBeNaN = function() { + this.message = function() { + return [ "Expected " + jasmine.pp(this.actual) + " to be NaN." ]; + }; + + return (this.actual !== this.actual); +}; + +/** + * Matcher that boolean not-nots the actual. + */ +jasmine.Matchers.prototype.toBeTruthy = function() { + return !!this.actual; +}; + + +/** + * Matcher that boolean nots the actual. + */ +jasmine.Matchers.prototype.toBeFalsy = function() { + return !this.actual; +}; + + +/** + * Matcher that checks to see if the actual, a Jasmine spy, was called. + */ +jasmine.Matchers.prototype.toHaveBeenCalled = function() { + if (arguments.length > 0) { + throw new Error('toHaveBeenCalled does not take arguments, use toHaveBeenCalledWith'); + } + + if (!jasmine.isSpy(this.actual)) { + throw new Error('Expected a spy, but got ' + jasmine.pp(this.actual) + '.'); + } + + this.message = function() { + return [ + "Expected spy " + this.actual.identity + " to have been called.", + "Expected spy " + this.actual.identity + " not to have been called." + ]; + }; + + return this.actual.wasCalled; +}; + +/** @deprecated Use expect(xxx).toHaveBeenCalled() instead */ +jasmine.Matchers.prototype.wasCalled = jasmine.Matchers.prototype.toHaveBeenCalled; + +/** + * Matcher that checks to see if the actual, a Jasmine spy, was not called. + * + * @deprecated Use expect(xxx).not.toHaveBeenCalled() instead + */ +jasmine.Matchers.prototype.wasNotCalled = function() { + if (arguments.length > 0) { + throw new Error('wasNotCalled does not take arguments'); + } + + if (!jasmine.isSpy(this.actual)) { + throw new Error('Expected a spy, but got ' + jasmine.pp(this.actual) + '.'); + } + + this.message = function() { + return [ + "Expected spy " + this.actual.identity + " to not have been called.", + "Expected spy " + this.actual.identity + " to have been called." + ]; + }; + + return !this.actual.wasCalled; +}; + +/** + * Matcher that checks to see if the actual, a Jasmine spy, was called with a set of parameters. + * + * @example + * + */ +jasmine.Matchers.prototype.toHaveBeenCalledWith = function() { + var expectedArgs = jasmine.util.argsToArray(arguments); + if (!jasmine.isSpy(this.actual)) { + throw new Error('Expected a spy, but got ' + jasmine.pp(this.actual) + '.'); + } + this.message = function() { + var invertedMessage = "Expected spy " + this.actual.identity + " not to have been called with " + jasmine.pp(expectedArgs) + " but it was."; + var positiveMessage = ""; + if (this.actual.callCount === 0) { + positiveMessage = "Expected spy " + this.actual.identity + " to have been called with " + jasmine.pp(expectedArgs) + " but it was never called."; + } else { + positiveMessage = "Expected spy " + this.actual.identity + " to have been called with " + jasmine.pp(expectedArgs) + " but actual calls were " + jasmine.pp(this.actual.argsForCall).replace(/^\[ | \]$/g, '') + } + return [positiveMessage, invertedMessage]; + }; + + return this.env.contains_(this.actual.argsForCall, expectedArgs); +}; + +/** @deprecated Use expect(xxx).toHaveBeenCalledWith() instead */ +jasmine.Matchers.prototype.wasCalledWith = jasmine.Matchers.prototype.toHaveBeenCalledWith; + +/** @deprecated Use expect(xxx).not.toHaveBeenCalledWith() instead */ +jasmine.Matchers.prototype.wasNotCalledWith = function() { + var expectedArgs = jasmine.util.argsToArray(arguments); + if (!jasmine.isSpy(this.actual)) { + throw new Error('Expected a spy, but got ' + jasmine.pp(this.actual) + '.'); + } + + this.message = function() { + return [ + "Expected spy not to have been called with " + jasmine.pp(expectedArgs) + " but it was", + "Expected spy to have been called with " + jasmine.pp(expectedArgs) + " but it was" + ]; + }; + + return !this.env.contains_(this.actual.argsForCall, expectedArgs); +}; + +/** + * Matcher that checks that the expected item is an element in the actual Array. + * + * @param {Object} expected + */ +jasmine.Matchers.prototype.toContain = function(expected) { + return this.env.contains_(this.actual, expected); +}; + +/** + * Matcher that checks that the expected item is NOT an element in the actual Array. + * + * @param {Object} expected + * @deprecated as of 1.0. Use not.toContain() instead. + */ +jasmine.Matchers.prototype.toNotContain = function(expected) { + return !this.env.contains_(this.actual, expected); +}; + +jasmine.Matchers.prototype.toBeLessThan = function(expected) { + return this.actual < expected; +}; + +jasmine.Matchers.prototype.toBeGreaterThan = function(expected) { + return this.actual > expected; +}; + +/** + * Matcher that checks that the expected item is equal to the actual item + * up to a given level of decimal precision (default 2). + * + * @param {Number} expected + * @param {Number} precision, as number of decimal places + */ +jasmine.Matchers.prototype.toBeCloseTo = function(expected, precision) { + if (!(precision === 0)) { + precision = precision || 2; + } + return Math.abs(expected - this.actual) < (Math.pow(10, -precision) / 2); +}; + +/** + * Matcher that checks that the expected exception was thrown by the actual. + * + * @param {String} [expected] + */ +jasmine.Matchers.prototype.toThrow = function(expected) { + var result = false; + var exception; + if (typeof this.actual != 'function') { + throw new Error('Actual is not a function'); + } + try { + this.actual(); + } catch (e) { + exception = e; + } + if (exception) { + result = (expected === jasmine.undefined || this.env.equals_(exception.message || exception, expected.message || expected)); + } + + var not = this.isNot ? "not " : ""; + + this.message = function() { + if (exception && (expected === jasmine.undefined || !this.env.equals_(exception.message || exception, expected.message || expected))) { + return ["Expected function " + not + "to throw", expected ? expected.message || expected : "an exception", ", but it threw", exception.message || exception].join(' '); + } else { + return "Expected function to throw an exception."; + } + }; + + return result; +}; + +jasmine.Matchers.Any = function(expectedClass) { + this.expectedClass = expectedClass; +}; + +jasmine.Matchers.Any.prototype.jasmineMatches = function(other) { + if (this.expectedClass == String) { + return typeof other == 'string' || other instanceof String; + } + + if (this.expectedClass == Number) { + return typeof other == 'number' || other instanceof Number; + } + + if (this.expectedClass == Function) { + return typeof other == 'function' || other instanceof Function; + } + + if (this.expectedClass == Object) { + return typeof other == 'object'; + } + + return other instanceof this.expectedClass; +}; + +jasmine.Matchers.Any.prototype.jasmineToString = function() { + return ''; +}; + +jasmine.Matchers.ObjectContaining = function (sample) { + this.sample = sample; +}; + +jasmine.Matchers.ObjectContaining.prototype.jasmineMatches = function(other, mismatchKeys, mismatchValues) { + mismatchKeys = mismatchKeys || []; + mismatchValues = mismatchValues || []; + + var env = jasmine.getEnv(); + + var hasKey = function(obj, keyName) { + return obj != null && obj[keyName] !== jasmine.undefined; + }; + + for (var property in this.sample) { + if (!hasKey(other, property) && hasKey(this.sample, property)) { + mismatchKeys.push("expected has key '" + property + "', but missing from actual."); + } + else if (!env.equals_(this.sample[property], other[property], mismatchKeys, mismatchValues)) { + mismatchValues.push("'" + property + "' was '" + (other[property] ? jasmine.util.htmlEscape(other[property].toString()) : other[property]) + "' in expected, but was '" + (this.sample[property] ? jasmine.util.htmlEscape(this.sample[property].toString()) : this.sample[property]) + "' in actual."); + } + } + + return (mismatchKeys.length === 0 && mismatchValues.length === 0); +}; + +jasmine.Matchers.ObjectContaining.prototype.jasmineToString = function () { + return ""; +}; +// Mock setTimeout, clearTimeout +// Contributed by Pivotal Computer Systems, www.pivotalsf.com + +jasmine.FakeTimer = function() { + this.reset(); + + var self = this; + self.setTimeout = function(funcToCall, millis) { + self.timeoutsMade++; + self.scheduleFunction(self.timeoutsMade, funcToCall, millis, false); + return self.timeoutsMade; + }; + + self.setInterval = function(funcToCall, millis) { + self.timeoutsMade++; + self.scheduleFunction(self.timeoutsMade, funcToCall, millis, true); + return self.timeoutsMade; + }; + + self.clearTimeout = function(timeoutKey) { + self.scheduledFunctions[timeoutKey] = jasmine.undefined; + }; + + self.clearInterval = function(timeoutKey) { + self.scheduledFunctions[timeoutKey] = jasmine.undefined; + }; + +}; + +jasmine.FakeTimer.prototype.reset = function() { + this.timeoutsMade = 0; + this.scheduledFunctions = {}; + this.nowMillis = 0; +}; + +jasmine.FakeTimer.prototype.tick = function(millis) { + var oldMillis = this.nowMillis; + var newMillis = oldMillis + millis; + this.runFunctionsWithinRange(oldMillis, newMillis); + this.nowMillis = newMillis; +}; + +jasmine.FakeTimer.prototype.runFunctionsWithinRange = function(oldMillis, nowMillis) { + var scheduledFunc; + var funcsToRun = []; + for (var timeoutKey in this.scheduledFunctions) { + scheduledFunc = this.scheduledFunctions[timeoutKey]; + if (scheduledFunc != jasmine.undefined && + scheduledFunc.runAtMillis >= oldMillis && + scheduledFunc.runAtMillis <= nowMillis) { + funcsToRun.push(scheduledFunc); + this.scheduledFunctions[timeoutKey] = jasmine.undefined; + } + } + + if (funcsToRun.length > 0) { + funcsToRun.sort(function(a, b) { + return a.runAtMillis - b.runAtMillis; + }); + for (var i = 0; i < funcsToRun.length; ++i) { + try { + var funcToRun = funcsToRun[i]; + this.nowMillis = funcToRun.runAtMillis; + funcToRun.funcToCall(); + if (funcToRun.recurring) { + this.scheduleFunction(funcToRun.timeoutKey, + funcToRun.funcToCall, + funcToRun.millis, + true); + } + } catch(e) { + } + } + this.runFunctionsWithinRange(oldMillis, nowMillis); + } +}; + +jasmine.FakeTimer.prototype.scheduleFunction = function(timeoutKey, funcToCall, millis, recurring) { + this.scheduledFunctions[timeoutKey] = { + runAtMillis: this.nowMillis + millis, + funcToCall: funcToCall, + recurring: recurring, + timeoutKey: timeoutKey, + millis: millis + }; +}; + +/** + * @namespace + */ +jasmine.Clock = { + defaultFakeTimer: new jasmine.FakeTimer(), + + reset: function() { + jasmine.Clock.assertInstalled(); + jasmine.Clock.defaultFakeTimer.reset(); + }, + + tick: function(millis) { + jasmine.Clock.assertInstalled(); + jasmine.Clock.defaultFakeTimer.tick(millis); + }, + + runFunctionsWithinRange: function(oldMillis, nowMillis) { + jasmine.Clock.defaultFakeTimer.runFunctionsWithinRange(oldMillis, nowMillis); + }, + + scheduleFunction: function(timeoutKey, funcToCall, millis, recurring) { + jasmine.Clock.defaultFakeTimer.scheduleFunction(timeoutKey, funcToCall, millis, recurring); + }, + + useMock: function() { + if (!jasmine.Clock.isInstalled()) { + var spec = jasmine.getEnv().currentSpec; + spec.after(jasmine.Clock.uninstallMock); + + jasmine.Clock.installMock(); + } + }, + + installMock: function() { + jasmine.Clock.installed = jasmine.Clock.defaultFakeTimer; + }, + + uninstallMock: function() { + jasmine.Clock.assertInstalled(); + jasmine.Clock.installed = jasmine.Clock.real; + }, + + real: { + setTimeout: jasmine.getGlobal().setTimeout, + clearTimeout: jasmine.getGlobal().clearTimeout, + setInterval: jasmine.getGlobal().setInterval, + clearInterval: jasmine.getGlobal().clearInterval + }, + + assertInstalled: function() { + if (!jasmine.Clock.isInstalled()) { + throw new Error("Mock clock is not installed, use jasmine.Clock.useMock()"); + } + }, + + isInstalled: function() { + return jasmine.Clock.installed == jasmine.Clock.defaultFakeTimer; + }, + + installed: null +}; +jasmine.Clock.installed = jasmine.Clock.real; + +//else for IE support +jasmine.getGlobal().setTimeout = function(funcToCall, millis) { + if (jasmine.Clock.installed.setTimeout.apply) { + return jasmine.Clock.installed.setTimeout.apply(this, arguments); + } else { + return jasmine.Clock.installed.setTimeout(funcToCall, millis); + } +}; + +jasmine.getGlobal().setInterval = function(funcToCall, millis) { + if (jasmine.Clock.installed.setInterval.apply) { + return jasmine.Clock.installed.setInterval.apply(this, arguments); + } else { + return jasmine.Clock.installed.setInterval(funcToCall, millis); + } +}; + +jasmine.getGlobal().clearTimeout = function(timeoutKey) { + if (jasmine.Clock.installed.clearTimeout.apply) { + return jasmine.Clock.installed.clearTimeout.apply(this, arguments); + } else { + return jasmine.Clock.installed.clearTimeout(timeoutKey); + } +}; + +jasmine.getGlobal().clearInterval = function(timeoutKey) { + if (jasmine.Clock.installed.clearTimeout.apply) { + return jasmine.Clock.installed.clearInterval.apply(this, arguments); + } else { + return jasmine.Clock.installed.clearInterval(timeoutKey); + } +}; + +/** + * @constructor + */ +jasmine.MultiReporter = function() { + this.subReporters_ = []; +}; +jasmine.util.inherit(jasmine.MultiReporter, jasmine.Reporter); + +jasmine.MultiReporter.prototype.addReporter = function(reporter) { + this.subReporters_.push(reporter); +}; + +(function() { + var functionNames = [ + "reportRunnerStarting", + "reportRunnerResults", + "reportSuiteResults", + "reportSpecStarting", + "reportSpecResults", + "log" + ]; + for (var i = 0; i < functionNames.length; i++) { + var functionName = functionNames[i]; + jasmine.MultiReporter.prototype[functionName] = (function(functionName) { + return function() { + for (var j = 0; j < this.subReporters_.length; j++) { + var subReporter = this.subReporters_[j]; + if (subReporter[functionName]) { + subReporter[functionName].apply(subReporter, arguments); + } + } + }; + })(functionName); + } +})(); +/** + * Holds results for a set of Jasmine spec. Allows for the results array to hold another jasmine.NestedResults + * + * @constructor + */ +jasmine.NestedResults = function() { + /** + * The total count of results + */ + this.totalCount = 0; + /** + * Number of passed results + */ + this.passedCount = 0; + /** + * Number of failed results + */ + this.failedCount = 0; + /** + * Was this suite/spec skipped? + */ + this.skipped = false; + /** + * @ignore + */ + this.items_ = []; +}; + +/** + * Roll up the result counts. + * + * @param result + */ +jasmine.NestedResults.prototype.rollupCounts = function(result) { + this.totalCount += result.totalCount; + this.passedCount += result.passedCount; + this.failedCount += result.failedCount; +}; + +/** + * Adds a log message. + * @param values Array of message parts which will be concatenated later. + */ +jasmine.NestedResults.prototype.log = function(values) { + this.items_.push(new jasmine.MessageResult(values)); +}; + +/** + * Getter for the results: message & results. + */ +jasmine.NestedResults.prototype.getItems = function() { + return this.items_; +}; + +/** + * Adds a result, tracking counts (total, passed, & failed) + * @param {jasmine.ExpectationResult|jasmine.NestedResults} result + */ +jasmine.NestedResults.prototype.addResult = function(result) { + if (result.type != 'log') { + if (result.items_) { + this.rollupCounts(result); + } else { + this.totalCount++; + if (result.passed()) { + this.passedCount++; + } else { + this.failedCount++; + } + } + } + this.items_.push(result); +}; + +/** + * @returns {Boolean} True if everything below passed + */ +jasmine.NestedResults.prototype.passed = function() { + return this.passedCount === this.totalCount; +}; +/** + * Base class for pretty printing for expectation results. + */ +jasmine.PrettyPrinter = function() { + this.ppNestLevel_ = 0; +}; + +/** + * Formats a value in a nice, human-readable string. + * + * @param value + */ +jasmine.PrettyPrinter.prototype.format = function(value) { + this.ppNestLevel_++; + try { + if (value === jasmine.undefined) { + this.emitScalar('undefined'); + } else if (value === null) { + this.emitScalar('null'); + } else if (value === jasmine.getGlobal()) { + this.emitScalar(''); + } else if (value.jasmineToString) { + this.emitScalar(value.jasmineToString()); + } else if (typeof value === 'string') { + this.emitString(value); + } else if (jasmine.isSpy(value)) { + this.emitScalar("spy on " + value.identity); + } else if (value instanceof RegExp) { + this.emitScalar(value.toString()); + } else if (typeof value === 'function') { + this.emitScalar('Function'); + } else if (typeof value.nodeType === 'number') { + this.emitScalar('HTMLNode'); + } else if (value instanceof Date) { + this.emitScalar('Date(' + value + ')'); + } else if (value.__Jasmine_been_here_before__) { + this.emitScalar(''); + } else if (jasmine.isArray_(value) || typeof value == 'object') { + value.__Jasmine_been_here_before__ = true; + if (jasmine.isArray_(value)) { + this.emitArray(value); + } else { + this.emitObject(value); + } + delete value.__Jasmine_been_here_before__; + } else { + this.emitScalar(value.toString()); + } + } finally { + this.ppNestLevel_--; + } +}; + +jasmine.PrettyPrinter.prototype.iterateObject = function(obj, fn) { + for (var property in obj) { + if (!obj.hasOwnProperty(property)) continue; + if (property == '__Jasmine_been_here_before__') continue; + fn(property, obj.__lookupGetter__ ? (obj.__lookupGetter__(property) !== jasmine.undefined && + obj.__lookupGetter__(property) !== null) : false); + } +}; + +jasmine.PrettyPrinter.prototype.emitArray = jasmine.unimplementedMethod_; +jasmine.PrettyPrinter.prototype.emitObject = jasmine.unimplementedMethod_; +jasmine.PrettyPrinter.prototype.emitScalar = jasmine.unimplementedMethod_; +jasmine.PrettyPrinter.prototype.emitString = jasmine.unimplementedMethod_; + +jasmine.StringPrettyPrinter = function() { + jasmine.PrettyPrinter.call(this); + + this.string = ''; +}; +jasmine.util.inherit(jasmine.StringPrettyPrinter, jasmine.PrettyPrinter); + +jasmine.StringPrettyPrinter.prototype.emitScalar = function(value) { + this.append(value); +}; + +jasmine.StringPrettyPrinter.prototype.emitString = function(value) { + this.append("'" + value + "'"); +}; + +jasmine.StringPrettyPrinter.prototype.emitArray = function(array) { + if (this.ppNestLevel_ > jasmine.MAX_PRETTY_PRINT_DEPTH) { + this.append("Array"); + return; + } + + this.append('[ '); + for (var i = 0; i < array.length; i++) { + if (i > 0) { + this.append(', '); + } + this.format(array[i]); + } + this.append(' ]'); +}; + +jasmine.StringPrettyPrinter.prototype.emitObject = function(obj) { + if (this.ppNestLevel_ > jasmine.MAX_PRETTY_PRINT_DEPTH) { + this.append("Object"); + return; + } + + var self = this; + this.append('{ '); + var first = true; + + this.iterateObject(obj, function(property, isGetter) { + if (first) { + first = false; + } else { + self.append(', '); + } + + self.append(property); + self.append(' : '); + if (isGetter) { + self.append(''); + } else { + self.format(obj[property]); + } + }); + + this.append(' }'); +}; + +jasmine.StringPrettyPrinter.prototype.append = function(value) { + this.string += value; +}; +jasmine.Queue = function(env) { + this.env = env; + + // parallel to blocks. each true value in this array means the block will + // get executed even if we abort + this.ensured = []; + this.blocks = []; + this.running = false; + this.index = 0; + this.offset = 0; + this.abort = false; +}; + +jasmine.Queue.prototype.addBefore = function(block, ensure) { + if (ensure === jasmine.undefined) { + ensure = false; + } + + this.blocks.unshift(block); + this.ensured.unshift(ensure); +}; + +jasmine.Queue.prototype.add = function(block, ensure) { + if (ensure === jasmine.undefined) { + ensure = false; + } + + this.blocks.push(block); + this.ensured.push(ensure); +}; + +jasmine.Queue.prototype.insertNext = function(block, ensure) { + if (ensure === jasmine.undefined) { + ensure = false; + } + + this.ensured.splice((this.index + this.offset + 1), 0, ensure); + this.blocks.splice((this.index + this.offset + 1), 0, block); + this.offset++; +}; + +jasmine.Queue.prototype.start = function(onComplete) { + this.running = true; + this.onComplete = onComplete; + this.next_(); +}; + +jasmine.Queue.prototype.isRunning = function() { + return this.running; +}; + +jasmine.Queue.LOOP_DONT_RECURSE = true; + +jasmine.Queue.prototype.next_ = function() { + var self = this; + var goAgain = true; + + while (goAgain) { + goAgain = false; + + if (self.index < self.blocks.length && !(this.abort && !this.ensured[self.index])) { + var calledSynchronously = true; + var completedSynchronously = false; + + var onComplete = function () { + if (jasmine.Queue.LOOP_DONT_RECURSE && calledSynchronously) { + completedSynchronously = true; + return; + } + + if (self.blocks[self.index].abort) { + self.abort = true; + } + + self.offset = 0; + self.index++; + + var now = new Date().getTime(); + if (self.env.updateInterval && now - self.env.lastUpdate > self.env.updateInterval) { + self.env.lastUpdate = now; + self.env.setTimeout(function() { + self.next_(); + }, 0); + } else { + if (jasmine.Queue.LOOP_DONT_RECURSE && completedSynchronously) { + goAgain = true; + } else { + self.next_(); + } + } + }; + self.blocks[self.index].execute(onComplete); + + calledSynchronously = false; + if (completedSynchronously) { + onComplete(); + } + + } else { + self.running = false; + if (self.onComplete) { + self.onComplete(); + } + } + } +}; + +jasmine.Queue.prototype.results = function() { + var results = new jasmine.NestedResults(); + for (var i = 0; i < this.blocks.length; i++) { + if (this.blocks[i].results) { + results.addResult(this.blocks[i].results()); + } + } + return results; +}; + + +/** + * Runner + * + * @constructor + * @param {jasmine.Env} env + */ +jasmine.Runner = function(env) { + var self = this; + self.env = env; + self.queue = new jasmine.Queue(env); + self.before_ = []; + self.after_ = []; + self.suites_ = []; +}; + +jasmine.Runner.prototype.execute = function() { + var self = this; + if (self.env.reporter.reportRunnerStarting) { + self.env.reporter.reportRunnerStarting(this); + } + self.queue.start(function () { + self.finishCallback(); + }); +}; + +jasmine.Runner.prototype.beforeEach = function(beforeEachFunction) { + beforeEachFunction.typeName = 'beforeEach'; + this.before_.splice(0,0,beforeEachFunction); +}; + +jasmine.Runner.prototype.afterEach = function(afterEachFunction) { + afterEachFunction.typeName = 'afterEach'; + this.after_.splice(0,0,afterEachFunction); +}; + + +jasmine.Runner.prototype.finishCallback = function() { + this.env.reporter.reportRunnerResults(this); +}; + +jasmine.Runner.prototype.addSuite = function(suite) { + this.suites_.push(suite); +}; + +jasmine.Runner.prototype.add = function(block) { + if (block instanceof jasmine.Suite) { + this.addSuite(block); + } + this.queue.add(block); +}; + +jasmine.Runner.prototype.specs = function () { + var suites = this.suites(); + var specs = []; + for (var i = 0; i < suites.length; i++) { + specs = specs.concat(suites[i].specs()); + } + return specs; +}; + +jasmine.Runner.prototype.suites = function() { + return this.suites_; +}; + +jasmine.Runner.prototype.topLevelSuites = function() { + var topLevelSuites = []; + for (var i = 0; i < this.suites_.length; i++) { + if (!this.suites_[i].parentSuite) { + topLevelSuites.push(this.suites_[i]); + } + } + return topLevelSuites; +}; + +jasmine.Runner.prototype.results = function() { + return this.queue.results(); +}; +/** + * Internal representation of a Jasmine specification, or test. + * + * @constructor + * @param {jasmine.Env} env + * @param {jasmine.Suite} suite + * @param {String} description + */ +jasmine.Spec = function(env, suite, description) { + if (!env) { + throw new Error('jasmine.Env() required'); + } + if (!suite) { + throw new Error('jasmine.Suite() required'); + } + var spec = this; + spec.id = env.nextSpecId ? env.nextSpecId() : null; + spec.env = env; + spec.suite = suite; + spec.description = description; + spec.queue = new jasmine.Queue(env); + + spec.afterCallbacks = []; + spec.spies_ = []; + + spec.results_ = new jasmine.NestedResults(); + spec.results_.description = description; + spec.matchersClass = null; +}; + +jasmine.Spec.prototype.getFullName = function() { + return this.suite.getFullName() + ' ' + this.description + '.'; +}; + + +jasmine.Spec.prototype.results = function() { + return this.results_; +}; + +/** + * All parameters are pretty-printed and concatenated together, then written to the spec's output. + * + * Be careful not to leave calls to jasmine.log in production code. + */ +jasmine.Spec.prototype.log = function() { + return this.results_.log(arguments); +}; + +jasmine.Spec.prototype.runs = function (func) { + var block = new jasmine.Block(this.env, func, this); + this.addToQueue(block); + return this; +}; + +jasmine.Spec.prototype.addToQueue = function (block) { + if (this.queue.isRunning()) { + this.queue.insertNext(block); + } else { + this.queue.add(block); + } +}; + +/** + * @param {jasmine.ExpectationResult} result + */ +jasmine.Spec.prototype.addMatcherResult = function(result) { + this.results_.addResult(result); +}; + +jasmine.Spec.prototype.expect = function(actual) { + var positive = new (this.getMatchersClass_())(this.env, actual, this); + positive.not = new (this.getMatchersClass_())(this.env, actual, this, true); + return positive; +}; + +/** + * Waits a fixed time period before moving to the next block. + * + * @deprecated Use waitsFor() instead + * @param {Number} timeout milliseconds to wait + */ +jasmine.Spec.prototype.waits = function(timeout) { + var waitsFunc = new jasmine.WaitsBlock(this.env, timeout, this); + this.addToQueue(waitsFunc); + return this; +}; + +/** + * Waits for the latchFunction to return true before proceeding to the next block. + * + * @param {Function} latchFunction + * @param {String} optional_timeoutMessage + * @param {Number} optional_timeout + */ +jasmine.Spec.prototype.waitsFor = function(latchFunction, optional_timeoutMessage, optional_timeout) { + var latchFunction_ = null; + var optional_timeoutMessage_ = null; + var optional_timeout_ = null; + + for (var i = 0; i < arguments.length; i++) { + var arg = arguments[i]; + switch (typeof arg) { + case 'function': + latchFunction_ = arg; + break; + case 'string': + optional_timeoutMessage_ = arg; + break; + case 'number': + optional_timeout_ = arg; + break; + } + } + + var waitsForFunc = new jasmine.WaitsForBlock(this.env, optional_timeout_, latchFunction_, optional_timeoutMessage_, this); + this.addToQueue(waitsForFunc); + return this; +}; + +jasmine.Spec.prototype.fail = function (e) { + var expectationResult = new jasmine.ExpectationResult({ + passed: false, + message: e ? jasmine.util.formatException(e) : 'Exception', + trace: { stack: e.stack } + }); + this.results_.addResult(expectationResult); +}; + +jasmine.Spec.prototype.getMatchersClass_ = function() { + return this.matchersClass || this.env.matchersClass; +}; + +jasmine.Spec.prototype.addMatchers = function(matchersPrototype) { + var parent = this.getMatchersClass_(); + var newMatchersClass = function() { + parent.apply(this, arguments); + }; + jasmine.util.inherit(newMatchersClass, parent); + jasmine.Matchers.wrapInto_(matchersPrototype, newMatchersClass); + this.matchersClass = newMatchersClass; +}; + +jasmine.Spec.prototype.finishCallback = function() { + this.env.reporter.reportSpecResults(this); +}; + +jasmine.Spec.prototype.finish = function(onComplete) { + this.removeAllSpies(); + this.finishCallback(); + if (onComplete) { + onComplete(); + } +}; + +jasmine.Spec.prototype.after = function(doAfter) { + if (this.queue.isRunning()) { + this.queue.add(new jasmine.Block(this.env, doAfter, this), true); + } else { + this.afterCallbacks.unshift(doAfter); + } +}; + +jasmine.Spec.prototype.execute = function(onComplete) { + var spec = this; + if (!spec.env.specFilter(spec)) { + spec.results_.skipped = true; + spec.finish(onComplete); + return; + } + + this.env.reporter.reportSpecStarting(this); + + spec.env.currentSpec = spec; + + spec.addBeforesAndAftersToQueue(); + + spec.queue.start(function () { + spec.finish(onComplete); + }); +}; + +jasmine.Spec.prototype.addBeforesAndAftersToQueue = function() { + var runner = this.env.currentRunner(); + var i; + + for (var suite = this.suite; suite; suite = suite.parentSuite) { + for (i = 0; i < suite.before_.length; i++) { + this.queue.addBefore(new jasmine.Block(this.env, suite.before_[i], this)); + } + } + for (i = 0; i < runner.before_.length; i++) { + this.queue.addBefore(new jasmine.Block(this.env, runner.before_[i], this)); + } + for (i = 0; i < this.afterCallbacks.length; i++) { + this.queue.add(new jasmine.Block(this.env, this.afterCallbacks[i], this), true); + } + for (suite = this.suite; suite; suite = suite.parentSuite) { + for (i = 0; i < suite.after_.length; i++) { + this.queue.add(new jasmine.Block(this.env, suite.after_[i], this), true); + } + } + for (i = 0; i < runner.after_.length; i++) { + this.queue.add(new jasmine.Block(this.env, runner.after_[i], this), true); + } +}; + +jasmine.Spec.prototype.explodes = function() { + throw 'explodes function should not have been called'; +}; + +jasmine.Spec.prototype.spyOn = function(obj, methodName, ignoreMethodDoesntExist) { + if (obj == jasmine.undefined) { + throw "spyOn could not find an object to spy upon for " + methodName + "()"; + } + + if (!ignoreMethodDoesntExist && obj[methodName] === jasmine.undefined) { + throw methodName + '() method does not exist'; + } + + if (!ignoreMethodDoesntExist && obj[methodName] && obj[methodName].isSpy) { + throw new Error(methodName + ' has already been spied upon'); + } + + var spyObj = jasmine.createSpy(methodName); + + this.spies_.push(spyObj); + spyObj.baseObj = obj; + spyObj.methodName = methodName; + spyObj.originalValue = obj[methodName]; + + obj[methodName] = spyObj; + + return spyObj; +}; + +jasmine.Spec.prototype.removeAllSpies = function() { + for (var i = 0; i < this.spies_.length; i++) { + var spy = this.spies_[i]; + spy.baseObj[spy.methodName] = spy.originalValue; + } + this.spies_ = []; +}; + +/** + * Internal representation of a Jasmine suite. + * + * @constructor + * @param {jasmine.Env} env + * @param {String} description + * @param {Function} specDefinitions + * @param {jasmine.Suite} parentSuite + */ +jasmine.Suite = function(env, description, specDefinitions, parentSuite) { + var self = this; + self.id = env.nextSuiteId ? env.nextSuiteId() : null; + self.description = description; + self.queue = new jasmine.Queue(env); + self.parentSuite = parentSuite; + self.env = env; + self.before_ = []; + self.after_ = []; + self.children_ = []; + self.suites_ = []; + self.specs_ = []; +}; + +jasmine.Suite.prototype.getFullName = function() { + var fullName = this.description; + for (var parentSuite = this.parentSuite; parentSuite; parentSuite = parentSuite.parentSuite) { + fullName = parentSuite.description + ' ' + fullName; + } + return fullName; +}; + +jasmine.Suite.prototype.finish = function(onComplete) { + this.env.reporter.reportSuiteResults(this); + this.finished = true; + if (typeof(onComplete) == 'function') { + onComplete(); + } +}; + +jasmine.Suite.prototype.beforeEach = function(beforeEachFunction) { + beforeEachFunction.typeName = 'beforeEach'; + this.before_.unshift(beforeEachFunction); +}; + +jasmine.Suite.prototype.afterEach = function(afterEachFunction) { + afterEachFunction.typeName = 'afterEach'; + this.after_.unshift(afterEachFunction); +}; + +jasmine.Suite.prototype.results = function() { + return this.queue.results(); +}; + +jasmine.Suite.prototype.add = function(suiteOrSpec) { + this.children_.push(suiteOrSpec); + if (suiteOrSpec instanceof jasmine.Suite) { + this.suites_.push(suiteOrSpec); + this.env.currentRunner().addSuite(suiteOrSpec); + } else { + this.specs_.push(suiteOrSpec); + } + this.queue.add(suiteOrSpec); +}; + +jasmine.Suite.prototype.specs = function() { + return this.specs_; +}; + +jasmine.Suite.prototype.suites = function() { + return this.suites_; +}; + +jasmine.Suite.prototype.children = function() { + return this.children_; +}; + +jasmine.Suite.prototype.execute = function(onComplete) { + var self = this; + this.queue.start(function () { + self.finish(onComplete); + }); +}; +jasmine.WaitsBlock = function(env, timeout, spec) { + this.timeout = timeout; + jasmine.Block.call(this, env, null, spec); +}; + +jasmine.util.inherit(jasmine.WaitsBlock, jasmine.Block); + +jasmine.WaitsBlock.prototype.execute = function (onComplete) { + if (jasmine.VERBOSE) { + this.env.reporter.log('>> Jasmine waiting for ' + this.timeout + ' ms...'); + } + this.env.setTimeout(function () { + onComplete(); + }, this.timeout); +}; +/** + * A block which waits for some condition to become true, with timeout. + * + * @constructor + * @extends jasmine.Block + * @param {jasmine.Env} env The Jasmine environment. + * @param {Number} timeout The maximum time in milliseconds to wait for the condition to become true. + * @param {Function} latchFunction A function which returns true when the desired condition has been met. + * @param {String} message The message to display if the desired condition hasn't been met within the given time period. + * @param {jasmine.Spec} spec The Jasmine spec. + */ +jasmine.WaitsForBlock = function(env, timeout, latchFunction, message, spec) { + this.timeout = timeout || env.defaultTimeoutInterval; + this.latchFunction = latchFunction; + this.message = message; + this.totalTimeSpentWaitingForLatch = 0; + jasmine.Block.call(this, env, null, spec); +}; +jasmine.util.inherit(jasmine.WaitsForBlock, jasmine.Block); + +jasmine.WaitsForBlock.TIMEOUT_INCREMENT = 10; + +jasmine.WaitsForBlock.prototype.execute = function(onComplete) { + if (jasmine.VERBOSE) { + this.env.reporter.log('>> Jasmine waiting for ' + (this.message || 'something to happen')); + } + var latchFunctionResult; + try { + latchFunctionResult = this.latchFunction.apply(this.spec); + } catch (e) { + this.spec.fail(e); + onComplete(); + return; + } + + if (latchFunctionResult) { + onComplete(); + } else if (this.totalTimeSpentWaitingForLatch >= this.timeout) { + var message = 'timed out after ' + this.timeout + ' msec waiting for ' + (this.message || 'something to happen'); + this.spec.fail({ + name: 'timeout', + message: message + }); + + this.abort = true; + onComplete(); + } else { + this.totalTimeSpentWaitingForLatch += jasmine.WaitsForBlock.TIMEOUT_INCREMENT; + var self = this; + this.env.setTimeout(function() { + self.execute(onComplete); + }, jasmine.WaitsForBlock.TIMEOUT_INCREMENT); + } +}; + +jasmine.version_= { + "major": 1, + "minor": 3, + "build": 1, + "revision": 1354556913 +}; diff --git a/test/unit/runner.html b/test/unit/runner.html new file mode 100644 index 00000000000..42fea8411a7 --- /dev/null +++ b/test/unit/runner.html @@ -0,0 +1,28 @@ + + + + + Vue.js unit tests + + + + + + + + + + \ No newline at end of file diff --git a/test/unit/binding_spec.js b/test/unit/specs/binding_spec.js similarity index 100% rename from test/unit/binding_spec.js rename to test/unit/specs/binding_spec.js diff --git a/test/unit/cache_spec.js b/test/unit/specs/cache_spec.js similarity index 96% rename from test/unit/cache_spec.js rename to test/unit/specs/cache_spec.js index 0fa29df6f17..39df0e89d36 100644 --- a/test/unit/cache_spec.js +++ b/test/unit/specs/cache_spec.js @@ -1,4 +1,4 @@ -var Cache = require('../../src/cache') +var Cache = require('../../../src/cache') /** * Debug function to assert cache state diff --git a/test/unit/directive_parser_spec.js b/test/unit/specs/directive_parser_spec.js similarity index 98% rename from test/unit/directive_parser_spec.js rename to test/unit/specs/directive_parser_spec.js index 7b244d1331d..eff5105a786 100644 --- a/test/unit/directive_parser_spec.js +++ b/test/unit/specs/directive_parser_spec.js @@ -1,4 +1,4 @@ -var parse = require('../../src/parse/directive').parse +var parse = require('../../../src/parse/directive').parse describe('Directive Parser', function () { diff --git a/test/unit/expression_parser_spec.js b/test/unit/specs/expression_parser_spec.js similarity index 97% rename from test/unit/expression_parser_spec.js rename to test/unit/specs/expression_parser_spec.js index c406edadaab..133fb2fb9a2 100644 --- a/test/unit/expression_parser_spec.js +++ b/test/unit/specs/expression_parser_spec.js @@ -1,4 +1,4 @@ -var expParser = require('../../src/parse/expression') +var expParser = require('../../../src/parse/expression') function assertExp (testCase) { var fn = expParser.parse(testCase.exp) diff --git a/test/unit/observer_spec.js b/test/unit/specs/observer_spec.js similarity index 99% rename from test/unit/observer_spec.js rename to test/unit/specs/observer_spec.js index 9cd04cfa442..a10e7dc1b45 100644 --- a/test/unit/observer_spec.js +++ b/test/unit/specs/observer_spec.js @@ -2,7 +2,7 @@ * Test data observation. */ -var Observer = require('../../src/observe/observer') +var Observer = require('../../../src/observe/observer') // internal emitter has fixed 3 arguments // so we need to fill up the assetions with undefined var u = undefined diff --git a/test/unit/path_parser_spec.js b/test/unit/specs/path_parser_spec.js similarity index 98% rename from test/unit/path_parser_spec.js rename to test/unit/specs/path_parser_spec.js index 05418c9a2f7..d2f8aae917a 100644 --- a/test/unit/path_parser_spec.js +++ b/test/unit/specs/path_parser_spec.js @@ -1,4 +1,4 @@ -var Path = require('../../src/parse/path') +var Path = require('../../../src/parse/path') function assertPath (str, expected) { var path = Path.parse(str) diff --git a/test/unit/scope_spec.js b/test/unit/specs/scope_spec.js similarity index 98% rename from test/unit/scope_spec.js rename to test/unit/specs/scope_spec.js index bbe2b398ccd..053a5ec684e 100644 --- a/test/unit/scope_spec.js +++ b/test/unit/specs/scope_spec.js @@ -3,8 +3,8 @@ * data event propagation and data sync */ -var Vue = require('../../src/vue') -var Observer = require('../../src/observe/observer') +var Vue = require('../../../src/vue') +var Observer = require('../../../src/observe/observer') var u = undefined Observer.pathDelimiter = '.' diff --git a/test/unit/template.js b/test/unit/specs/template.js similarity index 98% rename from test/unit/template.js rename to test/unit/specs/template.js index d9aad4da633..02bc883d8d3 100644 --- a/test/unit/template.js +++ b/test/unit/specs/template.js @@ -1,4 +1,4 @@ -var templateParser = require('../../src/parse/template') +var templateParser = require('../../../src/parse/template') var parse = templateParser.parse var testString = '
hello

world

' diff --git a/test/unit/util_spec.js b/test/unit/specs/util_spec.js similarity index 99% rename from test/unit/util_spec.js rename to test/unit/specs/util_spec.js index 47f8ab98d73..6258b536269 100644 --- a/test/unit/util_spec.js +++ b/test/unit/specs/util_spec.js @@ -1,5 +1,5 @@ -var _ = require('../../src/util') -var config = require('../../src/config') +var _ = require('../../../src/util') +var config = require('../../../src/config') config.silent = true describe('Util', function () { From 9a3c9829d467783db661f9daf1df6a1aba0fa565 Mon Sep 17 00:00:00 2001 From: Evan You Date: Sat, 2 Aug 2014 18:40:59 -0400 Subject: [PATCH 0090/1534] binding redesign + path optimization --- src/binding.js | 45 ----------------------- src/instance/bindings.js | 55 ++++++++++++++--------------- src/parse/path.js | 48 ++++++++++++++++++------- test/unit/specs/path_parser_spec.js | 33 +++++++++++------ 4 files changed, 83 insertions(+), 98 deletions(-) diff --git a/src/binding.js b/src/binding.js index f3db9122e25..e4a021c62c9 100644 --- a/src/binding.js +++ b/src/binding.js @@ -35,51 +35,6 @@ p.addChild = function (key, child) { return child } -/** - * Return the child at the given key - * - * @param {String} key - * @return {Binding} - */ - -p.getChild = function (key) { - return this.children[key] -} - -/** - * Traverse along a path and trigger updates - * along the way. - * - * @param {Array} path - */ - -p.updatePath = function (path) { - var b = this - for (var i = 0, l = path.length; i < l; i++) { - if (!b) return - b.notify() - b = b.children[path[i]] - } - // for the destination of path, we need to trigger - // change for every children. i.e. when an object is - // swapped, all its content need to be updated. - if (b) { - b.updateTree() - } -} - -/** - * Trigger updates for the subtree starting at - * self as root. Recursive. - */ - -p.updateTree = function () { - this.notify() - for (var key in this.children) { - this.children[key].updateTree() - } -} - /** * Add a directive subscriber. * diff --git a/src/instance/bindings.js b/src/instance/bindings.js index 00e24bd6c57..a140e5b9a94 100644 --- a/src/instance/bindings.js +++ b/src/instance/bindings.js @@ -1,5 +1,5 @@ var Binding = require('../binding') -var Observer = require('../observe/observer') +var Path = require('../parse/path') /** * Setup the binding tree. @@ -29,12 +29,12 @@ exports._initBindings = function () { root.addChild('$root', this.$root._rootBinding) } // observer already has callback context set to `this` - var updateBinding = this._updateBinding + var update = this._updateBindingAt this._observer - .on('set', updateBinding) - .on('add', updateBinding) - .on('delete', updateBinding) - .on('mutate', updateBinding) + .on('set', update) + .on('add', update) + .on('delete', update) + .on('mutate', update) } /** @@ -42,27 +42,15 @@ exports._initBindings = function () { * If `create` is true, create all bindings that do not * exist yet along the way. * - * @param {Array} path - * @param {Boolean} create + * @param {String} path + * @param {Boolean} fromObserver * @return {Binding|undefined} */ -exports._getBindingAt = function (path, create) { - var b = this._rootBinding - var child, key - for (var i = 0, l = path.length; i < l; i++) { - key = path[i] - child = b.getChild(key) - if (!child) { - if (create) { - child = b.addChild(key) - } else { - return - } - } - b = child - } - return b +exports._getBindingAt = function (path, fromObserver) { + return fromObserver + ? Path.getFromObserver(this._rootBinding, path) + : Path.get(this._rootBinding, path) } /** @@ -74,18 +62,27 @@ exports._getBindingAt = function (path, create) { */ exports._createBindingAt = function (path) { - return this._getBindingAt(path, true) + var b = this._rootBinding + var child, key + for (var i = 0, l = path.length; i < l; i++) { + key = path[i] + child = b.children[key] || b.addChild(key) + b = child + } + return b } /** - * Trigger a path update on the root binding. + * Trigger update for the binding at given path. * * @param {String} path - this path comes directly from the * data observer, so it is a single string * delimited by "\b". */ -exports._updateBinding = function (path) { - path = path.split(Observer.pathDelimiter) - this._rootBinding.updatePath(path) +exports._updateBindingAt = function (path) { + var binding = this._getBindingAt(path, true) + if (binding) { + binding.notify() + } } \ No newline at end of file diff --git a/src/parse/path.js b/src/parse/path.js index 7180ccfe791..43376bb276b 100644 --- a/src/parse/path.js +++ b/src/parse/path.js @@ -1,5 +1,6 @@ var Cache = require('../cache') var pathCache = new Cache(1000) +var Observer = require('../observe/observer') var identStart = '[$_a-zA-Z]' var identPart = '[$_a-zA-Z0-9]' var IDENT_RE = new RegExp('^' + identStart + '+' + identPart + '*' + '$') @@ -229,7 +230,7 @@ function formatAccessor(key) { * @return {Function} */ -exports.compileGetter = function (path) { +function compileGetter (path) { var body = 'if (o != null' var pathString = 'o' var key @@ -256,7 +257,7 @@ exports.parse = function (path) { if (!hit) { hit = parsePath(path) if (hit) { - hit.get = exports.compileGetter(hit) + hit.get = compileGetter(hit) pathCache.put(path, hit) } } @@ -264,24 +265,27 @@ exports.parse = function (path) { } /** - * Get from an object from a path + * Get from an object from a path string * * @param {Object} obj * @param {String} path */ exports.get = function (obj, path) { - if (typeof path === 'string') { - path = exports.parse(path) - } - if (!path) { - return - } - // path has compiled getter - if (path.get) { + path = exports.parse(path) + if (path) { return path.get(obj) } - // else do the traversal +} + +/** + * Get from an object from an array + * + * @param {Object} obj + * @param {Array} path + */ + +exports.getFromArray = function (obj, path) { for (var i = 0, l = path.length; i < l; i++) { if (obj == null) return obj = obj[path[i]] @@ -290,10 +294,28 @@ exports.get = function (obj, path) { } /** - * Set on an object from a path + * Get from an object from an Observer-delimitered path. + * e.g. "a\bb\bc" * * @param {Object} obj * @param {String} path + */ + +exports.getFromObserver = function (obj, path) { + var hit = pathCache.get(path) + if (!hit) { + hit = path.split(Observer.pathDelimiter) + hit.get = compileGetter(hit) + pathCache.put(path, hit) + } + return hit.get(obj) +} + +/** + * Set on an object from a path + * + * @param {Object} obj + * @param {String | Array} path * @param {*} val */ diff --git a/test/unit/specs/path_parser_spec.js b/test/unit/specs/path_parser_spec.js index d2f8aae917a..1e7ae9b7ec6 100644 --- a/test/unit/specs/path_parser_spec.js +++ b/test/unit/specs/path_parser_spec.js @@ -1,4 +1,5 @@ var Path = require('../../../src/parse/path') +var Observer = require('../../../src/observe/observer') function assertPath (str, expected) { var path = Path.parse(str) @@ -72,28 +73,38 @@ describe('Path', function () { expect(path1).toBe(path2) }) - it('compiled getter', function () { - var path = ['a', 'b-$$-c', '0'] + it('get', function () { + var path = 'a[\'b"b"c\'][0]' var obj = { a: { - 'b-$$-c': [12345] + 'b"b"c': [12345] } } - var getter = Path.compileGetter(path) - expect(getter(obj)).toBe(12345) + expect(Path.get(obj, path)).toBe(12345) + expect(Path.get(obj, 'a.c')).toBeUndefined() + }) - var path = Path.parse('a["b-$$-c"][0]') - expect(path.get(obj)).toBe(12345) + it('get from Array', function () { + var path = ['a','b','0'] + var obj = { + a: { + b: [123] + } + } + expect(Path.getFromArray(obj, path)).toBe(123) + expect(Path.getFromArray(obj, ['a','c'])).toBeUndefined() }) - it('get', function () { - var path = 'a[\'b"b"c\'][0]' + it('get from observer delimited path', function () { + var delim = Observer.pathDelimiter + var path = ['a','b','0'].join(delim) var obj = { a: { - 'b"b"c': [12345] + b: [123] } } - expect(Path.get(obj, path)).toBe(12345) + expect(Path.getFromObserver(obj, path)).toBe(123) + expect(Path.getFromObserver(obj, ['a','c'].join(delim))).toBeUndefined() }) it('set success', function () { From 8c52cc93d090bcebf38a3fa5d125e1054fb4727c Mon Sep 17 00:00:00 2001 From: Evan You Date: Sat, 2 Aug 2014 22:24:27 -0400 Subject: [PATCH 0091/1534] rewrite exp parser --- src/parse/expression.js | 116 ++++++++-------------- test/unit/specs/expression_parser_spec.js | 51 +++++++++- 2 files changed, 94 insertions(+), 73 deletions(-) diff --git a/src/parse/expression.js b/src/parse/expression.js index 1f41e19a295..a972ca25cc1 100644 --- a/src/parse/expression.js +++ b/src/parse/expression.js @@ -2,48 +2,13 @@ var _ = require('../util') var Cache = require('../cache') var expressionCache = new Cache(1000) -/** - * Extract all accessor paths from an expression. - * - * @param {String} code - * @return {Array} - extracted paths - */ - -// remove strings and object literal keys that could contain arbitrary chars -var PREPARE_RE = /'[^']*'|"[^"]*"|[\{,]\s*[\w\$_]+\s*:/g -// turn anything that is not valid path char into commas -var CONVERT_RE = /[^\w$\.]+/g -// remove keywords & number literals -var KEYWORDS = 'Math,break,case,catch,continue,debugger,default,delete,do,else,false,finally,for,function,if,in,instanceof,new,null,return,switch,this,throw,true,try,typeof,var,void,while,with,undefined,abstract,boolean,byte,char,class,const,double,enum,export,extends,final,float,goto,implements,import,int,interface,long,native,package,private,protected,public,short,static,super,synchronized,throws,transient,volatile,arguments,let,yield' -var KEYWORDS_RE = new RegExp('\\b' + KEYWORDS.replace(/,/g, '\\b|\\b') + '\\b|\\b\\d[^,]*', 'g') -// remove trailing commas -var COMMA_RE = /^,+|,+$/ -// split by commas -var SPLIT_RE = /,+/ - -function extractPaths (code) { - code = code - .replace(PREPARE_RE, ',') - .replace(CONVERT_RE, ',') - .replace(KEYWORDS_RE, '') - .replace(COMMA_RE, '') - return code - ? code.split(SPLIT_RE) - : [] -} - -/** - * Escape leading dollar signs from paths for regex construction. - * - * @param {String} path - * @return {String} - */ - -function escapeDollar (path) { - return path.charAt(0) === '$' - ? '\\' + path - : path -} +var wsRE = /\s/g +var newlineRE = /\n/g +var saveRE = /[\{,]\s*[\w\$_]+\s*:|'[^']*'|"[^"]*"/g +var restoreRE = /"(\d+)"/g +var pathRE = /[^\w$\.]([A-Za-z_$][\w$]*(\.[A-Za-z_$][\w$]*|\['.*?'\]|\[".*?"\])*)/g +var keywords = 'Math,break,case,catch,continue,debugger,default,delete,do,else,false,finally,for,function,if,in,instanceof,new,null,return,switch,this,throw,true,try,typeof,var,void,while,with,undefined,abstract,boolean,byte,char,class,const,double,enum,export,extends,final,float,goto,implements,import,int,interface,long,native,package,private,protected,public,short,static,super,synchronized,throws,transient,volatile,arguments,let,yield' +var keywordsRE = new RegExp('^(' + keywords.replace(/,/g, '\\b|') + '\\b)') /** * Save / Rewrite / Restore @@ -56,8 +21,8 @@ function escapeDollar (path) { */ var saved = [] -var NEWLINE_RE = /\n/g -var RESTORE_RE = /<%(\d+)%>/g +var paths = [] +var has = null /** * Save replacer @@ -68,19 +33,30 @@ var RESTORE_RE = /<%(\d+)%>/g function save (str) { var i = saved.length - saved[i] = str.replace(NEWLINE_RE, '\\n') - return '<%' + i + '%>' + saved[i] = str.replace(newlineRE, '\\n') + return '"' + i + '"' } /** * Path rewrite replacer * - * @param {String} path + * @param {String} raw * @return {String} */ -function rewrite (path) { - return 'scope.' + path +function rewrite (raw) { + var c = raw.charAt(0) + path = raw.slice(1) + if (keywordsRE.test(path)) { + return raw + } else { + path = path.replace(restoreRE, restore) + if (!has[path]) { + paths.push(path) + has[path] = true + } + return c + 'scope.' + path + } } /** @@ -134,44 +110,40 @@ function makeSetter (body) { /** * Parse an expression and rewrite into a getter/setter functions * - * @param {String} code + * @param {String} exp * @param {Boolean} needSet * @return {Function} */ -exports.parse = function (code, needSet) { +exports.parse = function (exp, needSet) { // try cache - var hit = expressionCache.get(code) + var hit = expressionCache.get(exp) if (hit) { return hit } - // extract paths - var paths = extractPaths(code) - var body = code - // rewrite paths - if (paths.length) { - var pathRE = new RegExp( - '(\\b|\\$)' + - paths.map(escapeDollar).join('|') + - '(\\b|\\$)', - 'g' - ) - saved.length = 0 - body = body // pad for regex - .replace(PREPARE_RE, save) - .replace(pathRE, rewrite) - .replace(RESTORE_RE, restore) - .trim() - } + // reset state + saved.length = 0 + paths.length = 0 + has = Object.create(null) + // save strings and object literal keys + var body = exp + .replace(saveRE, save) + .replace(wsRE, '') + // rewrite all paths + // pad 1 space here becaue the regex matches 1 extra char + body = (' ' + body) + .replace(pathRE, rewrite) + .replace(restoreRE, restore) // generate function var getter = makeGetter(body) if (getter) { + getter.paths = paths.slice() if (needSet) { getter.setter = makeSetter(body) } - expressionCache.put(code, getter) + expressionCache.put(exp, getter) } else { - _.warn('Invalid expression: "' + code + '"\nGenerated function body: ' + body) + _.warn('Invalid expression: "' + exp + '"\nGenerated function body: ' + body) } return getter } \ No newline at end of file diff --git a/test/unit/specs/expression_parser_spec.js b/test/unit/specs/expression_parser_spec.js index 133fb2fb9a2..6c77c51a103 100644 --- a/test/unit/specs/expression_parser_spec.js +++ b/test/unit/specs/expression_parser_spec.js @@ -55,7 +55,7 @@ var testCases = [ }, { // complex with nested values - exp: "todo.title + ' : ' + (todo.done ? 'yep' : 'nope')", + exp: "todo.title + ' : ' + (todo['done'] ? 'yep' : 'nope')", scope: { todo: { title: 'write tests', @@ -89,6 +89,55 @@ var testCases = [ haha: 'hoho' }, expected: 'namehoho123' + }, + { + // space between path segments + exp: ' a . b . c + d', + scope: { + a: { b: { c: 12 }}, + d: 3 + }, + expected: 15 + }, + { + // space in bracket identifiers + exp: ' a[ " a.b.c " ] + b [ \' e \' ]', + scope: { + a: {' a.b.c ': 123}, + b: {' e ': 234} + }, + expected: 357 + }, + { + // number literal + exp: 'a * 1e2 + 1.1', + scope: { + a: 3 + }, + expected: 301.1 + }, + { + //keyowrd + keyword literal + exp: 'true && a.true', + scope: { + a: { 'true': false } + }, + expected: false + }, + { + // super complex + exp: ' $a + b[ " a.b.c " ][\'123\'].$e&&c[ " d " ].e + Math.round(e) ', + scope: { + $a: 1, + b: { + ' a.b.c ': { + '123': { $e: 2 } + } + }, + c: { ' d ': {e: 3}}, + e: 4.5 + }, + expected: 8 } ] From fb1a149109102a217d45102240f1db05872fa98a Mon Sep 17 00:00:00 2001 From: Evan You Date: Sun, 3 Aug 2014 14:19:00 -0400 Subject: [PATCH 0092/1534] working on directive --- src/binding.js | 33 ++++---- src/directive.js | 163 ++++++++++++++++++++++++++++++++++++++- src/instance/bindings.js | 73 ++++++++++++++---- src/instance/init.js | 2 + src/observe/observer.js | 2 +- 5 files changed, 231 insertions(+), 42 deletions(-) diff --git a/src/binding.js b/src/binding.js index e4a021c62c9..aa8b76a5bab 100644 --- a/src/binding.js +++ b/src/binding.js @@ -1,23 +1,16 @@ -/** - * Assign unique id to each binding created so that directives - * can have an easier time avoiding duplicates and refreshing - * dependencies. - */ - -var uid = 0 - /** * A binding is an observable that can have multiple directives * subscribing to it. It can also have multiple other bindings * as children to form a trie-like structure. * + * All binding properties are prefixed with `_` so that they + * don't conflict with children keys. + * * @constructor */ function Binding () { - this.id = uid++ - this.children = Object.create(null) - this.subs = [] + this._subs = [] } var p = Binding.prototype @@ -29,9 +22,9 @@ var p = Binding.prototype * @param {Binding} child */ -p.addChild = function (key, child) { +p._addChild = function (key, child) { child = child || new Binding() - this.children[key] = child + this[key] = child return child } @@ -41,8 +34,8 @@ p.addChild = function (key, child) { * @param {Directive} sub */ -p.addSub = function (sub) { - this.subs.push(sub) +p._addSub = function (sub) { + this._subs.push(sub) } /** @@ -51,17 +44,17 @@ p.addSub = function (sub) { * @param {Directive} sub */ -p.removeSub = function (sub) { - this.subs.splice(this.subs.indexOf(sub), 1) +p._removeSub = function (sub) { + this._subs.splice(this._subs.indexOf(sub), 1) } /** * Notify all subscribers of a new value. */ -p.notify = function () { - for (var i = 0, l = this.subs.length; i < l; i++) { - this.subs[i]._update(this) +p._notify = function () { + for (var i = 0, l = this._subs.length; i < l; i++) { + this._subs[i]._update(this) } } diff --git a/src/directive.js b/src/directive.js index 3f4b403365a..d85510fb541 100644 --- a/src/directive.js +++ b/src/directive.js @@ -1,3 +1,8 @@ +var _ = require('./util') +var Path = require('./parse/path') +var Observer = require('./observe/observer') +var expParser = require('./parse/expression') + /** * A directive links a DOM element with a piece of data, which can * be either simple paths or computed properties. It subscribes to @@ -7,18 +12,168 @@ * @param {String} type * @param {Node} el * @param {Vue} vm - * @param {String} expression + * @param {Object} descriptor + * - {String} arg + * - {String} expression + * - {Array} filters * @constructor */ -function Directive (type, el, vm, expression) { - +function Directive (type, el, vm, descriptor) { + // public + this.type = type + this.el = el + this.vm = vm + this.arg = descriptor.arg + this.expression = descriptor.expression + this.filters = descriptor.filters + this.value = undefined + + // private + this._deps = Object.create(null) + this._newDeps = Object.create(null) + + // TODO + // test for simple path vs. expression + this._getter = expParser.parse(this.expression) + this._setter = this._getter.setter + + var self = this + + // add root level path as a dependency. + // this is specifically for the case where the expression + // references a non-existing root level path, and later + // that path is created with `vm.$add`. + // e.g. "a && a.b" + var paths = this._getter.paths + paths.forEach(function (path) { + if (path.indexOf('.') < 0 && path.indexOf('[') < 0) { + self._addDep(path) + } + }) + this._deps = this._newDeps + + // lock/unlock for setter + this._locked = false + this._unlock = function () { + self._locked = false + } + + // collect initial dependencies + this.get() } var p = Directive.prototype +/** + * Add a binding dependency to this directive. + * + * @param {String} path + */ + +p._addDep = function (path) { + var vm = this.vm + var newDeps = this._newDeps + var oldDeps = this._deps + if (!newDeps[path]) { + newDeps[path] = true + if (!oldDeps[path]) { + var binding = + vm._getBindingAt(path, true) || + vm._createBindingAt(path, true) + binding._addSub(this) + } + } +} + +/** + * Evaluate the getter, and re-collect dependencies. + */ + +p.get = function () { + this._beforeGet() + var value = this._getter.call(this.vm, this.vm.$scope) + if (this.filters) { + value = this._applyFilters(value, -1) + } + this._afterGet() + return value +} + +/** + * Set the corresponding value with the setter. + * This should only be used in two-way bindings like v-model. + * + * @param {*} value + */ + +p.set = function (value) { + if (this._setter) { + this._locked = true + if (this.filters) { + value = this._applyFilters(value, 1) + } + this._setter.call(this.vm, this.vm.$scope, value) + _.nextTick(this._unlock) + } +} + +/** + * Prepare for dependency collection. + */ + +p._beforeGet = function () { + Observer.emitGet = true + this.vm._targetDir = this + this._newDeps = Object.create(null) +} + +/** + * Clean up for dependency collection. + */ + +p._afterGet = function () { + this.vm._targetDir = null + Observer.emitGet = false + _.extend(this._newDeps, this._deps) + this._deps = this._newDeps +} + +/** + * The exposed subscriber interface. + * Will be called when a dependency changes. + */ + p._update = function () { - + this.value = this.get() + console.log('updated! new value: ' + this.value) +} + +/** + * Apply filters to a value. + * + * @param {*} value + * @param {Number} direction - -1 = read, 1 = write. + */ + +p._applyFilters = function (value, direction) { + if (direction < 0) { + // TODO read + return value + } else { + // TODO write + return value + } +} + +/** + * Remove self from all dependencies' subcriber list. + */ + +p._teardown = function () { + for (var p in this._deps) { + this._deps[p]._removeSub(this) + } } module.exports = Directive \ No newline at end of file diff --git a/src/instance/bindings.js b/src/instance/bindings.js index a140e5b9a94..35811ead6af 100644 --- a/src/instance/bindings.js +++ b/src/instance/bindings.js @@ -1,5 +1,6 @@ var Binding = require('../binding') var Path = require('../parse/path') +var Observer = require('../observe/observer') /** * Setup the binding tree. @@ -21,20 +22,23 @@ var Path = require('../parse/path') exports._initBindings = function () { var root = this._rootBinding = new Binding() // the $data binding points to the root itself! - root.addChild('$data', root) + root._addChild('$data', root) // point $parent and $root bindings to their // repective owners. if (this.$parent) { - root.addChild('$parent', this.$parent._rootBinding) - root.addChild('$root', this.$root._rootBinding) + root._addChild('$parent', this.$parent._rootBinding) + root._addChild('$root', this.$root._rootBinding) } - // observer already has callback context set to `this` - var update = this._updateBindingAt + // setup observer events this._observer - .on('set', update) - .on('add', update) - .on('delete', update) - .on('mutate', update) + // simple updates + .on('set', this._updateBindingAt) + .on('mutate', this._updateBindingAt) + .on('delete', this._updateBindingAt) + // adding properties is a bit different + .on('add', this._updateAdd) + // collect dependency + .on('get', this._collectDep) } /** @@ -43,7 +47,8 @@ exports._initBindings = function () { * exist yet along the way. * * @param {String} path - * @param {Boolean} fromObserver + * @param {Boolean} fromObserver - paths coming from the Observer are + * strings of segments delimted by "\b". * @return {Binding|undefined} */ @@ -57,16 +62,21 @@ exports._getBindingAt = function (path, fromObserver) { * Create a binding at a given path. Will also create * all bindings that do not exist yet along the way. * - * @param {Array} path + * @param {String} path + * @param {Boolean} fromObserver * @return {Binding} */ -exports._createBindingAt = function (path) { +exports._createBindingAt = function (path, fromObserver) { + path = fromObserver + ? path.split(Observer.pathDelimiter) + : Path.parse(path) + if (!path) return var b = this._rootBinding var child, key for (var i = 0, l = path.length; i < l; i++) { key = path[i] - child = b.children[key] || b.addChild(key) + child = b[key] || b._addChild(key) b = child } return b @@ -75,14 +85,43 @@ exports._createBindingAt = function (path) { /** * Trigger update for the binding at given path. * - * @param {String} path - this path comes directly from the - * data observer, so it is a single string - * delimited by "\b". + * @param {String} path */ exports._updateBindingAt = function (path) { var binding = this._getBindingAt(path, true) if (binding) { - binding.notify() + binding._notify() + } +} + +/** + * For newly added properties, since its binding has not been + * created yet, directives will not have it as a dependency yet. + * However, they will have its parent as a dependency. Therefore + * here we remove the last segment from the path and notify the + * added property's parent instead. + * + * @param {String} path + */ + +exports._updateAdd = function (path) { + var index = path.lastIndexOf(Observer.pathDelimiter) + if (index > -1) path = path.slice(0, index) + this._updateBindingAt(path) +} + +/** + * Collect dependency for the target directive being evaluated. + * + * @param {String} path + */ + +exports._collectDep = function (path) { + var directive = this._targetDir + // the get event might have come from a child vm's directive + // so this._targetDir is not guarunteed to be defined + if (directive) { + directive._addDep(path) } } \ No newline at end of file diff --git a/src/instance/init.js b/src/instance/init.js index 287b8c0c1a5..3cb08436acc 100644 --- a/src/instance/init.js +++ b/src/instance/init.js @@ -21,6 +21,8 @@ exports._init = function (options) { this._isDestroyed = false this._rawContent = null this._emitter = new Emitter(this) + // the current target directive for dependency collection + this._targetDir = null // setup parent relationship this.$parent = options.parent diff --git a/src/observe/observer.js b/src/observe/observer.js index 107b3ca3112..685d0c3bfee 100644 --- a/src/observe/observer.js +++ b/src/observe/observer.js @@ -198,6 +198,7 @@ p.convert = function (key, val) { set: function (newVal) { if (newVal === val) return ob.unobserve(val) + val = newVal ob.observe(key, newVal) ob.emit('set:self', key, newVal) ob.propagate('set', key, newVal) @@ -206,7 +207,6 @@ p.convert = function (key, val) { key + Observer.pathDelimiter + 'length', newVal.length) } - val = newVal } }) } From 8e710f065c9261de252be1ef93deba33ea441fa5 Mon Sep 17 00:00:00 2001 From: Evan You Date: Sun, 3 Aug 2014 14:50:57 -0400 Subject: [PATCH 0093/1534] add batcher integration to directives --- src/directive.js | 50 ++++++++++++++++++++++++++++++++++++++++++------ 1 file changed, 44 insertions(+), 6 deletions(-) diff --git a/src/directive.js b/src/directive.js index d85510fb541..726ab725caa 100644 --- a/src/directive.js +++ b/src/directive.js @@ -2,6 +2,10 @@ var _ = require('./util') var Path = require('./parse/path') var Observer = require('./observe/observer') var expParser = require('./parse/expression') +var Batcher = require('./batcher') + +var batcher = new Batcher() +var uid = 0 /** * A directive links a DOM element with a piece of data, which can @@ -29,7 +33,13 @@ function Directive (type, el, vm, descriptor) { this.filters = descriptor.filters this.value = undefined + // TODO + // mixin type definition + // private + this._id = ++uid + this._locked = false + this._unbound = false this._deps = Object.create(null) this._newDeps = Object.create(null) @@ -53,14 +63,39 @@ function Directive (type, el, vm, descriptor) { }) this._deps = this._newDeps - // lock/unlock for setter - this._locked = false + /** + * Unlock function used in .set() + */ + this._unlock = function () { self._locked = false } - // collect initial dependencies - this.get() + /** + * real updater with bound context + * to be pushed into batcher queue + * + * @param {Boolean} init + */ + + this._realUpdate = function (init) { + if (self._unbound) { + return + } + var value = self.get() + if ( + (typeof value === 'object' && value !== null) || + value !== self.value || + init + ) { + self.value = value + // TODO call definition update + console.log('updated! new value: ' + value) + } + } + + // update for the first time + this._realUpdate(true) } var p = Directive.prototype @@ -145,8 +180,10 @@ p._afterGet = function () { */ p._update = function () { - this.value = this.get() - console.log('updated! new value: ' + this.value) + batcher.push({ + id: this._id, + execute: this._realUpdate + }) } /** @@ -171,6 +208,7 @@ p._applyFilters = function (value, direction) { */ p._teardown = function () { + this._unbound = true for (var p in this._deps) { this._deps[p]._removeSub(this) } From 395e8e8e1d68759d210102f2d4f5288b09a19f24 Mon Sep 17 00:00:00 2001 From: Evan You Date: Sun, 3 Aug 2014 14:56:28 -0400 Subject: [PATCH 0094/1534] remove explorations --- explorations/inheritance.js | 78 ------------------------------------- explorations/test.html | 1 - 2 files changed, 79 deletions(-) delete mode 100644 explorations/inheritance.js delete mode 100644 explorations/test.html diff --git a/explorations/inheritance.js b/explorations/inheritance.js deleted file mode 100644 index 9c9c937c962..00000000000 --- a/explorations/inheritance.js +++ /dev/null @@ -1,78 +0,0 @@ -var Vue = require('../src/vue') - -window.model = { - a: 'parent a', - b: 'parent b', - c: { - d: 3 - }, - arr: [{a: 'item a'}], - go: function () { - console.log(this.a) - } -} - -window.vm = new Vue({ - data: model -}) - -window.child = new Vue({ - parent: vm, - data: { - a: 'child a', - change: function () { - this.c.d = 4 - this.b = 456 // Unlike Angular, setting primitive values in Vue WILL affect outer scope, - // unless you overwrite it in the instantiation data! - } - } -}) - -window.item = new Vue({ - parent: vm, - syncData: true, - data: vm.arr[0] -}) - -vm._observer.on('set', function (key, val) { - console.log('vm set:' + key.replace(/[\b]/g, '.'), val) -}) - -child._observer.on('set', function (key, val) { - console.log('child set:' + key.replace(/[\b]/g, '.'), val) -}) - -item._observer.on('set', function (key, val) { - console.log('item set:' + key.replace(/[\b]/g, '.'), val) -}) - -// TODO turn these into tests - -console.log(vm.a) // 'parent a' - -console.log(child.a) // 'child a' -console.log(child.$scope.a) // 'child a' -console.log(child.b) // undefined -console.log(child.$scope.b) // 'parent b' - -console.log(item.a) // 'item a' -console.log(item.$scope.a) // 'item a' -console.log(item.b) // undefined -console.log(item.$scope.b) // 'parent b' - -// set shadowed parent property -vm.a = 'haha!' // vm set:a haha! - -// set shadowed child property -child.a = 'hmm' // child set:a hmm - -// test parent scope change downward propagation -vm.b = 'hoho!' // child set:b hoho! - // item set:b hoho! - // vm set:b hoho! - -// set child owning an array item -item.a = 'wow' // child set:arr.0.a wow - // item set:arr.0.a wow - // vm set:arr.0.a wow - // item set:a wow \ No newline at end of file diff --git a/explorations/test.html b/explorations/test.html deleted file mode 100644 index 16f50060a09..00000000000 --- a/explorations/test.html +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file From 6fba91895d46461b2b74a89a0592f4a9761fa0e9 Mon Sep 17 00:00:00 2001 From: Evan You Date: Sun, 3 Aug 2014 17:23:55 -0400 Subject: [PATCH 0095/1534] directive filters --- changes.md | 12 ++++ src/directive.js | 160 ++++++++++++++++++++++++++++++++--------- src/parse/directive.js | 11 +++ 3 files changed, 151 insertions(+), 32 deletions(-) diff --git a/changes.md b/changes.md index 886c7fb8590..7e89f3bc837 100644 --- a/changes.md +++ b/changes.md @@ -67,6 +67,18 @@ computed: { } ``` +## Directive changes + +### New options + +- `literal`: replaces old options `isLiteral` and `isEmpty`. +- `twoway`: indicates the directive is two-way and may write back to the model. Allows the use of `directive.set(value)`. +- `paramAttributes`: an Array of attribute names to extract as parameters for the directive. For example, given the option value `['lazy']` and markup ``, you can access `this.params['my-param']` with value `'123'` inside directive functions. + +### Removed options: `isLiteral`, `isEmpty`, `isFn` + +- `isFn` is no longer necessary for directives expecting function values. + ## Two Way filters If a filter is defined as a function, it is treated as a read filter by default - i.e. it is applied when data is read from the model and applied to the DOM. You can now specify write filters as well, which are applied when writing to the model, triggered by user input. Write filters are only triggered on two-way bindings like `v-model`. diff --git a/src/directive.js b/src/directive.js index 726ab725caa..424c0578032 100644 --- a/src/directive.js +++ b/src/directive.js @@ -17,24 +17,28 @@ var uid = 0 * @param {Node} el * @param {Vue} vm * @param {Object} descriptor - * - {String} arg * - {String} expression - * - {Array} filters + * - {String} [arg] + * - {Array} [filters] + * @param {Object|Function} definition + * - {Function} update + * - {Function} [bind] + * - {Function} [unbind] + * - {Boolean} [literal] + * - {Boolean} [twoway] + * - {Array} [params] * @constructor */ -function Directive (type, el, vm, descriptor) { +function Directive (type, el, vm, descriptor, definition) { // public this.type = type this.el = el this.vm = vm + this.value = undefined this.arg = descriptor.arg - this.expression = descriptor.expression + this.expression = descriptor.expression.trim() this.filters = descriptor.filters - this.value = undefined - - // TODO - // mixin type definition // private this._id = ++uid @@ -43,18 +47,82 @@ function Directive (type, el, vm, descriptor) { this._deps = Object.create(null) this._newDeps = Object.create(null) - // TODO - // test for simple path vs. expression - this._getter = expParser.parse(this.expression) - this._setter = this._getter.setter + // init definition + this._initDef(definition) + + if (this.expression && !this.isLiteral) { + // TODO + // test for simple path vs. expression + this._getter = expParser.parse(this.expression, this.twoway) + this._setter = this._getter.setter + + // init filters + this._initFilters() + // init dependencies + this._initDeps() + // init methods that need to be context-bound + this._initBoundMethods() + // update for the first time + this._realUpdate(true) + } +} + +var p = Directive.prototype + +/** + * Initialize read and write filters + */ +p._initFilters = function () { + if (!this.filters) { + return + } var self = this + var vm = this.vm + var registry = vm.$options.filters + this.filters.forEach(function (f) { + var def = registry[f.name] + var args = f.args + var read, write + if (typeof def === 'function') { + read = def + } else { + read = def.read + write = def.write + } + if (read) { + if (!self._readFilters) { + self._readFilters = [] + } + self._readFilters.push(function (value) { + return args + ? read.apply(vm, [value].concat(args)) + : read.call(vm, value) + }) + } + if (write) { + if (!self._writeFilters) { + self._writeFilters = [] + } + self._writeFilters.push(function (value) { + return args + ? write.apply(vm, [value, self.value].concat(args)) + : write.call(vm, value, self.value) + }) + } + }) +} + +/** + * Add root level path as a dependency. + * this is specifically for the case where the expression + * references a non-existing root level path, and later + * that path is created with `vm.$add`. + * e.g. "a && a.b" + */ - // add root level path as a dependency. - // this is specifically for the case where the expression - // references a non-existing root level path, and later - // that path is created with `vm.$add`. - // e.g. "a && a.b" +p._initDeps = function () { + var self = this var paths = this._getter.paths paths.forEach(function (path) { if (path.indexOf('.') < 0 && path.indexOf('[') < 0) { @@ -62,6 +130,37 @@ function Directive (type, el, vm, descriptor) { } }) this._deps = this._newDeps +} + +/** + * Initialize the directive instance's definition. + */ + +p._initDef = function (definition) { + _.extend(this, definition) + // init params + var el = this.el + var attrs = this.paramAttributes + if (attrs) { + var params = this.params = {} + attrs.forEach(function (p) { + params[p] = el.getAttribute(p) + el.removeAttribute(p) + }) + } + // call bind hook + if (this.bind) { + this.bind() + } +} + +/** + * Initialize a few methods that need to be context-bound + * so we don't have to create them ad-hoc everytime + */ + +p._initBoundMethods = function () { + var self = this /** * Unlock function used in .set() @@ -89,17 +188,13 @@ function Directive (type, el, vm, descriptor) { init ) { self.value = value - // TODO call definition update - console.log('updated! new value: ' + value) + if (self.update) { + self.update(value) + } } } - - // update for the first time - this._realUpdate(true) } -var p = Directive.prototype - /** * Add a binding dependency to this directive. * @@ -128,7 +223,7 @@ p._addDep = function (path) { p.get = function () { this._beforeGet() var value = this._getter.call(this.vm, this.vm.$scope) - if (this.filters) { + if (this._readFilters) { value = this._applyFilters(value, -1) } this._afterGet() @@ -145,7 +240,7 @@ p.get = function () { p.set = function (value) { if (this._setter) { this._locked = true - if (this.filters) { + if (this._writeFilters) { value = this._applyFilters(value, 1) } this._setter.call(this.vm, this.vm.$scope, value) @@ -194,13 +289,13 @@ p._update = function () { */ p._applyFilters = function (value, direction) { - if (direction < 0) { - // TODO read - return value - } else { - // TODO write - return value + var filters = direction > 0 + ? this._writeFilters + : this._readFilters + for (var i = 0, l = filters.length; i < l; i++) { + value = filters[i](value) } + return value } /** @@ -208,6 +303,7 @@ p._applyFilters = function (value, direction) { */ p._teardown = function () { + if (this.unbind) this.unbind() this._unbound = true for (var p in this._deps) { this._deps[p]._removeSub(this) diff --git a/src/parse/directive.js b/src/parse/directive.js index f99799c5013..309eaa5ab8a 100644 --- a/src/parse/directive.js +++ b/src/parse/directive.js @@ -60,6 +60,17 @@ function pushFilter () { * Parse a directive string into an Array of AST-like objects * representing directives. * + * Example: + * + * "click: a = a + 1 | uppercase" will yield: + * { + * arg: 'click', + * expression: 'a = a + 1', + * filters: [ + * { name: 'uppercase', args: null } + * ] + * } + * * @param {String} str * @return {Array} */ From e5273ab6da10b55d3b2a351b0f604a5c639376d6 Mon Sep 17 00:00:00 2001 From: Evan You Date: Sun, 3 Aug 2014 17:26:29 -0400 Subject: [PATCH 0096/1534] typos --- changes.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/changes.md b/changes.md index 7e89f3bc837..2da9fc5bb42 100644 --- a/changes.md +++ b/changes.md @@ -72,8 +72,8 @@ computed: { ### New options - `literal`: replaces old options `isLiteral` and `isEmpty`. -- `twoway`: indicates the directive is two-way and may write back to the model. Allows the use of `directive.set(value)`. -- `paramAttributes`: an Array of attribute names to extract as parameters for the directive. For example, given the option value `['lazy']` and markup ``, you can access `this.params['my-param']` with value `'123'` inside directive functions. +- `twoway`: indicates the directive is two-way and may write back to the model. Allows the use of `this.set(value)` inside directive functions. +- `paramAttributes`: an Array of attribute names to extract as parameters for the directive. For example, given the option value `['my-param']` and markup ``, you can access `this.params['my-param']` with value `'123'` inside directive functions. ### Removed options: `isLiteral`, `isEmpty`, `isFn` From abf6bda8302d97b23c9a52439b3d0031de51f5e9 Mon Sep 17 00:00:00 2001 From: Evan You Date: Mon, 4 Aug 2014 14:02:18 -0400 Subject: [PATCH 0097/1534] optimize expression parser for simple path scenarios --- src/directive.js | 58 +++++----- src/instance/bindings.js | 16 +-- src/instance/scope.js | 1 - src/parse/expression.js | 125 ++++++++++++++++------ src/parse/path.js | 32 +++--- src/util/env.js | 2 - test/unit/specs/expression_parser_spec.js | 31 +++++- 7 files changed, 166 insertions(+), 99 deletions(-) diff --git a/src/directive.js b/src/directive.js index 424c0578032..e3e87b2a8d3 100644 --- a/src/directive.js +++ b/src/directive.js @@ -1,5 +1,4 @@ var _ = require('./util') -var Path = require('./parse/path') var Observer = require('./observe/observer') var expParser = require('./parse/expression') var Batcher = require('./batcher') @@ -69,6 +68,28 @@ function Directive (type, el, vm, descriptor, definition) { var p = Directive.prototype +/** + * Initialize the directive instance's definition. + */ + +p._initDef = function (definition) { + _.extend(this, definition) + // init params + var el = this.el + var attrs = this.paramAttributes + if (attrs) { + var params = this.params = {} + attrs.forEach(function (p) { + params[p] = el.getAttribute(p) + el.removeAttribute(p) + }) + } + // call bind hook + if (this.bind) { + this.bind() + } +} + /** * Initialize read and write filters */ @@ -118,42 +139,21 @@ p._initFilters = function () { * this is specifically for the case where the expression * references a non-existing root level path, and later * that path is created with `vm.$add`. - * e.g. "a && a.b" + * + * e.g. in "a && a.b", if `a` is not present at compilation, + * the directive will end up with no dependency at all and + * never gets updated. */ p._initDeps = function () { var self = this var paths = this._getter.paths paths.forEach(function (path) { - if (path.indexOf('.') < 0 && path.indexOf('[') < 0) { - self._addDep(path) - } + self._addDep(path) }) this._deps = this._newDeps } -/** - * Initialize the directive instance's definition. - */ - -p._initDef = function (definition) { - _.extend(this, definition) - // init params - var el = this.el - var attrs = this.paramAttributes - if (attrs) { - var params = this.params = {} - attrs.forEach(function (p) { - params[p] = el.getAttribute(p) - el.removeAttribute(p) - }) - } - // call bind hook - if (this.bind) { - this.bind() - } -} - /** * Initialize a few methods that need to be context-bound * so we don't have to create them ad-hoc everytime @@ -209,8 +209,8 @@ p._addDep = function (path) { newDeps[path] = true if (!oldDeps[path]) { var binding = - vm._getBindingAt(path, true) || - vm._createBindingAt(path, true) + vm._getBindingAt(path) || + vm._createBindingAt(path) binding._addSub(this) } } diff --git a/src/instance/bindings.js b/src/instance/bindings.js index 35811ead6af..5abd5695c4e 100644 --- a/src/instance/bindings.js +++ b/src/instance/bindings.js @@ -47,15 +47,11 @@ exports._initBindings = function () { * exist yet along the way. * * @param {String} path - * @param {Boolean} fromObserver - paths coming from the Observer are - * strings of segments delimted by "\b". * @return {Binding|undefined} */ -exports._getBindingAt = function (path, fromObserver) { - return fromObserver - ? Path.getFromObserver(this._rootBinding, path) - : Path.get(this._rootBinding, path) +exports._getBindingAt = function (path) { + return Path.getFromObserver(this._rootBinding, path) } /** @@ -63,15 +59,11 @@ exports._getBindingAt = function (path, fromObserver) { * all bindings that do not exist yet along the way. * * @param {String} path - * @param {Boolean} fromObserver * @return {Binding} */ -exports._createBindingAt = function (path, fromObserver) { - path = fromObserver - ? path.split(Observer.pathDelimiter) - : Path.parse(path) - if (!path) return +exports._createBindingAt = function (path) { + path = path.split(Observer.pathDelimiter) var b = this._rootBinding var child, key for (var i = 0, l = path.length; i < l; i++) { diff --git a/src/instance/scope.js b/src/instance/scope.js index 89c093f3317..d9afbe48259 100644 --- a/src/instance/scope.js +++ b/src/instance/scope.js @@ -135,7 +135,6 @@ exports._teardownData = function () { */ exports._initProxy = function () { - var options = this.$options var scope = this.$scope // scope --> vm diff --git a/src/parse/expression.js b/src/parse/expression.js index a972ca25cc1..d7013106b9b 100644 --- a/src/parse/expression.js +++ b/src/parse/expression.js @@ -1,4 +1,5 @@ var _ = require('../util') +var Path = require('./path') var Cache = require('../cache') var expressionCache = new Cache(1000) @@ -6,9 +7,14 @@ var wsRE = /\s/g var newlineRE = /\n/g var saveRE = /[\{,]\s*[\w\$_]+\s*:|'[^']*'|"[^"]*"/g var restoreRE = /"(\d+)"/g -var pathRE = /[^\w$\.]([A-Za-z_$][\w$]*(\.[A-Za-z_$][\w$]*|\['.*?'\]|\[".*?"\])*)/g +var pathTestRE = /^[A-Za-z_$][\w$]*(\.[A-Za-z_$][\w$]*|\['.*?'\]|\[".*?"\])*$/ +var pathReplaceRE = /[^\w$\.]([A-Za-z_$][\w$]*(\.[A-Za-z_$][\w$]*|\['.*?'\]|\[".*?"\])*)/g var keywords = 'Math,break,case,catch,continue,debugger,default,delete,do,else,false,finally,for,function,if,in,instanceof,new,null,return,switch,this,throw,true,try,typeof,var,void,while,with,undefined,abstract,boolean,byte,char,class,const,double,enum,export,extends,final,float,goto,implements,import,int,interface,long,native,package,private,protected,public,short,static,super,synchronized,throws,transient,volatile,arguments,let,yield' var keywordsRE = new RegExp('^(' + keywords.replace(/,/g, '\\b|') + '\\b)') +// note the following two regexes are only used on valid paths +// so no need to exclude number for first char +var rootPathRE = /^[\w$]+/ +var rootPathTestRE = /^[\w$]+$/ /** * Save / Rewrite / Restore @@ -46,13 +52,20 @@ function save (str) { function rewrite (raw) { var c = raw.charAt(0) - path = raw.slice(1) + var path = raw.slice(1) if (keywordsRE.test(path)) { return raw } else { - path = path.replace(restoreRE, restore) + path = path.indexOf('"') > -1 + ? path.replace(restoreRE, restore) + : path if (!has[path]) { - paths.push(path) + // we store root level paths e.g. "a" + // so that the owner directive can add + // them as default dependencies. + if (rootPathTestRE.test(path)) { + paths.push(path) + } has[path] = true } return c + 'scope.' + path @@ -71,6 +84,62 @@ function restore (str, i) { return saved[i] } +/** + * Rewrite an expression, prefixing all path accessors with `scope.` + * and generate getter/setter functions. + * + * @param {String} exp + * @return {Function} + */ + +function compileExpFns (exp) { + // reset state + saved.length = 0 + paths = [] + has = Object.create(null) + // save strings and object literal keys + var body = exp + .replace(saveRE, save) + .replace(wsRE, '') + // rewrite all paths + // pad 1 space here becaue the regex matches 1 extra char + body = (' ' + body) + .replace(pathReplaceRE, rewrite) + .replace(restoreRE, restore) + var getter = makeGetter(exp, body) + if (getter) { + getter.paths = paths + getter.setter = makeSetter(body) + } + return getter +} + +/** + * Compile getter setters for a simple path. + * + * @param {String} exp + * @return {Function} + */ + +function compilePathFns (exp) { + var getter, path + if (exp.indexOf('[') < 0) { + // really simple path + path = exp.split('.') + getter = Path.compileGetter(path) + } else { + // do the real parsing + path = Path.parse(path) + getter = path.get + } + // save root path segment + getter.paths = [exp.match(rootPathRE)[0]] + getter.setter = function (obj, val) { + Path.set(obj, path, val) + } + return getter +} + /** * Build a getter function. Requires eval. * @@ -81,10 +150,12 @@ function restore (str, i) { * @return {Function|undefined} */ -function makeGetter (body) { +function makeGetter (exp, body) { try { return new Function('scope', 'return ' + body + ';') - } catch (e) {} + } catch (e) { + _.warn('Invalid expression: "' + exp + '\nGenerated function body: ' + body) + } } /** @@ -93,6 +164,9 @@ function makeGetter (body) { * This is only needed in rare situations like "a[b]" where * a settable path requires dynamic evaluation. * + * Not doing try-catch here because this only gets called + * if makeGetter() worked. + * * This setter function may throw error when called if the * expression body is not a valid left-hand expression in * assignment. @@ -102,48 +176,29 @@ function makeGetter (body) { */ function makeSetter (body) { - try { - return new Function('scope', 'value', body + ' = value;') - } catch (e) {} + return new Function('scope', 'value', body + ' = value;') } /** * Parse an expression and rewrite into a getter/setter functions * * @param {String} exp - * @param {Boolean} needSet * @return {Function} */ -exports.parse = function (exp, needSet) { +exports.parse = function (exp) { // try cache var hit = expressionCache.get(exp) if (hit) { return hit } - // reset state - saved.length = 0 - paths.length = 0 - has = Object.create(null) - // save strings and object literal keys - var body = exp - .replace(saveRE, save) - .replace(wsRE, '') - // rewrite all paths - // pad 1 space here becaue the regex matches 1 extra char - body = (' ' + body) - .replace(pathRE, rewrite) - .replace(restoreRE, restore) - // generate function - var getter = makeGetter(body) - if (getter) { - getter.paths = paths.slice() - if (needSet) { - getter.setter = makeSetter(body) - } - expressionCache.put(exp, getter) - } else { - _.warn('Invalid expression: "' + exp + '"\nGenerated function body: ' + body) - } + exp = exp.trim() + // we do a simple path check to optimize for that scenario. + // the check fails valid paths with unusal whitespaces, but + // that's too rare and we don't care. + var getter = pathTestRE.test(exp) + ? compilePathFns(exp) + : compileExpFns(exp) + expressionCache.put(exp, getter) return getter } \ No newline at end of file diff --git a/src/parse/path.js b/src/parse/path.js index 43376bb276b..f9f95447873 100644 --- a/src/parse/path.js +++ b/src/parse/path.js @@ -1,9 +1,7 @@ var Cache = require('../cache') var pathCache = new Cache(1000) var Observer = require('../observe/observer') -var identStart = '[$_a-zA-Z]' -var identPart = '[$_a-zA-Z0-9]' -var IDENT_RE = new RegExp('^' + identStart + '+' + identPart + '*' + '$') +var identRE = /^[$_a-zA-Z]+[\w$]*$/ /** * Path-parsing algorithm scooped from Polymer/observe-js @@ -204,33 +202,31 @@ function parsePath (path) { return // parse error } -function isIndex(s) { - return +s === s >>> 0; -} - -function isIdent(s) { - return IDENT_RE.test(s); -} +/** + * Format a accessor segment based on its type. + * + * @param {String} key + * @return {Boolean} + */ function formatAccessor(key) { - if (isIdent(key)) { + if (identRE.test(key)) { // identifier return '.' + key - } else if (isIndex(key)) { + } else if (+key === key >>> 0) { // bracket index return '[' + key + ']'; - } else { + } else { // bracket string return '["' + key.replace(/"/g, '\\"') + '"]'; } } /** - * Compiles a getter function with a set path, which - * is much more efficient than the dynamic path getter. + * Compiles a getter function with a fixed path. * * @param {Array} path * @return {Function} */ -function compileGetter (path) { +exports.compileGetter = function (path) { var body = 'if (o != null' var pathString = 'o' var key @@ -257,7 +253,7 @@ exports.parse = function (path) { if (!hit) { hit = parsePath(path) if (hit) { - hit.get = compileGetter(hit) + hit.get = exports.compileGetter(hit) pathCache.put(path, hit) } } @@ -305,7 +301,7 @@ exports.getFromObserver = function (obj, path) { var hit = pathCache.get(path) if (!hit) { hit = path.split(Observer.pathDelimiter) - hit.get = compileGetter(hit) + hit.get = exports.compileGetter(hit) pathCache.put(path, hit) } return hit.get(obj) diff --git a/src/util/env.js b/src/util/env.js index e8413738136..d8f0a8847a3 100644 --- a/src/util/env.js +++ b/src/util/env.js @@ -1,5 +1,3 @@ -/* global chrome */ - /** * Are we in a browser or in Node? * Calling toString on window has inconsistent results in browsers diff --git a/test/unit/specs/expression_parser_spec.js b/test/unit/specs/expression_parser_spec.js index 6c77c51a103..43839294323 100644 --- a/test/unit/specs/expression_parser_spec.js +++ b/test/unit/specs/expression_parser_spec.js @@ -6,6 +6,22 @@ function assertExp (testCase) { } var testCases = [ + { + // simple path that doesn't exist + exp: 'a.b.c', + scope: { + a: {} + }, + expected: undefined + }, + { + // simple path that exists + exp: 'a.b.d', + scope: { + a:{b:{d:123}} + }, + expected: 123 + }, { // string concat exp: 'a+b', @@ -147,8 +163,8 @@ describe('Expression Parser', function () { testCases.forEach(assertExp) }) - it('parse setter', function () { - var setter = expParser.parse('a[b]', true).setter + it('dynamic setter', function () { + var setter = expParser.parse('a[b]').setter var scope = { a: { c: 1 }, b: 'c' @@ -157,6 +173,17 @@ describe('Expression Parser', function () { expect(scope.a.c).toBe(2) }) + it('simple path setter', function () { + var setter = expParser.parse('a.b.c').setter + var scope = {} + expect(function () { + setter(scope, 123) + }).not.toThrow() + scope.a = {b:{c:0}} + setter(scope, 123) + expect(scope.a.b.c).toBe(123) + }) + it('cache', function () { var fn1 = expParser.parse('a + b') var fn2 = expParser.parse('a + b') From 2f9021e6c11df55951f02b62122cbec9169625c5 Mon Sep 17 00:00:00 2001 From: Evan You Date: Mon, 4 Aug 2014 14:12:11 -0400 Subject: [PATCH 0098/1534] fix complex path for expression --- src/parse/expression.js | 2 +- test/unit/specs/expression_parser_spec.js | 8 ++++++++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/src/parse/expression.js b/src/parse/expression.js index d7013106b9b..7f9d312ca39 100644 --- a/src/parse/expression.js +++ b/src/parse/expression.js @@ -129,7 +129,7 @@ function compilePathFns (exp) { getter = Path.compileGetter(path) } else { // do the real parsing - path = Path.parse(path) + path = Path.parse(exp) getter = path.get } // save root path segment diff --git a/test/unit/specs/expression_parser_spec.js b/test/unit/specs/expression_parser_spec.js index 43839294323..af43ad9afc2 100644 --- a/test/unit/specs/expression_parser_spec.js +++ b/test/unit/specs/expression_parser_spec.js @@ -22,6 +22,14 @@ var testCases = [ }, expected: 123 }, + // complex path + { + exp: 'a["b"].c', + scope: { + a:{b:{c:234}} + }, + expected: 234 + }, { // string concat exp: 'a+b', From 23fe83cf45c48411bed3a422a36ccdce3892e390 Mon Sep 17 00:00:00 2001 From: Evan You Date: Mon, 4 Aug 2014 14:34:11 -0400 Subject: [PATCH 0099/1534] fix expression parser setter --- benchmarks/bench.js | 4 ++- benchmarks/expression.js | 35 ++++++++++++++++++ src/parse/expression.js | 44 +++++++++++++++++------ test/unit/specs/expression_parser_spec.js | 4 +-- 4 files changed, 73 insertions(+), 14 deletions(-) create mode 100644 benchmarks/expression.js diff --git a/benchmarks/bench.js b/benchmarks/bench.js index 8c509f9af67..e9915ff4eb3 100644 --- a/benchmarks/bench.js +++ b/benchmarks/bench.js @@ -1,3 +1,5 @@ require('./observer').run(function () { - require('./instantiation').run() + require('./instantiation').run(function () { + require('./expression') + }) }) \ No newline at end of file diff --git a/benchmarks/expression.js b/benchmarks/expression.js new file mode 100644 index 00000000000..d2ba58249d0 --- /dev/null +++ b/benchmarks/expression.js @@ -0,0 +1,35 @@ +console.log('\Expression Parser\n') + +var Cache = require('../src/cache') +var parse = require('../src/parse/expression').parse + +Cache.prototype.get = Cache.prototype.put = function () {} + +function bench (id, fn) { + var s = Date.now() + var max = i = 10000 + while (i--) { + fn() + } + var used = Date.now() - s + var ops = Math.round(16 / (used / max)) + console.log(id + ': ' + ops + ' ops/frame') +} + +var side + +bench('simple path', function () { + side = parse('a.b.c') +}) + +bench('complex path', function () { + side = parse('a["b"].c') +}) + +bench('simple exp', function () { + side = parse('a.b + c') +}) + +bench('complex exp', function () { + side = parse('a.b + c') +}) \ No newline at end of file diff --git a/src/parse/expression.js b/src/parse/expression.js index 7f9d312ca39..fe51a52825b 100644 --- a/src/parse/expression.js +++ b/src/parse/expression.js @@ -89,10 +89,11 @@ function restore (str, i) { * and generate getter/setter functions. * * @param {String} exp + * @param {Boolean} needSet * @return {Function} */ -function compileExpFns (exp) { +function compileExpFns (exp, needSet) { // reset state saved.length = 0 paths = [] @@ -106,10 +107,13 @@ function compileExpFns (exp) { body = (' ' + body) .replace(pathReplaceRE, rewrite) .replace(restoreRE, restore) - var getter = makeGetter(exp, body) + var getter = makeGetter(body) if (getter) { + getter.body = body getter.paths = paths - getter.setter = makeSetter(body) + if (needSet) { + getter.setter = makeSetter(body) + } } return getter } @@ -134,6 +138,7 @@ function compilePathFns (exp) { } // save root path segment getter.paths = [exp.match(rootPathRE)[0]] + // always generate setter for simple paths getter.setter = function (obj, val) { Path.set(obj, path, val) } @@ -150,11 +155,11 @@ function compilePathFns (exp) { * @return {Function|undefined} */ -function makeGetter (exp, body) { +function makeGetter (body) { try { return new Function('scope', 'return ' + body + ';') } catch (e) { - _.warn('Invalid expression: "' + exp + '\nGenerated function body: ' + body) + _.warn('Invalid expression. Generated function body: ' + body) } } @@ -164,9 +169,6 @@ function makeGetter (exp, body) { * This is only needed in rare situations like "a[b]" where * a settable path requires dynamic evaluation. * - * Not doing try-catch here because this only gets called - * if makeGetter() worked. - * * This setter function may throw error when called if the * expression body is not a valid left-hand expression in * assignment. @@ -176,20 +178,40 @@ function makeGetter (exp, body) { */ function makeSetter (body) { - return new Function('scope', 'value', body + ' = value;') + try { + return new Function('scope', 'value', body + ' = value;') + } catch (e) { + _.warn('Invalid setter function body: ' + body) + } +} + +/** + * Check for setter existence on a cache hit. + * + * @param {Function} fn + */ + +function checkSetter (fn) { + if (!fn.setter) { + fn.setter = makeSetter(fn.body) + } } /** * Parse an expression and rewrite into a getter/setter functions * * @param {String} exp + * @param {Boolean} needSet * @return {Function} */ -exports.parse = function (exp) { +exports.parse = function (exp, needSet) { // try cache var hit = expressionCache.get(exp) if (hit) { + if (needSet) { + checkSetter(hit) + } return hit } exp = exp.trim() @@ -198,7 +220,7 @@ exports.parse = function (exp) { // that's too rare and we don't care. var getter = pathTestRE.test(exp) ? compilePathFns(exp) - : compileExpFns(exp) + : compileExpFns(exp, needSet) expressionCache.put(exp, getter) return getter } \ No newline at end of file diff --git a/test/unit/specs/expression_parser_spec.js b/test/unit/specs/expression_parser_spec.js index af43ad9afc2..0fac5096a5c 100644 --- a/test/unit/specs/expression_parser_spec.js +++ b/test/unit/specs/expression_parser_spec.js @@ -172,7 +172,7 @@ describe('Expression Parser', function () { }) it('dynamic setter', function () { - var setter = expParser.parse('a[b]').setter + var setter = expParser.parse('a[b]', true).setter var scope = { a: { c: 1 }, b: 'c' @@ -182,7 +182,7 @@ describe('Expression Parser', function () { }) it('simple path setter', function () { - var setter = expParser.parse('a.b.c').setter + var setter = expParser.parse('a.b.c', true).setter var scope = {} expect(function () { setter(scope, 123) From 8600488609145c052f1cda6c87f49d6e0a0223f9 Mon Sep 17 00:00:00 2001 From: Evan You Date: Mon, 4 Aug 2014 22:45:06 -0400 Subject: [PATCH 0100/1534] add test for expression parser path extraction --- src/parse/expression.js | 19 ++++---- test/unit/specs/expression_parser_spec.js | 55 ++++++++++++++++------- 2 files changed, 47 insertions(+), 27 deletions(-) diff --git a/src/parse/expression.js b/src/parse/expression.js index fe51a52825b..48f5d2999b1 100644 --- a/src/parse/expression.js +++ b/src/parse/expression.js @@ -11,10 +11,9 @@ var pathTestRE = /^[A-Za-z_$][\w$]*(\.[A-Za-z_$][\w$]*|\['.*?'\]|\[".*?"\])*$/ var pathReplaceRE = /[^\w$\.]([A-Za-z_$][\w$]*(\.[A-Za-z_$][\w$]*|\['.*?'\]|\[".*?"\])*)/g var keywords = 'Math,break,case,catch,continue,debugger,default,delete,do,else,false,finally,for,function,if,in,instanceof,new,null,return,switch,this,throw,true,try,typeof,var,void,while,with,undefined,abstract,boolean,byte,char,class,const,double,enum,export,extends,final,float,goto,implements,import,int,interface,long,native,package,private,protected,public,short,static,super,synchronized,throws,transient,volatile,arguments,let,yield' var keywordsRE = new RegExp('^(' + keywords.replace(/,/g, '\\b|') + '\\b)') -// note the following two regexes are only used on valid paths +// note the following regex is only used on valid paths // so no need to exclude number for first char var rootPathRE = /^[\w$]+/ -var rootPathTestRE = /^[\w$]+$/ /** * Save / Rewrite / Restore @@ -59,14 +58,14 @@ function rewrite (raw) { path = path.indexOf('"') > -1 ? path.replace(restoreRE, restore) : path - if (!has[path]) { - // we store root level paths e.g. "a" - // so that the owner directive can add - // them as default dependencies. - if (rootPathTestRE.test(path)) { - paths.push(path) - } - has[path] = true + // we store root level paths e.g. "a" + // so that the owner directive can add + // them as default dependencies. + var match = path.match(rootPathRE) + var rootPath = match && match[0] + if (rootPath && !has[rootPath]) { + paths.push(rootPath) + has[rootPath] = true } return c + 'scope.' + path } diff --git a/test/unit/specs/expression_parser_spec.js b/test/unit/specs/expression_parser_spec.js index 0fac5096a5c..ba5247486fa 100644 --- a/test/unit/specs/expression_parser_spec.js +++ b/test/unit/specs/expression_parser_spec.js @@ -3,6 +3,10 @@ var expParser = require('../../../src/parse/expression') function assertExp (testCase) { var fn = expParser.parse(testCase.exp) expect(fn(testCase.scope)).toEqual(testCase.expected) + expect(fn.paths.length).toBe(testCase.paths.length) + fn.paths.forEach(function (p, i) { + expect(p).toBe(testCase.paths[i]) + }) } var testCases = [ @@ -12,7 +16,8 @@ var testCases = [ scope: { a: {} }, - expected: undefined + expected: undefined, + paths: ['a'] }, { // simple path that exists @@ -20,7 +25,8 @@ var testCases = [ scope: { a:{b:{d:123}} }, - expected: 123 + expected: 123, + paths: ['a'] }, // complex path { @@ -28,7 +34,8 @@ var testCases = [ scope: { a:{b:{c:234}} }, - expected: 234 + expected: 234, + paths: ['a'] }, { // string concat @@ -37,7 +44,8 @@ var testCases = [ a: 'hello', b: 'world' }, - expected: 'helloworld' + expected: 'helloworld', + paths: ['a', 'b'] }, { // math @@ -46,7 +54,8 @@ var testCases = [ a: 100, b: 23 }, - expected: 100 - 23 * 2 + 45 + expected: 100 - 23 * 2 + 45, + paths: ['a', 'b'] }, { // boolean logic @@ -58,7 +67,8 @@ var testCases = [ d: false, e: 'worked' }, - expected: 'worked' + expected: 'worked', + paths: ['a', 'b', 'c', 'd', 'e'] }, { // inline string with newline @@ -66,7 +76,8 @@ var testCases = [ scope: { a: 'inline ' }, - expected: 'inline hel\nlo' + expected: 'inline hel\nlo', + paths: ['a'] }, { // dollar signs and underscore @@ -75,7 +86,8 @@ var testCases = [ _a: 'underscore', $b: 'dollar' }, - expected: 'underscore dollar' + expected: 'underscore dollar', + paths: ['_a', '$b'] }, { // complex with nested values @@ -86,13 +98,15 @@ var testCases = [ done: false } }, - expected: 'write tests : nope' + expected: 'write tests : nope', + paths: ['todo'] }, { // expression with no data variables exp: "'a' + 'b'", scope: {}, - expected: 'ab' + expected: 'ab', + paths: [] }, { // values with same variable name inside strings @@ -101,7 +115,8 @@ var testCases = [ test: 1, hi: 2 }, - expected: '"test"1\'hi\'2' + expected: '"test"1\'hi\'2', + paths: ['test', 'hi'] }, { // expressions with inline object literals @@ -112,7 +127,8 @@ var testCases = [ }, haha: 'hoho' }, - expected: 'namehoho123' + expected: 'namehoho123', + paths: ['sortRows', 'haha'] }, { // space between path segments @@ -121,7 +137,8 @@ var testCases = [ a: { b: { c: 12 }}, d: 3 }, - expected: 15 + expected: 15, + paths: ['a', 'd'] }, { // space in bracket identifiers @@ -130,7 +147,8 @@ var testCases = [ a: {' a.b.c ': 123}, b: {' e ': 234} }, - expected: 357 + expected: 357, + paths: ['a', 'b'] }, { // number literal @@ -138,7 +156,8 @@ var testCases = [ scope: { a: 3 }, - expected: 301.1 + expected: 301.1, + paths: ['a'] }, { //keyowrd + keyword literal @@ -146,7 +165,8 @@ var testCases = [ scope: { a: { 'true': false } }, - expected: false + expected: false, + paths: ['a'] }, { // super complex @@ -161,7 +181,8 @@ var testCases = [ c: { ' d ': {e: 3}}, e: 4.5 }, - expected: 8 + expected: 8, + paths: ['$a', 'b', 'c', 'e'] } ] From c2eee4ea39340f6cf97e9ac1f2ed5fc9b62db28d Mon Sep 17 00:00:00 2001 From: Evan You Date: Mon, 4 Aug 2014 23:13:47 -0400 Subject: [PATCH 0101/1534] return objects from expreesion parser, since attaching properties to functions is slower than using a wrapper object. --- src/directive.js | 18 +++++------ src/parse/expression.js | 38 +++++++++++++---------- test/unit/specs/expression_parser_spec.js | 18 +++++------ 3 files changed, 39 insertions(+), 35 deletions(-) diff --git a/src/directive.js b/src/directive.js index e3e87b2a8d3..58b1c8540d2 100644 --- a/src/directive.js +++ b/src/directive.js @@ -50,15 +50,14 @@ function Directive (type, el, vm, descriptor, definition) { this._initDef(definition) if (this.expression && !this.isLiteral) { - // TODO - // test for simple path vs. expression - this._getter = expParser.parse(this.expression, this.twoway) - this._setter = this._getter.setter - + // parse expression + var res = expParser.parse(this.expression, this.twoway) + this._getter = res.get + this._setter = res.set + // init dependencies + this._initDeps(res.paths) // init filters this._initFilters() - // init dependencies - this._initDeps() // init methods that need to be context-bound this._initBoundMethods() // update for the first time @@ -143,11 +142,12 @@ p._initFilters = function () { * e.g. in "a && a.b", if `a` is not present at compilation, * the directive will end up with no dependency at all and * never gets updated. + * + * @param {Array} paths */ -p._initDeps = function () { +p._initDeps = function (paths) { var self = this - var paths = this._getter.paths paths.forEach(function (path) { self._addDep(path) }) diff --git a/src/parse/expression.js b/src/parse/expression.js index 48f5d2999b1..decf95aa58f 100644 --- a/src/parse/expression.js +++ b/src/parse/expression.js @@ -108,13 +108,15 @@ function compileExpFns (exp, needSet) { .replace(restoreRE, restore) var getter = makeGetter(body) if (getter) { - getter.body = body - getter.paths = paths - if (needSet) { - getter.setter = makeSetter(body) + return { + get : getter, + body : body, + paths : paths, + set : needSet + ? makeSetter(body) + : null } } - return getter } /** @@ -135,13 +137,15 @@ function compilePathFns (exp) { path = Path.parse(exp) getter = path.get } - // save root path segment - getter.paths = [exp.match(rootPathRE)[0]] - // always generate setter for simple paths - getter.setter = function (obj, val) { - Path.set(obj, path, val) + return { + get: getter, + // always generate setter for simple paths + set: function (obj, val) { + Path.set(obj, path, val) + }, + // save root path segment + paths: [exp.match(rootPathRE)[0]] } - return getter } /** @@ -190,9 +194,9 @@ function makeSetter (body) { * @param {Function} fn */ -function checkSetter (fn) { - if (!fn.setter) { - fn.setter = makeSetter(fn.body) +function checkSetter (hit) { + if (!hit.set) { + fn.set = makeSetter(hit.body) } } @@ -217,9 +221,9 @@ exports.parse = function (exp, needSet) { // we do a simple path check to optimize for that scenario. // the check fails valid paths with unusal whitespaces, but // that's too rare and we don't care. - var getter = pathTestRE.test(exp) + var res = pathTestRE.test(exp) ? compilePathFns(exp) : compileExpFns(exp, needSet) - expressionCache.put(exp, getter) - return getter + expressionCache.put(exp, res) + return res } \ No newline at end of file diff --git a/test/unit/specs/expression_parser_spec.js b/test/unit/specs/expression_parser_spec.js index ba5247486fa..41db6c4a476 100644 --- a/test/unit/specs/expression_parser_spec.js +++ b/test/unit/specs/expression_parser_spec.js @@ -1,10 +1,10 @@ var expParser = require('../../../src/parse/expression') function assertExp (testCase) { - var fn = expParser.parse(testCase.exp) - expect(fn(testCase.scope)).toEqual(testCase.expected) - expect(fn.paths.length).toBe(testCase.paths.length) - fn.paths.forEach(function (p, i) { + var res = expParser.parse(testCase.exp) + expect(res.get(testCase.scope)).toEqual(testCase.expected) + expect(res.paths.length).toBe(testCase.paths.length) + res.paths.forEach(function (p, i) { expect(p).toBe(testCase.paths[i]) }) } @@ -193,23 +193,23 @@ describe('Expression Parser', function () { }) it('dynamic setter', function () { - var setter = expParser.parse('a[b]', true).setter + var res = expParser.parse('a[b]', true) var scope = { a: { c: 1 }, b: 'c' } - setter(scope, 2) + res.set(scope, 2) expect(scope.a.c).toBe(2) }) it('simple path setter', function () { - var setter = expParser.parse('a.b.c', true).setter + var res = expParser.parse('a.b.c', true) var scope = {} expect(function () { - setter(scope, 123) + res.set(scope, 123) }).not.toThrow() scope.a = {b:{c:0}} - setter(scope, 123) + res.set(scope, 123) expect(scope.a.b.c).toBe(123) }) From bb5600c4c7d1cf114393187a19ea3869734c5dbb Mon Sep 17 00:00:00 2001 From: Evan You Date: Mon, 4 Aug 2014 23:16:13 -0400 Subject: [PATCH 0102/1534] fix checkSetter() --- src/parse/expression.js | 4 ++-- test/unit/specs/expression_parser_spec.js | 3 +++ 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/src/parse/expression.js b/src/parse/expression.js index decf95aa58f..3793b2001d8 100644 --- a/src/parse/expression.js +++ b/src/parse/expression.js @@ -191,12 +191,12 @@ function makeSetter (body) { /** * Check for setter existence on a cache hit. * - * @param {Function} fn + * @param {Function} hit */ function checkSetter (hit) { if (!hit.set) { - fn.set = makeSetter(hit.body) + hit.set = makeSetter(hit.body) } } diff --git a/test/unit/specs/expression_parser_spec.js b/test/unit/specs/expression_parser_spec.js index 41db6c4a476..7b19193e194 100644 --- a/test/unit/specs/expression_parser_spec.js +++ b/test/unit/specs/expression_parser_spec.js @@ -193,6 +193,9 @@ describe('Expression Parser', function () { }) it('dynamic setter', function () { + // make sure checkSetter works: + // should add setter if a cache hit doesn't have hit function. + expParser.parse('a[b]') var res = expParser.parse('a[b]', true) var scope = { a: { c: 1 }, From 09d00355e4e33a19607655709d7f6b2a8ae3817e Mon Sep 17 00:00:00 2001 From: Evan You Date: Tue, 5 Aug 2014 14:29:53 -0400 Subject: [PATCH 0103/1534] add test coverage & improve coverage for existing tests --- .gitignore | 3 +- gruntfile.js | 14 ++++- package.json | 3 +- src/observe/array-augmentations.js | 2 +- src/observe/observer.js | 55 +++++++------------ src/parse/path.js | 11 ++-- src/parse/template.js | 6 ++- src/util/debug.js | 14 ++--- test/unit/specs/directive_parser_spec.js | 36 +++++++++---- test/unit/specs/expression_parser_spec.js | 40 ++++++++++++-- test/unit/specs/observer_spec.js | 51 ++++++++++++++++++ test/unit/specs/path_parser_spec.js | 12 +++-- test/unit/specs/template.js | 24 ++++++--- test/unit/specs/util_spec.js | 64 ++++++++++++++++++++++- 14 files changed, 249 insertions(+), 86 deletions(-) diff --git a/.gitignore b/.gitignore index 70310172ea6..ffc84257488 100644 --- a/.gitignore +++ b/.gitignore @@ -3,4 +3,5 @@ test/unit/specs.js explorations node_modules .DS_Store -benchmarks/browser.js \ No newline at end of file +benchmarks/browser.js +coverage \ No newline at end of file diff --git a/gruntfile.js b/gruntfile.js index 6dcfd962f0e..f7a0a9d41e0 100644 --- a/gruntfile.js +++ b/gruntfile.js @@ -48,7 +48,17 @@ module.exports = function (grunt) { phantom: { options: { browsers: ['PhantomJS'], - reporters: ['progress'] + reporters: ['progress', 'coverage'], + preprocessors: { + 'src/**/*.js': ['commonjs', 'coverage'], + 'test/unit/specs/*.js': ['commonjs'] + }, + coverageReporter: { + reporters: [ + { type: 'lcov' }, + { type: 'text-summary' } + ] + } } } }, @@ -119,7 +129,7 @@ module.exports = function (grunt) { }) grunt.registerTask('unit', ['karma:browsers']) - grunt.registerTask('phantom', ['karma:phantom']) + grunt.registerTask('cover', ['karma:phantom']) grunt.registerTask('bench', ['browserify:bench']) grunt.registerTask('watch', ['browserify:watch']) grunt.registerTask('build', ['browserify:test', 'browserify:build', 'uglify:build']) diff --git a/package.json b/package.json index 651f3b61ca6..168e8543fff 100644 --- a/package.json +++ b/package.json @@ -31,7 +31,8 @@ "karma": "^0.12.16", "karma-browserify": "^0.2.1", "karma-chrome-launcher": "^0.1.4", - "karma-commonjs": "0.0.10", + "karma-commonjs": "^0.0.10", + "karma-coverage": "^0.2.5", "karma-firefox-launcher": "^0.1.3", "karma-jasmine": "^0.1.5", "karma-phantomjs-launcher": "^0.1.4" diff --git a/src/observe/array-augmentations.js b/src/observe/array-augmentations.js index e3b2d2c4cb3..3a5f652f0ea 100644 --- a/src/observe/array-augmentations.js +++ b/src/observe/array-augmentations.js @@ -51,7 +51,7 @@ var arrayAugmentations = Object.create(Array.prototype) // link/unlink added/removed elements if (inserted) ob.link(inserted, index) - if (removed) ob.unlink(removed) + if (removed) ob.unlink(removed, index) // update indices if (method !== 'push' && method !== 'pop') { diff --git a/src/observe/observer.js b/src/observe/observer.js index 685d0c3bfee..44b5ee91057 100644 --- a/src/observe/observer.js +++ b/src/observe/observer.js @@ -3,6 +3,8 @@ var Emitter = require('../emitter') var arrayAugmentations = require('./array-augmentations') var objectAugmentations = require('./object-augmentations') +var uid = 0 + /** * Type enums */ @@ -31,6 +33,7 @@ var OBJECT = 1 function Observer (value, type, options) { Emitter.call(this, options && options.callbackContext) + this.id = ++uid this.value = value this.type = type this.parents = null @@ -155,11 +158,18 @@ p.observe = function (key, val) { var ob = Observer.create(val) if (ob) { // register self as a parent of the child observer. - if (ob.findParent(this) > -1) return - (ob.parents || (ob.parents = [])).push({ + var parents = ob.parents + if (!parents) { + ob.parents = parents = Object.create(null) + } + if (parents[this.id]) { + _.warn('Observing duplicate key: ' + key) + return + } + parents[this.id] = { ob: this, key: key - }) + } } } @@ -172,7 +182,7 @@ p.observe = function (key, val) { p.unobserve = function (val) { if (val && val.$observer) { - val.$observer.findParent(this, true) + val.$observer.parents[this.id] = null } } @@ -202,11 +212,6 @@ p.convert = function (key, val) { ob.observe(key, newVal) ob.emit('set:self', key, newVal) ob.propagate('set', key, newVal) - if (_.isArray(newVal)) { - ob.propagate('set', - key + Observer.pathDelimiter + 'length', - newVal.length) - } } }) } @@ -223,14 +228,13 @@ p.convert = function (key, val) { p.propagate = function (event, path, val, mutation) { this.emit(event, path, val, mutation) if (!this.parents) return - for (var i = 0, l = this.parents.length; i < l; i++) { - var parent = this.parents[i] - var ob = parent.ob + for (var id in this.parents) { + var parent = this.parents[id] var key = parent.key var parentPath = path ? key + Observer.pathDelimiter + path : key - ob.propagate(event, parentPath, val, mutation) + parent.ob.propagate(event, parentPath, val, mutation) } } @@ -246,32 +250,9 @@ p.updateIndices = function () { while (i--) { ob = arr[i] && arr[i].$observer if (ob) { - var j = ob.findParent(this) - ob.parents[j].key = i - } - } -} - -/** - * Find a parent option object - * - * @param {Observer} parent - * @param {Boolean} [remove] - whether to remove the parent - * @return {Number} - index of parent - */ - -p.findParent = function (parent, remove) { - var parents = this.parents - if (!parents) return -1 - var i = parents.length - while (i--) { - var p = parents[i] - if (p.ob === parent) { - if (remove) parents.splice(i, 1) - return i + ob.parents[this.id].key = i } } - return -1 } module.exports = Observer \ No newline at end of file diff --git a/src/parse/path.js b/src/parse/path.js index f9f95447873..b5f2071e533 100644 --- a/src/parse/path.js +++ b/src/parse/path.js @@ -159,10 +159,7 @@ function parsePath (path) { } } - function maybeUnescapeQuote() { - if (index >= path.length) { - return - } + function maybeUnescapeQuote () { var nextChar = path[index + 1] if ((mode === 'inSingleQuote' && nextChar === "'") || (mode === 'inDoubleQuote' && nextChar === '"')) { @@ -177,7 +174,7 @@ function parsePath (path) { index++ c = path[index] - if (c === '\\' && maybeUnescapeQuote(mode)) { + if (c === '\\' && maybeUnescapeQuote()) { continue } @@ -198,8 +195,6 @@ function parsePath (path) { return keys } } - - return // parse error } /** @@ -320,7 +315,7 @@ exports.set = function (obj, path, val) { path = exports.parse(path) } if (!path) { - return + return false } for (var i = 0, l = path.length - 1; i < l; i++) { if (!obj || typeof obj !== 'object') { diff --git a/src/parse/template.js b/src/parse/template.js index fb507df53e3..0aeec627d0c 100644 --- a/src/parse/template.js +++ b/src/parse/template.js @@ -1,3 +1,5 @@ +/* global DocumentFragment */ + var Cache = require('../cache') var templateCache = new Cache(100) @@ -93,7 +95,7 @@ function nodeToFragment (node) { var tag = node.tagName // if its a template tag and the browser supports it, // its content is already a document fragment. - if (tag === 'TEMPLATE' && node.content) { + if (tag === 'TEMPLATE' && node.content instanceof DocumentFragment) { return node.content } return tag === 'SCRIPT' @@ -119,7 +121,7 @@ exports.parse = function (template) { var node, frag // if the template is already a document fragment -- do nothing - if (template instanceof window.DocumentFragment) { + if (template instanceof DocumentFragment) { return template } diff --git a/src/util/debug.js b/src/util/debug.js index 4df9c5bbc10..dfe707306bb 100644 --- a/src/util/debug.js +++ b/src/util/debug.js @@ -14,26 +14,26 @@ function enableDebug () { /** * Log a message. * - * @param {String} msg + * @param {*...} */ - exports.log = function (msg) { + exports.log = function () { if (hasConsole && config.debug) { - console.log(msg) + console.log.apply(console, arguments) } } /** * We've got a problem here. * - * @param {String} msg + * @param {*...} */ - exports.warn = function (msg) { + exports.warn = function () { if (hasConsole && !config.silent) { - console.warn(msg) + console.warn.apply(console, arguments) if (config.debug && console.trace) { - console.trace(msg) + console.trace() } } } diff --git a/test/unit/specs/directive_parser_spec.js b/test/unit/specs/directive_parser_spec.js index eff5105a786..3d806d6555d 100644 --- a/test/unit/specs/directive_parser_spec.js +++ b/test/unit/specs/directive_parser_spec.js @@ -17,18 +17,22 @@ describe('Directive Parser', function () { expect(res[0].raw).toBe('arg:exp') }) - it('arg : exp | abc', function () { - var res = parse(' arg : exp | abc de') + // filters + it('arg : exp | abc de | bcd', function () { + var res = parse(' arg : exp | abc de | bcd') expect(res.length).toBe(1) expect(res[0].expression).toBe('exp') expect(res[0].arg).toBe('arg') - expect(res[0].raw).toBe('arg : exp | abc de') - expect(res[0].filters.length).toBe(1) + expect(res[0].raw).toBe('arg : exp | abc de | bcd') + expect(res[0].filters.length).toBe(2) expect(res[0].filters[0].name).toBe('abc') expect(res[0].filters[0].args.length).toBe(1) expect(res[0].filters[0].args[0]).toBe('de') + expect(res[0].filters[1].name).toBe('bcd') + expect(res[0].filters[1].args).toBeNull() }) + // double pipe it('a || b | c', function () { var res = parse('a || b | c') expect(res.length).toBe(1) @@ -39,13 +43,15 @@ describe('Directive Parser', function () { expect(res[0].filters[0].args).toBeNull() }) - it('a ? b : c', function () { - var res = parse('a ? b : c') + // single quote + boolean + it('a ? \'b\' : c', function () { + var res = parse('a ? \'b\' : c') expect(res.length).toBe(1) - expect(res[0].expression).toBe('a ? b : c') + expect(res[0].expression).toBe('a ? \'b\' : c') expect(res[0].filters).toBeUndefined() }) + // double quote + boolean it('"a:b:c||d|e|f" || d ? a : b', function () { var res = parse('"a:b:c||d|e|f" || d ? a : b') expect(res.length).toBe(1) @@ -54,6 +60,7 @@ describe('Directive Parser', function () { expect(res[0].arg).toBeUndefined() }) + // multiple simple clause it('a, b, c', function () { var res = parse('a, b, c') expect(res.length).toBe(3) @@ -62,21 +69,27 @@ describe('Directive Parser', function () { expect(res[2].expression).toBe('c') }) - it('a:b | c, d:e | f, g:h | i', function () { - var res = parse('a:b | c, d:e | f, g:h | i') + // multiple complex clause + it('a:b | c | j, d:e | f | k l, g:h | i', function () { + var res = parse('a:b | c | j, d:e | f | k l, g:h | i') expect(res.length).toBe(3) expect(res[0].arg).toBe('a') expect(res[0].expression).toBe('b') - expect(res[0].filters.length).toBe(1) + expect(res[0].filters.length).toBe(2) expect(res[0].filters[0].name).toBe('c') expect(res[0].filters[0].args).toBeNull() + expect(res[0].filters[1].name).toBe('j') + expect(res[0].filters[1].args).toBeNull() expect(res[1].arg).toBe('d') expect(res[1].expression).toBe('e') - expect(res[1].filters.length).toBe(1) + expect(res[1].filters.length).toBe(2) expect(res[1].filters[0].name).toBe('f') expect(res[1].filters[0].args).toBeNull() + expect(res[1].filters[1].name).toBe('k') + expect(res[1].filters[1].args.length).toBe(1) + expect(res[1].filters[1].args[0]).toBe('l') expect(res[2].arg).toBe('g') expect(res[2].expression).toBe('h') @@ -85,6 +98,7 @@ describe('Directive Parser', function () { expect(res[2].filters[0].args).toBeNull() }) + // super complex it('click:test(c.indexOf(d,f),"e,f"), input: d || [e,f], ok:{a:1,b:2}', function () { var res = parse('click:test(c.indexOf(d,f),"e,f"), input: d || [e,f], ok:{a:1,b:2}') expect(res.length).toBe(3) diff --git a/test/unit/specs/expression_parser_spec.js b/test/unit/specs/expression_parser_spec.js index 7b19193e194..2c558581bee 100644 --- a/test/unit/specs/expression_parser_spec.js +++ b/test/unit/specs/expression_parser_spec.js @@ -1,4 +1,5 @@ var expParser = require('../../../src/parse/expression') +var _ = require('../../../src/util') function assertExp (testCase) { var res = expParser.parse(testCase.exp) @@ -217,9 +218,40 @@ describe('Expression Parser', function () { }) it('cache', function () { - var fn1 = expParser.parse('a + b') - var fn2 = expParser.parse('a + b') - expect(fn1).toBe(fn2) + var res1 = expParser.parse('a + b') + var res2 = expParser.parse('a + b') + expect(res1).toBe(res2) }) -}) \ No newline at end of file + describe('invalid expression', function () { + + beforeEach(function () { + spyOn(_, 'warn') + }) + + it('should warn on invalid expression', function () { + var res = expParser.parse('a--b"ffff') + expect(_.warn).toHaveBeenCalled() + }) + + if (leftHandThrows()) { + it('should warn on invalid left hand expression for setter', function () { + var res = expParser.parse('a+b', true) + expect(_.warn).toHaveBeenCalled() + }) + } + }) +}) + +/** + * check if creating a new Function with invalid left-hand + * assignment would throw + */ + +function leftHandThrows () { + try { + var fn = new Function('a + b = 1') + } catch (e) { + return true + } +} \ No newline at end of file diff --git a/test/unit/specs/observer_spec.js b/test/unit/specs/observer_spec.js index a10e7dc1b45..d225d6b153a 100644 --- a/test/unit/specs/observer_spec.js +++ b/test/unit/specs/observer_spec.js @@ -3,6 +3,7 @@ */ var Observer = require('../../../src/observe/observer') +var _ = require('../../../src/util') // internal emitter has fixed 3 arguments // so we need to fill up the assetions with undefined var u = undefined @@ -93,6 +94,17 @@ describe('Observer', function () { expect(obj.b).toBe(234) }) + it('warn duplicate value', function () { + spyOn(_, 'warn') + var obj = { + a: { b: 123 }, + b: null + } + var ob = Observer.create(obj) + obj.b = obj.a + expect(_.warn).toHaveBeenCalled() + }) + it('array get', function () { Observer.emitGet = true @@ -274,6 +286,10 @@ describe('Observer', function () { var ob = Observer.create(obj) ob.on('add', spy) + // ignore existing keys + obj.$add('a', 123) + expect(spy.callCount).toBe(0) + // add event var add = {d:2} obj.a.$add('c', add) @@ -290,6 +306,10 @@ describe('Observer', function () { var ob = Observer.create(obj) ob.on('delete', spy) + // ignore non-present key + obj.$delete('c') + expect(spy.callCount).toBe(0) + obj.a.$delete('b') expect(spy).toHaveBeenCalledWith('a.b', u, u) }) @@ -318,6 +338,16 @@ describe('Observer', function () { expect(spy).toHaveBeenCalledWith('1.a', 4, u) }) + it('array.$set with out of bound length', function () { + var arr = [{a:1}, {a:2}] + var ob = Observer.create(arr) + var inserted = {a:3} + arr.$set(3, inserted) + expect(arr.length).toBe(4) + expect(arr[2]).toBeUndefined() + expect(arr[3]).toBe(inserted) + }) + it('array.$remove', function () { var arr = [{a:1}, {a:2}] var ob = Observer.create(arr) @@ -339,4 +369,25 @@ describe('Observer', function () { expect(spy).toHaveBeenCalledWith('0.a', 3, u) }) + it('array.$remove object', function () { + var arr = [{a:1}, {a:2}] + var ob = Observer.create(arr) + ob.on('mutate', spy) + var removed = arr.$remove(arr[0]) + + expect(spy.mostRecentCall.args[0]).toBe('') + expect(spy.mostRecentCall.args[1]).toBe(arr) + var mutation = spy.mostRecentCall.args[2] + expect(mutation).toBeDefined() + expect(mutation.method).toBe('splice') + expect(mutation.index).toBe(0) + expect(mutation.removed.length).toBe(1) + expect(mutation.inserted.length).toBe(0) + expect(mutation.removed[0]).toBe(removed) + + ob.on('set', spy) + arr[0].a = 3 + expect(spy).toHaveBeenCalledWith('0.a', 3, u) + }) + }) \ No newline at end of file diff --git a/test/unit/specs/path_parser_spec.js b/test/unit/specs/path_parser_spec.js index 1e7ae9b7ec6..0a0e90a7066 100644 --- a/test/unit/specs/path_parser_spec.js +++ b/test/unit/specs/path_parser_spec.js @@ -92,7 +92,7 @@ describe('Path', function () { } } expect(Path.getFromArray(obj, path)).toBe(123) - expect(Path.getFromArray(obj, ['a','c'])).toBeUndefined() + expect(Path.getFromArray(obj, ['a','c','d'])).toBeUndefined() }) it('get from observer delimited path', function () { @@ -122,11 +122,17 @@ describe('Path', function () { }) it('set fail', function () { - var path = 'a.b.c' var obj = { a: null } - var res = Path.set(obj, path, 12345) + var res = Path.set(obj, 'a.b.c', 12345) + expect(res).toBe(false) + res = Path.set(obj, 'a.b', 12345) + expect(res).toBe(false) + }) + + it('set invalid', function () { + var res = Path.set({}, 'ab[c]d', 123) expect(res).toBe(false) }) diff --git a/test/unit/specs/template.js b/test/unit/specs/template.js index 02bc883d8d3..c7ec5b36834 100644 --- a/test/unit/specs/template.js +++ b/test/unit/specs/template.js @@ -10,14 +10,15 @@ describe('Template Parser', function () { expect(res).toBe(frag) }) - // only test template node if it works in the browser being tested. - var templateNode = document.createElement('template') - if (templateNode.content) { - it('should return content if argument is a valid template node', function () { - var res = parse(templateNode) - expect(res).toBe(templateNode.content) - }) - } + it('should return content if argument is a valid template node', function () { + var templateNode = document.createElement('template') + if (!templateNode.content) { + // mock the content + templateNode.content = document.createDocumentFragment() + } + var res = parse(templateNode) + expect(res).toBe(templateNode.content) + }) it('should parse if argument is a template string', function () { var res = parse(testString) @@ -26,6 +27,13 @@ describe('Template Parser', function () { expect(res.querySelector('.test').textContent).toBe('world') }) + it('should work if the template string doesn\'t contain tags', function () { + var res = parse('hello!') + expect(res instanceof DocumentFragment).toBeTruthy() + expect(res.childNodes.length).toBe(1) + expect(res.firstChild.nodeType).toBe(3) // Text node + }) + it('should parse textContent if argument is a script node', function () { var node = document.createElement('script') node.textContent = testString diff --git a/test/unit/specs/util_spec.js b/test/unit/specs/util_spec.js index 6258b536269..ae6a344b387 100644 --- a/test/unit/specs/util_spec.js +++ b/test/unit/specs/util_spec.js @@ -163,6 +163,14 @@ describe('Util', function () { expect(child.nextSibling).toBe(target) }) + it('after with sibling', function () { + var sibling = div() + parent.appendChild(sibling) + _.after(target, child) + expect(target.parentNode).toBe(parent) + expect(child.nextSibling).toBe(target) + }) + it('remove', function () { _.remove(child) expect(child.parentNode).toBeNull() @@ -175,6 +183,13 @@ describe('Util', function () { expect(parent.firstChild).toBe(target) }) + it('prepend to empty node', function () { + parent.removeChild(child) + _.prepend(target, parent) + expect(target.parentNode).toBe(parent) + expect(parent.firstChild).toBe(target) + }) + it('copyAttributes', function () { parent.setAttribute('test1', 1) parent.setAttribute('test2', 2) @@ -183,9 +198,56 @@ describe('Util', function () { expect(target.getAttribute('test1')).toBe('1') expect(target.getAttribute('test2')).toBe('2') }) - }) + } + + if (typeof console !== undefined) { + + describe('Debug', function () { + beforeEach(function () { + spyOn(console, 'log') + spyOn(console, 'warn') + if (console.trace) { + spyOn(console, 'trace') + } + }) + + it('log when debug is true', function () { + config.debug = true + _.log('hello', 'world') + expect(console.log).toHaveBeenCalledWith('hello', 'world') + }) + + it('not log when debug is false', function () { + config.debug = false + _.log('bye', 'world') + expect(console.log.callCount).toBe(0) + }) + + it('warn when silent is false', function () { + config.silent = false + _.warn('oops', 'ops') + expect(console.warn).toHaveBeenCalledWith('oops', 'ops') + }) + + it('not warn when silent is ture', function () { + config.silent = true + _.warn('oops', 'ops') + expect(console.warn.callCount).toBe(0) + }) + + if (console.trace) { + it('trace when not silent and debugging', function () { + config.debug = true + config.silent = false + _.warn('haha') + expect(console.trace).toHaveBeenCalled() + config.debug = false + config.silent = true + }) + } + }) } describe('Option merging', function () { From dceaafb37921c2a58f35b79190e5318fcdf61de4 Mon Sep 17 00:00:00 2001 From: Evan You Date: Tue, 5 Aug 2014 14:48:38 -0400 Subject: [PATCH 0104/1534] minor yak sahving --- src/api/lifecycle.js | 6 +++--- src/observe/array-augmentations.js | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/api/lifecycle.js b/src/api/lifecycle.js index cc68a2a0b95..c2514cfca5d 100644 --- a/src/api/lifecycle.js +++ b/src/api/lifecycle.js @@ -1,5 +1,3 @@ -var _ = require('../util') - /** * Set instance target element and kick off the compilation process. * The passed in `el` can be a selector string, an existing Element, @@ -26,6 +24,8 @@ exports.$mount = function (el) { exports.$destroy = function (remove) { this._callHook('beforeDestroy') - // TODO + if (remove) { + // TODO + } this._callHook('afterDestroy') } \ No newline at end of file diff --git a/src/observe/array-augmentations.js b/src/observe/array-augmentations.js index 3a5f652f0ea..e3b2d2c4cb3 100644 --- a/src/observe/array-augmentations.js +++ b/src/observe/array-augmentations.js @@ -51,7 +51,7 @@ var arrayAugmentations = Object.create(Array.prototype) // link/unlink added/removed elements if (inserted) ob.link(inserted, index) - if (removed) ob.unlink(removed, index) + if (removed) ob.unlink(removed) // update indices if (method !== 'push' && method !== 'pop') { From d9c463855c0175eb8973d6e4600107b392396d6a Mon Sep 17 00:00:00 2001 From: Evan You Date: Tue, 5 Aug 2014 18:35:39 -0400 Subject: [PATCH 0105/1534] test for shared observe and unobserve --- src/observe/observer.js | 1 + test/unit/specs/observer_spec.js | 20 ++++++++++++++++++++ 2 files changed, 21 insertions(+) diff --git a/src/observe/observer.js b/src/observe/observer.js index 44b5ee91057..54f3b1dd885 100644 --- a/src/observe/observer.js +++ b/src/observe/observer.js @@ -230,6 +230,7 @@ p.propagate = function (event, path, val, mutation) { if (!this.parents) return for (var id in this.parents) { var parent = this.parents[id] + if (!parent) continue var key = parent.key var parentPath = path ? key + Observer.pathDelimiter + path diff --git a/test/unit/specs/observer_spec.js b/test/unit/specs/observer_spec.js index d225d6b153a..f673192a652 100644 --- a/test/unit/specs/observer_spec.js +++ b/test/unit/specs/observer_spec.js @@ -390,4 +390,24 @@ describe('Observer', function () { expect(spy).toHaveBeenCalledWith('0.a', 3, u) }) + it('shared observe', function () { + var obj = { a: 1 } + var parentA = { child1: obj } + var parentB = { child2: obj } + var obA = Observer.create(parentA) + var obB = Observer.create(parentB) + obA.on('set', spy) + obB.on('set', spy) + obj.a = 2 + expect(spy.callCount).toBe(2) + expect(spy).toHaveBeenCalledWith('child1.a', 2, u) + expect(spy).toHaveBeenCalledWith('child2.a', 2, u) + // test unobserve + parentA.child1 = null + obj.a = 3 + expect(spy.callCount).toBe(4) + expect(spy).toHaveBeenCalledWith('child1', null, u) + expect(spy).toHaveBeenCalledWith('child2.a', 3, u) + }) + }) \ No newline at end of file From 4ac355b1d1f0897ee68085668d107cef14758eda Mon Sep 17 00:00:00 2001 From: Evan You Date: Tue, 5 Aug 2014 23:57:51 -0400 Subject: [PATCH 0106/1534] 100% coverage for utils --- src/directive.js | 8 ++++---- src/instance/element.js | 4 ++++ src/util/env.js | 25 ++++++++++++------------- src/util/lang.js | 12 +++++------- 4 files changed, 25 insertions(+), 24 deletions(-) diff --git a/src/directive.js b/src/directive.js index 58b1c8540d2..a3f58897431 100644 --- a/src/directive.js +++ b/src/directive.js @@ -147,10 +147,10 @@ p._initFilters = function () { */ p._initDeps = function (paths) { - var self = this - paths.forEach(function (path) { - self._addDep(path) - }) + var i = paths.length + while (i--) { + this._addDep(paths[i]) + } this._deps = this._newDeps } diff --git a/src/instance/element.js b/src/instance/element.js index be94b1ebebd..761e686b005 100644 --- a/src/instance/element.js +++ b/src/instance/element.js @@ -12,7 +12,11 @@ var templateParser = require('../parse/template') exports._initElement = function (el) { if (typeof el === 'string') { + var selector = el el = document.querySelector(el) + if (!el) { + _.warn('Cannot find element: ' + selector) + } } // If the passed in `el` is a DocumentFragment, the instance is // considered a "block instance" which manages not a single element, diff --git a/src/util/env.js b/src/util/env.js index d8f0a8847a3..7f73e70504f 100644 --- a/src/util/env.js +++ b/src/util/env.js @@ -38,20 +38,19 @@ exports.isIE9 = var testElement = inBrowser ? document.createElement('div') - : null + : undefined exports.transitionEndEvent = (function () { - if (!inBrowser) { - return null - } - var map = { - 'webkitTransition' : 'webkitTransitionEnd', - 'transition' : 'transitionend', - 'mozTransition' : 'transitionend' - } - for (var prop in map) { - if (testElement.style[prop] !== undefined) { - return map[prop] + if (inBrowser) { + var map = { + 'webkitTransition' : 'webkitTransitionEnd', + 'transition' : 'transitionend', + 'mozTransition' : 'transitionend' + } + for (var prop in map) { + if (testElement.style[prop] !== undefined) { + return map[prop] + } } } })() @@ -60,4 +59,4 @@ exports.animationEndEvent = inBrowser ? testElement.style.animation !== undefined ? 'animationend' : 'webkitAnimationEnd' - : null \ No newline at end of file + : undefined \ No newline at end of file diff --git a/src/util/lang.js b/src/util/lang.js index af441cc12ec..a2a02e7e468 100644 --- a/src/util/lang.js +++ b/src/util/lang.js @@ -123,10 +123,8 @@ exports.define = function (obj, key, val, enumerable) { * @param {Object} proto */ -if ('__proto__' in {}) { - exports.augment = function (target, proto) { - target.__proto__ = proto - } -} else { - exports.augment = exports.deepMixin -} \ No newline at end of file +exports.augment = '__proto__' in {} + ? function (target, proto) { + target.__proto__ = proto + } + : exports.deepMixin \ No newline at end of file From 012576aa5e2095932ee3cd80ff5179ba5c9e193e Mon Sep 17 00:00:00 2001 From: Evan You Date: Thu, 7 Aug 2014 10:37:49 -0400 Subject: [PATCH 0107/1534] remove faulty nodeType check when parsing template --- src/parse/template.js | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/parse/template.js b/src/parse/template.js index 0aeec627d0c..40acc831cad 100644 --- a/src/parse/template.js +++ b/src/parse/template.js @@ -74,9 +74,7 @@ function stringToFragment (templateString) { var child /* jshint boss:true */ while (child = node.firstChild) { - if (node.nodeType === 1) { - frag.appendChild(child) - } + frag.appendChild(child) } } From 12353d1de05f9f9e3880a401a630edc81d9dac11 Mon Sep 17 00:00:00 2001 From: Evan You Date: Thu, 7 Aug 2014 13:04:36 -0400 Subject: [PATCH 0108/1534] keep attached/detached for now --- changes.md | 4 ---- src/util/option.js | 2 ++ 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/changes.md b/changes.md index 2da9fc5bb42..7ce663bb136 100644 --- a/changes.md +++ b/changes.md @@ -50,10 +50,6 @@ The `lazy` option is removed because this does not belong at the vm level. Users This new hook is introduced to accompany the separation of instantiation and DOM mounting. It is called right before the DOM compilation starts and `this.$el` is available, so you can do some pre-processing on the element here. -### removed hooks: `attached` & `detached` - -These two have caused confusions about when they'd actually fire, and proper use cases seem to be rare. Let me know if you have important use cases for these two hooks. - ## Computed Properties `$get` and `$set` is now simply `get` and `set`: diff --git a/src/util/option.js b/src/util/option.js index b92ff97a245..16cea223cd0 100644 --- a/src/util/option.js +++ b/src/util/option.js @@ -22,6 +22,8 @@ var strats = {} strats.created = strats.ready = +strats.attached = +strats.detached = strats.beforeMount = strats.beforeDestroy = strats.afterDestroy = From 46c05fd31ad4facebf9b5883c5bdc899abd487ee Mon Sep 17 00:00:00 2001 From: Evan You Date: Thu, 7 Aug 2014 15:00:12 -0400 Subject: [PATCH 0109/1534] sync data by default --- src/binding.js | 2 +- src/instance/bindings.js | 2 ++ src/instance/scope.js | 43 ++++++++++++--------------- test/unit/specs/scope_spec.js | 56 +++++++++++++++++++++++++++++++++-- 4 files changed, 76 insertions(+), 27 deletions(-) diff --git a/src/binding.js b/src/binding.js index aa8b76a5bab..a068bc1acca 100644 --- a/src/binding.js +++ b/src/binding.js @@ -54,7 +54,7 @@ p._removeSub = function (sub) { p._notify = function () { for (var i = 0, l = this._subs.length; i < l; i++) { - this._subs[i]._update(this) + this._subs[i]._update() } } diff --git a/src/instance/bindings.js b/src/instance/bindings.js index 5abd5695c4e..7fa6e8442f3 100644 --- a/src/instance/bindings.js +++ b/src/instance/bindings.js @@ -81,6 +81,8 @@ exports._createBindingAt = function (path) { */ exports._updateBindingAt = function (path) { + // root binding updates on any change + this._rootBinding._notify() var binding = this._getBindingAt(path, true) if (binding) { binding._notify() diff --git a/src/instance/scope.js b/src/instance/scope.js index d9afbe48259..10f7457adec 100644 --- a/src/instance/scope.js +++ b/src/instance/scope.js @@ -49,12 +49,13 @@ exports._initScope = function () { exports._teardownScope = function () { this.$scope = null - if (!this.$parent) return - var pob = this.$parent._observer - var listeners = this._scopeListeners - scopeEvents.forEach(function (event) { - pob.off(event, listeners[event]) - }) + if (this.$parent) { + var pob = this.$parent._observer + var listeners = this._scopeListeners + scopeEvents.forEach(function (event) { + pob.off(event, listeners[event]) + }) + } } /** @@ -82,10 +83,14 @@ exports._initData = function (data, init) { if (!init) { // teardown old sync listeners - this._teardownData() + this._unsyncData() // delete keys not present in the new data for (key in scope) { - if (scope.hasOwnProperty(key) && !(key in data)) { + if ( + key.charAt(0) !== '$' && + scope.hasOwnProperty(key) && + !(key in data) + ) { scope.$delete(key) } } @@ -103,20 +108,9 @@ exports._initData = function (data, init) { } // setup sync between scope and new data - if (this.$options.syncData) { - this._dataObserver = Observer.create(data) - this._sync() - } -} - -/** - * Stop data-syncing. - */ - -exports._teardownData = function () { - if (this.$options.syncData) { - this._unsync() - } + this._data = data + this._dataObserver = Observer.create(data) + this._syncData() } /** @@ -159,6 +153,7 @@ exports._initProxy = function () { // proxy vm parent & root on scope _.proxy(scope, this, '$parent') _.proxy(scope, this, '$root') + _.proxy(scope, this, '$data') } /** @@ -213,7 +208,7 @@ exports._initMethods = function () { * the original data. Requires teardown. */ -exports._sync = function () { +exports._syncData = function () { var data = this._data var scope = this.$scope var locked = false @@ -273,7 +268,7 @@ exports._sync = function () { * Teardown the sync between scope and previous data object. */ -exports._unsync = function () { +exports._unsyncData = function () { var listeners = this._syncListeners this._observer diff --git a/test/unit/specs/scope_spec.js b/test/unit/specs/scope_spec.js index 053a5ec684e..f3dce1e7f58 100644 --- a/test/unit/specs/scope_spec.js +++ b/test/unit/specs/scope_spec.js @@ -77,7 +77,6 @@ describe('Scope', function () { } var vm = new Vue({ - syncData: true, data: data }) @@ -207,7 +206,6 @@ describe('Scope', function () { var child = new Vue({ parent: parent, - syncData: true, data: parent.arr[0] }) @@ -232,6 +230,60 @@ describe('Scope', function () { }) + describe('swapping $data', function () { + + var oldData = { a: 1, c: 4 } + var newData = { a: 2, b: 3 } + var vm = new Vue({ + data: oldData + }) + var vmSpy = jasmine.createSpy('vm') + var vmAddSpy = jasmine.createSpy('vmAdd') + var oldDataSpy = jasmine.createSpy('oldData') + vm._observer.on('set', vmSpy) + vm._observer.on('add', vmAddSpy) + oldData.$observer.on('set', oldDataSpy) + + vm.$data = newData + + it('should sync new data', function () { + expect(vm._data).toBe(newData) + expect(vm.a).toBe(2) + expect(vm.b).toBe(3) + expect(vmSpy).toHaveBeenCalledWith('a', 2, u) + expect(vmAddSpy).toHaveBeenCalledWith('b', 3, u) + }) + + it('should unsync old data', function () { + expect(vm.hasOwnProperty('c')).toBe(false) + vm.a = 3 + expect(oldDataSpy.callCount).toBe(0) + expect(oldData.a).toBe(1) + expect(newData.a).toBe(3) + }) + + }) + + describe('scope teardown', function () { + var parent = new Vue({ + data: { + a: 123 + } + }) + var child = new Vue({ + parent: parent + }) + var spy = jasmine.createSpy('teardown') + child._observer.on('set', spy) + + it('should stop relaying parent events', function () { + child._teardownScope() + parent.a = 234 + expect(spy.callCount).toBe(0) + expect(child.$scope).toBeNull() + }) + }) + describe('computed', function () { var vm = new Vue({ From 40db03ad4e07c78fcd1ac40a48a221ab192711cf Mon Sep 17 00:00:00 2001 From: Evan You Date: Thu, 7 Aug 2014 15:14:10 -0400 Subject: [PATCH 0110/1534] remove syncData option in changes.md --- changes.md | 4 ---- 1 file changed, 4 deletions(-) diff --git a/changes.md b/changes.md index 7ce663bb136..434d2761a47 100644 --- a/changes.md +++ b/changes.md @@ -30,10 +30,6 @@ This is very useful, but it probably should only be available in implicit child It's probably easy to understand why `el` and `parent` are instance only. But why `data`? Because it's really easy to shoot yourself in the foot when you use `data` in `Vue.extend()`. Non-primitive values will be shared by reference across all instances created from that constructor, and changing it from one instance will affect the state of all the others! It's a bit like shared properties on the prototype. In vanilla javascript, the proper way to initialize instance data is to do so in the constructor: `this.someData = {}`. Similarly in Vue, you can do so in the `created` hook by setting `this.$data.someData = {}`. -### new option: `syncData`. - -A side effect of the new scope/data model is that the `data` object being passed in is no longer mutated by default, because all its properties are copied into the scope instead, and the scope is now the source of truth. To sync changes to the scope back to the original data object, you need to now explicitly pass in `syncData: true` in the options. In most cases, this is not necessary, but you do need to be aware of this. - ### new option: `events`. When events are used extensively for cross-vm communication, the ready hook can get kinda messy. The new `events` option is similar to its Backbone equivalent, where you can declaratiely register a bunch of event listeners. From 8ba2da6e5d6fb7e5f7f2c869efd111fc38e3e3af Mon Sep 17 00:00:00 2001 From: Evan You Date: Thu, 7 Aug 2014 16:50:11 -0400 Subject: [PATCH 0111/1534] fix observer Vue instance check --- src/observe/observer.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/observe/observer.js b/src/observe/observer.js index 54f3b1dd885..1bbaacd35f4 100644 --- a/src/observe/observer.js +++ b/src/observe/observer.js @@ -90,7 +90,7 @@ Observer.create = function (value, options) { return value.$observer } if (_.isArray(value)) { return new Observer(value, ARRAY, options) - } else if (_.isObject(value) && !value._scope) { // avoid Vue instance + } else if (_.isObject(value) && !value.$scope) { // avoid Vue instance return new Observer(value, OBJECT, options) } } From 8602b3545db8a1c5f9c855b20a1e45dc77341638 Mon Sep 17 00:00:00 2001 From: Evan You Date: Thu, 7 Aug 2014 18:44:18 -0400 Subject: [PATCH 0112/1534] allow quoted arguments in diretives --- benchmarks/dir.js | 36 ++++++++++++++++++++++++ benchmarks/expression.js | 2 +- src/parse/directive.js | 18 ++++++++---- test/unit/specs/directive_parser_spec.js | 31 ++++++++++---------- 4 files changed, 65 insertions(+), 22 deletions(-) create mode 100644 benchmarks/dir.js diff --git a/benchmarks/dir.js b/benchmarks/dir.js new file mode 100644 index 00000000000..d8f3ef5bb77 --- /dev/null +++ b/benchmarks/dir.js @@ -0,0 +1,36 @@ +console.log('\nDirective Parser\n') + +var Cache = require('../src/cache') +var parse = require('../src/parse/directive').parse + +Cache.prototype.get = Cache.prototype.put = function () {} + +function bench (id, fn) { + var s = Date.now() + var max = i = 100000 + while (i--) { + fn() + } + var used = Date.now() - s + var ops = Math.round(16 / (used / max)) + console.log(id + ': ' + ops + ' ops/frame') +} + + +var side + +bench('simple path', function () { + side = parse('a.b.c') +}) + +bench('filters', function () { + side = parse('a.b.c | a | b | c') +}) + +bench('multi', function () { + side = parse('abc:ddd, bcd:eee, fsef:fff') +}) + +bench('complex exp', function () { + side = parse('test:a.b + { c: d} + "abced{fsefesf}ede"') +}) \ No newline at end of file diff --git a/benchmarks/expression.js b/benchmarks/expression.js index d2ba58249d0..b4297cd5611 100644 --- a/benchmarks/expression.js +++ b/benchmarks/expression.js @@ -31,5 +31,5 @@ bench('simple exp', function () { }) bench('complex exp', function () { - side = parse('a.b + c') + side = parse('a.b + { c: d} + "abced{fsefesf}ede"') }) \ No newline at end of file diff --git a/src/parse/directive.js b/src/parse/directive.js index 309eaa5ab8a..5470c631061 100644 --- a/src/parse/directive.js +++ b/src/parse/directive.js @@ -1,7 +1,7 @@ var Cache = require('../cache') var cache = new Cache(1000) -var ARG_RE = /^[\w\$-]+$/ -var FILTER_TOKEN_RE = /[^\s'"]+|'[^']+'|"[^"]+"/g +var argRE = /^[\w\$-]+$|^'[^']*'$|^"[^"]*"$/ +var filterTokenRE = /[^\s'"]+|'[^']+'|"[^"]+"/g /** * Parser state @@ -20,6 +20,7 @@ var dirs var dir var lastFilterIndex var arg +var argC /** * Push a directive object into the result Array @@ -46,7 +47,7 @@ function pushFilter () { var filter if (exp) { filter = {} - var tokens = exp.match(FILTER_TOKEN_RE) + var tokens = exp.match(filterTokenRE) filter.name = tokens[0] filter.args = tokens.length > 1 ? tokens.slice(1) : null } @@ -107,9 +108,16 @@ exports.parse = function (s) { } else if (c === ':' && !dir.expression && !dir.arg) { // argument arg = str.slice(begin, i).trim() - if (ARG_RE.test(arg)) { + // test for valid argument here + // since we may have caught stuff like first half of + // an object literal or a ternary expression. + if (argRE.test(arg)) { argIndex = i + 1 - dir.arg = arg + argC = arg.charAt(0) + // strip quotes + dir.arg = argC === '"' || argC === "'" + ? arg.slice(1, -1) + : arg } } else if (c === '|' && str.charAt(i + 1) !== '|' && str.charAt(i - 1) !== '|') { if (dir.expression === undefined) { diff --git a/test/unit/specs/directive_parser_spec.js b/test/unit/specs/directive_parser_spec.js index 3d806d6555d..3b6ddd8a73a 100644 --- a/test/unit/specs/directive_parser_spec.js +++ b/test/unit/specs/directive_parser_spec.js @@ -2,14 +2,14 @@ var parse = require('../../../src/parse/directive').parse describe('Directive Parser', function () { - it('exp', function () { + it('simple', function () { var res = parse('exp') expect(res.length).toBe(1) expect(res[0].expression).toBe('exp') expect(res[0].raw).toBe('exp') }) - it('arg:exp', function () { + it('with arg', function () { var res = parse('arg:exp') expect(res.length).toBe(1) expect(res[0].expression).toBe('exp') @@ -17,8 +17,7 @@ describe('Directive Parser', function () { expect(res[0].raw).toBe('arg:exp') }) - // filters - it('arg : exp | abc de | bcd', function () { + it('with filters', function () { var res = parse(' arg : exp | abc de | bcd') expect(res.length).toBe(1) expect(res[0].expression).toBe('exp') @@ -32,8 +31,7 @@ describe('Directive Parser', function () { expect(res[0].filters[1].args).toBeNull() }) - // double pipe - it('a || b | c', function () { + it('double pipe', function () { var res = parse('a || b | c') expect(res.length).toBe(1) expect(res[0].expression).toBe('a || b') @@ -43,16 +41,14 @@ describe('Directive Parser', function () { expect(res[0].filters[0].args).toBeNull() }) - // single quote + boolean - it('a ? \'b\' : c', function () { + it('single quote + boolean', function () { var res = parse('a ? \'b\' : c') expect(res.length).toBe(1) expect(res[0].expression).toBe('a ? \'b\' : c') expect(res[0].filters).toBeUndefined() }) - // double quote + boolean - it('"a:b:c||d|e|f" || d ? a : b', function () { + it('double quote + boolean', function () { var res = parse('"a:b:c||d|e|f" || d ? a : b') expect(res.length).toBe(1) expect(res[0].expression).toBe('"a:b:c||d|e|f" || d ? a : b') @@ -60,8 +56,7 @@ describe('Directive Parser', function () { expect(res[0].arg).toBeUndefined() }) - // multiple simple clause - it('a, b, c', function () { + it('multiple simple clauses', function () { var res = parse('a, b, c') expect(res.length).toBe(3) expect(res[0].expression).toBe('a') @@ -69,8 +64,7 @@ describe('Directive Parser', function () { expect(res[2].expression).toBe('c') }) - // multiple complex clause - it('a:b | c | j, d:e | f | k l, g:h | i', function () { + it('multiple complex clauses', function () { var res = parse('a:b | c | j, d:e | f | k l, g:h | i') expect(res.length).toBe(3) @@ -98,8 +92,7 @@ describe('Directive Parser', function () { expect(res[2].filters[0].args).toBeNull() }) - // super complex - it('click:test(c.indexOf(d,f),"e,f"), input: d || [e,f], ok:{a:1,b:2}', function () { + it('nexted function calls + array/object literals', function () { var res = parse('click:test(c.indexOf(d,f),"e,f"), input: d || [e,f], ok:{a:1,b:2}') expect(res.length).toBe(3) expect(res[0].arg).toBe('click') @@ -111,6 +104,12 @@ describe('Directive Parser', function () { expect(res[2].expression).toBe('{a:1,b:2}') }) + it('quoted arguments', function () { + var res = parse('"xlink:href":a?"fsef":ff') + expect(res.length).toBe(1) + expect(res[0].arg).toBe('xlink:href') + }) + it('cache', function () { var res1 = parse('a || b | c') var res2 = parse('a || b | c') From 763f2b60c1ed59ac84ce1faa78affe81940310b0 Mon Sep 17 00:00:00 2001 From: Evan You Date: Fri, 8 Aug 2014 01:05:51 -0400 Subject: [PATCH 0113/1534] separate directive & watcher implementation --- src/batcher.js | 8 +- src/directive.js | 285 ++++++++----------------------------------- src/instance/init.js | 4 +- src/util/env.js | 13 +- src/watcher.js | 237 +++++++++++++++++++++++++++++++++++ 5 files changed, 307 insertions(+), 240 deletions(-) create mode 100644 src/watcher.js diff --git a/src/batcher.js b/src/batcher.js index 698e66a525b..501ec767c36 100644 --- a/src/batcher.js +++ b/src/batcher.js @@ -1,7 +1,7 @@ var _ = require('./util') /** - * The Batcher maintains a job queue to be executed + * The Batcher maintains a job queue to be run * async on the next event loop. */ @@ -21,7 +21,7 @@ var p = Batcher.prototype * properties: * - {String|Number} id * - {Boolean} override - * - {Function} execute + * - {Function} run */ p.push = function (job) { @@ -54,11 +54,11 @@ p.flush = function () { this._preFlush() } // do not cache length because more jobs might be pushed - // as we execute existing jobs + // as we run existing jobs for (var i = 0; i < this.queue.length; i++) { var job = this.queue[i] if (!job.cancelled) { - job.execute() + job.run() } } this.reset() diff --git a/src/directive.js b/src/directive.js index a3f58897431..590a2e22751 100644 --- a/src/directive.js +++ b/src/directive.js @@ -1,10 +1,5 @@ var _ = require('./util') -var Observer = require('./observe/observer') -var expParser = require('./parse/expression') -var Batcher = require('./batcher') - -var batcher = new Batcher() -var uid = 0 +var Watcher = require('./watcher') /** * A directive links a DOM element with a piece of data, which can @@ -12,56 +7,40 @@ var uid = 0 * a list of dependencies (Bindings) and refreshes the list during * its getter evaluation. * - * @param {String} type + * @param {String} name * @param {Node} el * @param {Vue} vm * @param {Object} descriptor * - {String} expression * - {String} [arg] * - {Array} [filters] - * @param {Object|Function} definition - * - {Function} update - * - {Function} [bind] - * - {Function} [unbind] - * - {Boolean} [literal] - * - {Boolean} [twoway] - * - {Array} [params] * @constructor */ -function Directive (type, el, vm, descriptor, definition) { +function Directive (name, el, vm, descriptor) { // public - this.type = type + this.name = name this.el = el this.vm = vm - this.value = undefined this.arg = descriptor.arg - this.expression = descriptor.expression.trim() + this.expression = descriptor.expression this.filters = descriptor.filters // private - this._id = ++uid this._locked = false this._unbound = false - this._deps = Object.create(null) - this._newDeps = Object.create(null) - // init definition - this._initDef(definition) - - if (this.expression && !this.isLiteral) { - // parse expression - var res = expParser.parse(this.expression, this.twoway) - this._getter = res.get - this._setter = res.set - // init dependencies - this._initDeps(res.paths) - // init filters - this._initFilters() - // init methods that need to be context-bound - this._initBoundMethods() - // update for the first time - this._realUpdate(true) + this._initDef() + + if (this.expression && !this.isLiteral && this.update) { + this._watcher = new Watcher( + vm, + this.expression, + this._update, + this, // callback context + this.filters, + this.twoway + ) } } @@ -71,8 +50,9 @@ var p = Directive.prototype * Initialize the directive instance's definition. */ -p._initDef = function (definition) { - _.extend(this, definition) +p._initDef = function () { + var def = this.vm.$options.directives[this.name] + _.extend(this, def) // init params var el = this.el var attrs = this.paramAttributes @@ -83,151 +63,51 @@ p._initDef = function (definition) { el.removeAttribute(p) }) } - // call bind hook - if (this.bind) { - this.bind() - } } /** - * Initialize read and write filters - */ - -p._initFilters = function () { - if (!this.filters) { - return - } - var self = this - var vm = this.vm - var registry = vm.$options.filters - this.filters.forEach(function (f) { - var def = registry[f.name] - var args = f.args - var read, write - if (typeof def === 'function') { - read = def - } else { - read = def.read - write = def.write - } - if (read) { - if (!self._readFilters) { - self._readFilters = [] - } - self._readFilters.push(function (value) { - return args - ? read.apply(vm, [value].concat(args)) - : read.call(vm, value) - }) - } - if (write) { - if (!self._writeFilters) { - self._writeFilters = [] - } - self._writeFilters.push(function (value) { - return args - ? write.apply(vm, [value, self.value].concat(args)) - : write.call(vm, value, self.value) - }) - } - }) -} - -/** - * Add root level path as a dependency. - * this is specifically for the case where the expression - * references a non-existing root level path, and later - * that path is created with `vm.$add`. - * - * e.g. in "a && a.b", if `a` is not present at compilation, - * the directive will end up with no dependency at all and - * never gets updated. + * Apply the directive, call definition bind() if present, + * and call first update(). * - * @param {Array} paths + * @private */ -p._initDeps = function (paths) { - var i = paths.length - while (i--) { - this._addDep(paths[i]) - } - this._deps = this._newDeps -} - -/** - * Initialize a few methods that need to be context-bound - * so we don't have to create them ad-hoc everytime - */ - -p._initBoundMethods = function () { - var self = this - - /** - * Unlock function used in .set() - */ - - this._unlock = function () { - self._locked = false +p._bind = function () { + var value = this._watcher.value + if (this.bind) { + this.bind(value) } - - /** - * real updater with bound context - * to be pushed into batcher queue - * - * @param {Boolean} init - */ - - this._realUpdate = function (init) { - if (self._unbound) { - return - } - var value = self.get() - if ( - (typeof value === 'object' && value !== null) || - value !== self.value || - init - ) { - self.value = value - if (self.update) { - self.update(value) - } - } + if (this.update) { + this.update(value) } } /** - * Add a binding dependency to this directive. + * Callback for the watcher. + * Check locked or not before calling definition update. * - * @param {String} path + * @param {*} value + * @private */ -p._addDep = function (path) { - var vm = this.vm - var newDeps = this._newDeps - var oldDeps = this._deps - if (!newDeps[path]) { - newDeps[path] = true - if (!oldDeps[path]) { - var binding = - vm._getBindingAt(path) || - vm._createBindingAt(path) - binding._addSub(this) - } +p._update = function (value) { + if (!this._locked) { + this.update(value) } } /** - * Evaluate the getter, and re-collect dependencies. + * Teardown the watcher and call unbind. + * + * @private */ -p.get = function () { - this._beforeGet() - var value = this._getter.call(this.vm, this.vm.$scope) - if (this._readFilters) { - value = this._applyFilters(value, -1) +p._teardown = function () { + if (this.unbind) { + this.unbind() } - this._afterGet() - return value + this._watcher.teardown() + this._unbound = true } /** @@ -235,78 +115,19 @@ p.get = function () { * This should only be used in two-way bindings like v-model. * * @param {*} value + * @param {Boolean} lock - prevent wrtie triggering update. + * @public */ -p.set = function (value) { - if (this._setter) { - this._locked = true - if (this._writeFilters) { - value = this._applyFilters(value, 1) +p.set = function (value, lock) { + if (this.twoway) { + if (lock) { + this._locked = true + } + this._watcher.set(value) + if (lock) { + _.nextTick(this._unlock, this) } - this._setter.call(this.vm, this.vm.$scope, value) - _.nextTick(this._unlock) - } -} - -/** - * Prepare for dependency collection. - */ - -p._beforeGet = function () { - Observer.emitGet = true - this.vm._targetDir = this - this._newDeps = Object.create(null) -} - -/** - * Clean up for dependency collection. - */ - -p._afterGet = function () { - this.vm._targetDir = null - Observer.emitGet = false - _.extend(this._newDeps, this._deps) - this._deps = this._newDeps -} - -/** - * The exposed subscriber interface. - * Will be called when a dependency changes. - */ - -p._update = function () { - batcher.push({ - id: this._id, - execute: this._realUpdate - }) -} - -/** - * Apply filters to a value. - * - * @param {*} value - * @param {Number} direction - -1 = read, 1 = write. - */ - -p._applyFilters = function (value, direction) { - var filters = direction > 0 - ? this._writeFilters - : this._readFilters - for (var i = 0, l = filters.length; i < l; i++) { - value = filters[i](value) - } - return value -} - -/** - * Remove self from all dependencies' subcriber list. - */ - -p._teardown = function () { - if (this.unbind) this.unbind() - this._unbound = true - for (var p in this._deps) { - this._deps[p]._removeSub(this) } } diff --git a/src/instance/init.js b/src/instance/init.js index 3cb08436acc..7caf3300251 100644 --- a/src/instance/init.js +++ b/src/instance/init.js @@ -21,8 +21,8 @@ exports._init = function (options) { this._isDestroyed = false this._rawContent = null this._emitter = new Emitter(this) - // the current target directive for dependency collection - this._targetDir = null + // the current target watcher for dependency collection + this._currentWatcher = null // setup parent relationship this.$parent = options.parent diff --git a/src/util/env.js b/src/util/env.js index 7f73e70504f..c23ab92cfa9 100644 --- a/src/util/env.js +++ b/src/util/env.js @@ -13,15 +13,24 @@ var inBrowser = exports.inBrowser = /** * Defer a task to the start of the next event loop * - * @param {Function} fn + * @param {Function} cb + * @param {Object} ctx */ -exports.nextTick = inBrowser +var defer = inBrowser ? (window.requestAnimationFrame || window.webkitRequestAnimationFrame || setTimeout) : setTimeout +exports.nextTick = function (cb, ctx) { + if (ctx) { + defer(function () { cb.call(ctx) }, 0) + } else { + defer(cb, 0) + } +} + /** * Detect if we are in IE9... * diff --git a/src/watcher.js b/src/watcher.js new file mode 100644 index 00000000000..347bc583173 --- /dev/null +++ b/src/watcher.js @@ -0,0 +1,237 @@ +var _ = require('./util') +var Observer = require('./observe/observer') +var expParser = require('./parse/expression') +var Batcher = require('./batcher') + +var batcher = new Batcher() +var uid = 0 + +/** + * A watcher parses an expression, collects dependencies, + * and fires callback when the expression value changes. + * This is used for both the $watch() api and directives. + * + * @param {Vue} vm + * @param {String} expression + * @param {Function} cb + * @param {Object} [ctx] + * @param {Array} [filters] + * @param {Boolean} [needSet] + * @constructor + */ + +function Watcher (vm, expression, cb, ctx, filters, needSet) { + this.vm = vm + this.expression = expression + this.cb = cb // change callback + this.ctx = ctx || vm // change callback context + this.id = ++uid // uid for batching + this.value = undefined + this.active = true + this.deps = Object.create(null) + this.newDeps = Object.create(null) + // setup filters if any. + this.initFilters(filters) + // parse expression for getter/setter + var res = expParser.parse(expression, needSet) + this.getter = res.get + this.setter = res.set + this.initDeps(res.paths) +} + +var p = Watcher.prototype + +/** + * Initialize the value and dependencies. + * + * Here we need to add root level path as dependencies. + * This is specifically for the case where the expression + * references a non-existing root level path, and later + * that path is created with `vm.$add`. + * + * e.g. in "a && a.b", if `a` is not present at compilation, + * the directive will end up with no dependency at all and + * never gets updated. + * + * @param {Array} paths + */ + +p.initDeps = function (paths) { + var i = paths.length + while (i--) { + this.addDep(paths[i]) + } + this.value = this.get() +} + +/** + * Initialize read and write filters. + * We delegate directive filters here to the watcher + * because they need to be included in the dependency + * collection process. + * + * @param {Array} filters + */ + +p.initFilters = function (filters) { + if (!filters) { + return + } + var self = this + var vm = this.vm + var registry = vm.$options.filters + filters.forEach(function (f) { + var def = registry[f.name] + var args = f.args + var read, write + if (typeof def === 'function') { + read = def + } else { + read = def.read + write = def.write + } + if (read) { + if (!self.readFilters) { + self.readFilters = [] + } + self.readFilters.push(function (value) { + return args + ? read.apply(vm, [value].concat(args)) + : read.call(vm, value) + }) + } + if (write) { + if (!self.writeFilters) { + self.writeFilters = [] + } + self.writeFilters.push(function (value) { + return args + ? write.apply(vm, [value, self.value].concat(args)) + : write.call(vm, value, self.value) + }) + } + }) +} + +/** + * Add a binding dependency to this directive. + * + * @param {String} path + */ + +p.addDep = function (path) { + var vm = this.vm + var newDeps = this.newDeps + var oldDeps = this.deps + if (!newDeps[path]) { + newDeps[path] = true + if (!oldDeps[path]) { + var binding = + vm._getBindingAt(path) || + vm._createBindingAt(path) + binding._addSub(this) + } + } +} + +/** + * Evaluate the getter, and re-collect dependencies. + */ + +p.get = function () { + this.beforeGet() + var value = this.getter.call(this.vm, this.vm.$scope) + if (this.readFilters) { + value = applyFilters(value, this.readFilters) + } + this.afterGet() + return value +} + +/** + * Set the corresponding value with the setter. + * + * @param {*} value + */ + +p.set = function (value) { + if (this.writeFilters) { + value = applyFilters(value, this.writeFilters) + } + this.setter.call(this.vm, this.vm.$scope, value) +} + +/** + * Prepare for dependency collection. + */ + +p.beforeGet = function () { + Observer.emitGet = true + this.vm._currentWatcher = this + this.newDeps = Object.create(null) +} + +/** + * Clean up for dependency collection. + */ + +p.afterGet = function () { + this.vm._currentWatcher = null + Observer.emitGet = false + _.extend(this.newDeps, this.deps) + this.deps = this.newDeps +} + +/** + * Subscriber interface. + * Will be called when a dependency changes. + */ + +p._update = function () { + batcher.push(this) +} + +/** + * Batcher job interface. + * Will be called by the batcher. + */ + +p.run = function () { + if (this.active) { + var value = this.get() + if ( + (typeof value === 'object' && value !== null) || + value !== this.value + ) { + this.cb.call(this.ctx, value, this.value) + this.value = value + } + } +} + +/** + * Remove self from all dependencies' subcriber list. + */ + +p.teardown = function () { + this.active = false + for (var p in this.deps) { + this.deps[p].removeSub(this) + } +} + +/** + * Apply filters to a value + * + * @param {*} value + * @param {Array} filters + */ + +function applyFilters (value, filters) { + for (var i = 0, l = filters.length; i < l; i++) { + value = filters[i](value) + } + return value +} + +module.exports = Watcher \ No newline at end of file From 926933922184ea6a22df3e60d263420c3bb3470e Mon Sep 17 00:00:00 2001 From: Evan You Date: Fri, 8 Aug 2014 01:50:44 -0400 Subject: [PATCH 0114/1534] no longer need to prefix update with _ --- src/binding.js | 2 +- src/watcher.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/binding.js b/src/binding.js index a068bc1acca..506aeff82c2 100644 --- a/src/binding.js +++ b/src/binding.js @@ -54,7 +54,7 @@ p._removeSub = function (sub) { p._notify = function () { for (var i = 0, l = this._subs.length; i < l; i++) { - this._subs[i]._update() + this._subs[i].update() } } diff --git a/src/watcher.js b/src/watcher.js index 347bc583173..d4096852727 100644 --- a/src/watcher.js +++ b/src/watcher.js @@ -187,7 +187,7 @@ p.afterGet = function () { * Will be called when a dependency changes. */ -p._update = function () { +p.update = function () { batcher.push(this) } From eaf7216613c3f46ca6515bf771ad7a61ab1a7a61 Mon Sep 17 00:00:00 2001 From: Evan You Date: Fri, 8 Aug 2014 17:56:21 -0400 Subject: [PATCH 0115/1534] test for watcher --- src/instance/bindings.js | 10 +- src/watcher.js | 3 +- test/unit/lib/jasmine-async.js | 65 +++++++++ test/unit/runner.html | 1 + test/unit/specs/watcher_spec.js | 234 ++++++++++++++++++++++++++++++++ 5 files changed, 307 insertions(+), 6 deletions(-) create mode 100644 test/unit/lib/jasmine-async.js create mode 100644 test/unit/specs/watcher_spec.js diff --git a/src/instance/bindings.js b/src/instance/bindings.js index 7fa6e8442f3..ee77927fc74 100644 --- a/src/instance/bindings.js +++ b/src/instance/bindings.js @@ -112,10 +112,10 @@ exports._updateAdd = function (path) { */ exports._collectDep = function (path) { - var directive = this._targetDir - // the get event might have come from a child vm's directive - // so this._targetDir is not guarunteed to be defined - if (directive) { - directive._addDep(path) + var watcher = this._currentWatcher + // the get event might have come from a child vm's watcher + // so this._currentWatcher is not guarunteed to be defined + if (watcher) { + watcher.addDep(path) } } \ No newline at end of file diff --git a/src/watcher.js b/src/watcher.js index d4096852727..e8732df248b 100644 --- a/src/watcher.js +++ b/src/watcher.js @@ -203,8 +203,9 @@ p.run = function () { (typeof value === 'object' && value !== null) || value !== this.value ) { - this.cb.call(this.ctx, value, this.value) + var oldValue = this.value this.value = value + this.cb.call(this.ctx, value, oldValue) } } } diff --git a/test/unit/lib/jasmine-async.js b/test/unit/lib/jasmine-async.js new file mode 100644 index 00000000000..a5cda19e723 --- /dev/null +++ b/test/unit/lib/jasmine-async.js @@ -0,0 +1,65 @@ +// https://github.com/mout/mout/blob/master/tests/lib/jasmine/jasmine.async.js + +// borrowed from jasmine-node +// makes async tests easier +// https://github.com/mhevery/jasmine-node +// released under MIT license +(function() { + var withoutAsync = {}; + + function monkeyPatch(jasmineFunction) { + withoutAsync[jasmineFunction] = jasmine.Env.prototype[jasmineFunction]; + return jasmine.Env.prototype[jasmineFunction] = function() { + var args = Array.prototype.slice.call(arguments, 0); + var timeout = null; + if (isLastArgumentATimeout(args)) { + timeout = args.pop(); + } + if (isLastArgumentAnAsyncSpecFunction(args)) + { + var specFunction = args.pop(); + args.push(function() { + return asyncSpec(specFunction, this, timeout); + }); + } + return withoutAsync[jasmineFunction].apply(this, args); + }; + } + + // since oldIE doesn't support forEach + monkeyPatch('it'); + monkeyPatch('beforeEach'); + monkeyPatch('afterEach'); + + function isLastArgumentATimeout(args) + { + return args.length > 0 && (typeof args[args.length-1]) === "number"; + } + + function isLastArgumentAnAsyncSpecFunction(args) + { + return args.length > 0 && (typeof args[args.length-1]) === "function" && args[args.length-1].length > 0; + } + + function asyncSpec(specFunction, spec, timeout) { + if (timeout == null) timeout = jasmine.DEFAULT_TIMEOUT_INTERVAL || 1000; + var done = false; + spec.runs(function() { + try { + return specFunction(function(error) { + done = true; + if (error != null) return spec.fail(error); + }); + } catch (e) { + done = true; + throw e; + } + }); + return spec.waitsFor(function() { + if (done === true) { + return true; + } + }, "spec to complete", timeout); + }; + +}).call(this); \ No newline at end of file diff --git a/test/unit/runner.html b/test/unit/runner.html index 42fea8411a7..b0d48364d34 100644 --- a/test/unit/runner.html +++ b/test/unit/runner.html @@ -7,6 +7,7 @@ + - + - diff --git a/test/unit/specs/batcher_spec.js b/test/unit/specs/batcher_spec.js new file mode 100644 index 00000000000..8739ee75f31 --- /dev/null +++ b/test/unit/specs/batcher_spec.js @@ -0,0 +1,65 @@ +var Batcher = require('../../../src/batcher') +var nextTick = require('../../../src/util').nextTick + +describe('Batcher', function () { + + var batcher = new Batcher() + var spy + + beforeEach(function () { + spy = jasmine.createSpy('batcher') + }) + + it('push', function (done) { + batcher.push({ + run: spy + }) + nextTick(function () { + expect(spy.calls.count()).toBe(1) + done() + }) + }) + + it('dedup', function (done) { + batcher.push({ + id: 1, + run: spy + }) + batcher.push({ + id: 1, + run: spy + }) + nextTick(function () { + expect(spy.calls.count()).toBe(1) + done() + }) + }) + + it('override', function (done) { + var spy2 = jasmine.createSpy('batcher') + batcher.push({ + id: 1, + run: spy + }) + batcher.push({ + id: 1, + run: spy2, + override: true + }) + nextTick(function () { + expect(spy.calls.count()).toBe(0) + expect(spy2.calls.count()).toBe(1) + done() + }) + }) + + it('preFlush hook', function (done) { + batcher._preFlush = spy + batcher.push({ run: function () {}}) + nextTick(function () { + expect(spy.calls.count()).toBe(1) + done() + }) + }) + +}) \ No newline at end of file diff --git a/test/unit/specs/observer_spec.js b/test/unit/specs/observer_spec.js index f673192a652..065f2c7fe05 100644 --- a/test/unit/specs/observer_spec.js +++ b/test/unit/specs/observer_spec.js @@ -31,12 +31,12 @@ describe('Observer', function () { var t = obj.a expect(spy).toHaveBeenCalledWith('a', u, u) - expect(spy.callCount).toBe(1) + expect(spy.calls.count()).toBe(1) t = obj.b.c expect(spy).toHaveBeenCalledWith('b', u, u) expect(spy).toHaveBeenCalledWith('b.c', u, u) - expect(spy.callCount).toBe(3) + expect(spy.calls.count()).toBe(3) Observer.emitGet = false }) @@ -53,21 +53,21 @@ describe('Observer', function () { obj.a = 3 expect(spy).toHaveBeenCalledWith('a', 3, u) - expect(spy.callCount).toBe(1) + expect(spy.calls.count()).toBe(1) obj.b.c = 4 expect(spy).toHaveBeenCalledWith('b.c', 4, u) - expect(spy.callCount).toBe(2) + expect(spy.calls.count()).toBe(2) // swap set var newB = { c: 5 } obj.b = newB expect(spy).toHaveBeenCalledWith('b', newB, u) - expect(spy.callCount).toBe(3) + expect(spy.calls.count()).toBe(3) // same value set should not emit events obj.a = 3 - expect(spy.callCount).toBe(3) + expect(spy.calls.count()).toBe(3) }) it('ignore prefix', function () { @@ -79,7 +79,7 @@ describe('Observer', function () { ob.on('set', spy) obj._test = 234 obj.$test = 345 - expect(spy.callCount).toBe(0) + expect(spy.calls.count()).toBe(0) }) it('ignore accessors', function () { @@ -118,7 +118,7 @@ describe('Observer', function () { var t = obj.arr[0].a expect(spy).toHaveBeenCalledWith('arr', u, u) expect(spy).toHaveBeenCalledWith('arr.0.a', u, u) - expect(spy.callCount).toBe(2) + expect(spy.calls.count()).toBe(2) Observer.emitGet = false }) @@ -144,9 +144,9 @@ describe('Observer', function () { var ob = Observer.create(arr) ob.on('mutate', spy) arr.push({a:3}) - expect(spy.mostRecentCall.args[0]).toBe('') - expect(spy.mostRecentCall.args[1]).toBe(arr) - var mutation = spy.mostRecentCall.args[2] + expect(spy.calls.mostRecent().args[0]).toBe('') + expect(spy.calls.mostRecent().args[1]).toBe(arr) + var mutation = spy.calls.mostRecent().args[2] expect(mutation).toBeDefined() expect(mutation.method).toBe('push') expect(mutation.index).toBe(2) @@ -165,9 +165,9 @@ describe('Observer', function () { var ob = Observer.create(arr) ob.on('mutate', spy) arr.pop() - expect(spy.mostRecentCall.args[0]).toBe('') - expect(spy.mostRecentCall.args[1]).toBe(arr) - var mutation = spy.mostRecentCall.args[2] + expect(spy.calls.mostRecent().args[0]).toBe('') + expect(spy.calls.mostRecent().args[1]).toBe(arr) + var mutation = spy.calls.mostRecent().args[2] expect(mutation).toBeDefined() expect(mutation.method).toBe('pop') expect(mutation.index).toBe(1) @@ -182,9 +182,9 @@ describe('Observer', function () { var ob = Observer.create(arr) ob.on('mutate', spy) arr.shift() - expect(spy.mostRecentCall.args[0]).toBe('') - expect(spy.mostRecentCall.args[1]).toBe(arr) - var mutation = spy.mostRecentCall.args[2] + expect(spy.calls.mostRecent().args[0]).toBe('') + expect(spy.calls.mostRecent().args[1]).toBe(arr) + var mutation = spy.calls.mostRecent().args[2] expect(mutation).toBeDefined() expect(mutation.method).toBe('shift') expect(mutation.index).toBe(0) @@ -203,9 +203,9 @@ describe('Observer', function () { var ob = Observer.create(arr) ob.on('mutate', spy) arr.unshift(unshifted) - expect(spy.mostRecentCall.args[0]).toBe('') - expect(spy.mostRecentCall.args[1]).toBe(arr) - var mutation = spy.mostRecentCall.args[2] + expect(spy.calls.mostRecent().args[0]).toBe('') + expect(spy.calls.mostRecent().args[1]).toBe(arr) + var mutation = spy.calls.mostRecent().args[2] expect(mutation).toBeDefined() expect(mutation.method).toBe('unshift') expect(mutation.index).toBe(0) @@ -225,9 +225,9 @@ describe('Observer', function () { var ob = Observer.create(arr) ob.on('mutate', spy) arr.splice(1, 1, inserted) - expect(spy.mostRecentCall.args[0]).toBe('') - expect(spy.mostRecentCall.args[1]).toBe(arr) - var mutation = spy.mostRecentCall.args[2] + expect(spy.calls.mostRecent().args[0]).toBe('') + expect(spy.calls.mostRecent().args[1]).toBe(arr) + var mutation = spy.calls.mostRecent().args[2] expect(mutation).toBeDefined() expect(mutation.method).toBe('splice') expect(mutation.index).toBe(1) @@ -248,9 +248,9 @@ describe('Observer', function () { arr.sort(function (a, b) { return a.a < b.a ? 1 : -1 }) - expect(spy.mostRecentCall.args[0]).toBe('') - expect(spy.mostRecentCall.args[1]).toBe(arr) - var mutation = spy.mostRecentCall.args[2] + expect(spy.calls.mostRecent().args[0]).toBe('') + expect(spy.calls.mostRecent().args[1]).toBe(arr) + var mutation = spy.calls.mostRecent().args[2] expect(mutation).toBeDefined() expect(mutation.method).toBe('sort') expect(mutation.index).toBeUndefined() @@ -267,9 +267,9 @@ describe('Observer', function () { var ob = Observer.create(arr) ob.on('mutate', spy) arr.reverse() - expect(spy.mostRecentCall.args[0]).toBe('') - expect(spy.mostRecentCall.args[1]).toBe(arr) - var mutation = spy.mostRecentCall.args[2] + expect(spy.calls.mostRecent().args[0]).toBe('') + expect(spy.calls.mostRecent().args[1]).toBe(arr) + var mutation = spy.calls.mostRecent().args[2] expect(mutation).toBeDefined() expect(mutation.method).toBe('reverse') expect(mutation.index).toBeUndefined() @@ -288,7 +288,7 @@ describe('Observer', function () { // ignore existing keys obj.$add('a', 123) - expect(spy.callCount).toBe(0) + expect(spy.calls.count()).toBe(0) // add event var add = {d:2} @@ -308,7 +308,7 @@ describe('Observer', function () { // ignore non-present key obj.$delete('c') - expect(spy.callCount).toBe(0) + expect(spy.calls.count()).toBe(0) obj.a.$delete('b') expect(spy).toHaveBeenCalledWith('a.b', u, u) @@ -322,9 +322,9 @@ describe('Observer', function () { var removed = arr[1] arr.$set(1, inserted) - expect(spy.mostRecentCall.args[0]).toBe('') - expect(spy.mostRecentCall.args[1]).toBe(arr) - var mutation = spy.mostRecentCall.args[2] + expect(spy.calls.mostRecent().args[0]).toBe('') + expect(spy.calls.mostRecent().args[1]).toBe(arr) + var mutation = spy.calls.mostRecent().args[2] expect(mutation).toBeDefined() expect(mutation.method).toBe('splice') expect(mutation.index).toBe(1) @@ -354,9 +354,9 @@ describe('Observer', function () { ob.on('mutate', spy) var removed = arr.$remove(0) - expect(spy.mostRecentCall.args[0]).toBe('') - expect(spy.mostRecentCall.args[1]).toBe(arr) - var mutation = spy.mostRecentCall.args[2] + expect(spy.calls.mostRecent().args[0]).toBe('') + expect(spy.calls.mostRecent().args[1]).toBe(arr) + var mutation = spy.calls.mostRecent().args[2] expect(mutation).toBeDefined() expect(mutation.method).toBe('splice') expect(mutation.index).toBe(0) @@ -375,9 +375,9 @@ describe('Observer', function () { ob.on('mutate', spy) var removed = arr.$remove(arr[0]) - expect(spy.mostRecentCall.args[0]).toBe('') - expect(spy.mostRecentCall.args[1]).toBe(arr) - var mutation = spy.mostRecentCall.args[2] + expect(spy.calls.mostRecent().args[0]).toBe('') + expect(spy.calls.mostRecent().args[1]).toBe(arr) + var mutation = spy.calls.mostRecent().args[2] expect(mutation).toBeDefined() expect(mutation.method).toBe('splice') expect(mutation.index).toBe(0) @@ -399,13 +399,13 @@ describe('Observer', function () { obA.on('set', spy) obB.on('set', spy) obj.a = 2 - expect(spy.callCount).toBe(2) + expect(spy.calls.count()).toBe(2) expect(spy).toHaveBeenCalledWith('child1.a', 2, u) expect(spy).toHaveBeenCalledWith('child2.a', 2, u) // test unobserve parentA.child1 = null obj.a = 3 - expect(spy.callCount).toBe(4) + expect(spy.calls.count()).toBe(4) expect(spy).toHaveBeenCalledWith('child1', null, u) expect(spy).toHaveBeenCalledWith('child2.a', 3, u) }) diff --git a/test/unit/specs/scope_spec.js b/test/unit/specs/scope_spec.js index f3dce1e7f58..861cd5270f3 100644 --- a/test/unit/specs/scope_spec.js +++ b/test/unit/specs/scope_spec.js @@ -37,12 +37,12 @@ describe('Scope', function () { // set on scope vm.$scope.a = 2 - expect(spy.callCount).toBe(1) + expect(spy.calls.count()).toBe(1) expect(spy).toHaveBeenCalledWith('a', 2, u) // set on vm vm.b.c = 3 - expect(spy.callCount).toBe(2) + expect(spy.calls.count()).toBe(2) expect(spy).toHaveBeenCalledWith('b.c', 3, u) }) @@ -54,12 +54,12 @@ describe('Scope', function () { // add on scope vm.$scope.$add('c', 123) - expect(spy.callCount).toBe(1) + expect(spy.calls.count()).toBe(1) expect(spy).toHaveBeenCalledWith('c', 123, u) // delete on scope vm.$scope.$delete('c') - expect(spy.callCount).toBe(2) + expect(spy.calls.count()).toBe(2) expect(spy).toHaveBeenCalledWith('c', u, u) // vm $add/$delete are tested in the api suite @@ -161,27 +161,27 @@ describe('Scope', function () { var spy = jasmine.createSpy('inheritance') child._observer.on('set', spy) parent.c = 'c changed' - expect(spy.callCount).toBe(1) + expect(spy.calls.count()).toBe(1) expect(spy).toHaveBeenCalledWith('c', 'c changed', u) spy = jasmine.createSpy('inheritance') child._observer.on('add', spy) parent.$scope.$add('e', 123) - expect(spy.callCount).toBe(1) + expect(spy.calls.count()).toBe(1) expect(spy).toHaveBeenCalledWith('e', 123, u) spy = jasmine.createSpy('inheritance') child._observer.on('delete', spy) parent.$scope.$delete('e') - expect(spy.callCount).toBe(1) + expect(spy.calls.count()).toBe(1) expect(spy).toHaveBeenCalledWith('e', u, u) spy = jasmine.createSpy('inheritance') child._observer.on('mutate', spy) parent.arr.reverse() - expect(spy.mostRecentCall.args[0]).toBe('arr') - expect(spy.mostRecentCall.args[1]).toBe(parent.arr) - expect(spy.mostRecentCall.args[2].method).toBe('reverse') + expect(spy.calls.mostRecent().args[0]).toBe('arr') + expect(spy.calls.mostRecent().args[1]).toBe(parent.arr) + expect(spy.calls.mostRecent().args[2].method).toBe('reverse') }) @@ -191,7 +191,7 @@ describe('Scope', function () { var spy = jasmine.createSpy('inheritance') child._observer.on('set', spy) parent.a = 'a changed' - expect(spy.callCount).toBe(0) + expect(spy.calls.count()).toBe(0) }) }) @@ -220,10 +220,10 @@ describe('Scope', function () { // make sure data sync is working expect(parent.arr[0].a).toBe(3) - expect(parentSpy.callCount).toBe(1) + expect(parentSpy.calls.count()).toBe(1) expect(parentSpy).toHaveBeenCalledWith('arr.0.a', 3, u) - expect(childSpy.callCount).toBe(2) + expect(childSpy.calls.count()).toBe(2) expect(childSpy).toHaveBeenCalledWith('a', 3, u) expect(childSpy).toHaveBeenCalledWith('arr.0.a', 3, u) }) @@ -257,7 +257,7 @@ describe('Scope', function () { it('should unsync old data', function () { expect(vm.hasOwnProperty('c')).toBe(false) vm.a = 3 - expect(oldDataSpy.callCount).toBe(0) + expect(oldDataSpy.calls.count()).toBe(0) expect(oldData.a).toBe(1) expect(newData.a).toBe(3) }) @@ -279,7 +279,7 @@ describe('Scope', function () { it('should stop relaying parent events', function () { child._teardownScope() parent.a = 234 - expect(spy.callCount).toBe(0) + expect(spy.calls.count()).toBe(0) expect(child.$scope).toBeNull() }) }) diff --git a/test/unit/specs/util_spec.js b/test/unit/specs/util_spec.js index ae6a344b387..c14b77a50e0 100644 --- a/test/unit/specs/util_spec.js +++ b/test/unit/specs/util_spec.js @@ -201,7 +201,7 @@ describe('Util', function () { }) } - if (typeof console !== undefined) { + if (typeof console !== 'undefined') { describe('Debug', function () { @@ -222,7 +222,7 @@ describe('Util', function () { it('not log when debug is false', function () { config.debug = false _.log('bye', 'world') - expect(console.log.callCount).toBe(0) + expect(console.log.calls.count()).toBe(0) }) it('warn when silent is false', function () { @@ -234,7 +234,7 @@ describe('Util', function () { it('not warn when silent is ture', function () { config.silent = true _.warn('oops', 'ops') - expect(console.warn.callCount).toBe(0) + expect(console.warn.calls.count()).toBe(0) }) if (console.trace) { diff --git a/test/unit/specs/watcher_spec.js b/test/unit/specs/watcher_spec.js index f28e6bbaa6b..2f3cd53f0ba 100644 --- a/test/unit/specs/watcher_spec.js +++ b/test/unit/specs/watcher_spec.js @@ -1,5 +1,5 @@ var Vue = require('../../../src/vue') -var then = Vue.nextTick +var nextTick = Vue.nextTick var Watcher = require('../../../src/watcher') describe('Watcher', function () { @@ -24,11 +24,11 @@ describe('Watcher', function () { var watcher = new Watcher(vm, 'b.c', spy) expect(watcher.value).toBe(2) vm.b.c = 3 - then(function () { + nextTick(function () { expect(watcher.value).toBe(3) expect(spy).toHaveBeenCalledWith(3, 2) vm.b = { c: 4 } // swapping the object - then(function () { + nextTick(function () { expect(watcher.value).toBe(4) expect(spy).toHaveBeenCalledWith(4, 3) done() @@ -40,11 +40,11 @@ describe('Watcher', function () { var watcher = new Watcher(vm, 'b["c"]', spy) expect(watcher.value).toBe(2) vm.b.c = 3 - then(function () { + nextTick(function () { expect(watcher.value).toBe(3) expect(spy).toHaveBeenCalledWith(3, 2) vm.b = { c: 4 } // swapping the object - then(function () { + nextTick(function () { expect(watcher.value).toBe(4) expect(spy).toHaveBeenCalledWith(4, 3) done() @@ -56,11 +56,11 @@ describe('Watcher', function () { var watcher = new Watcher(vm, 'b[c]', spy) expect(watcher.value).toBe(2) vm.b.c = 3 - then(function () { + nextTick(function () { expect(watcher.value).toBe(3) expect(spy).toHaveBeenCalledWith(3, 2) vm.c = 'd' // changing the dynamic segment in path - then(function () { + nextTick(function () { expect(watcher.value).toBe(4) expect(spy).toHaveBeenCalledWith(4, 3) done() @@ -72,18 +72,18 @@ describe('Watcher', function () { var watcher = new Watcher(vm, 'a + b.c', spy) expect(watcher.value).toBe(3) vm.b.c = 3 - then(function () { + nextTick(function () { expect(watcher.value).toBe(4) - expect(spy.callCount).toBe(1) + expect(spy.calls.count()).toBe(1) expect(spy).toHaveBeenCalledWith(4, 3) // change two dependencies at once vm.a = 2 vm.b.c = 4 - then(function () { + nextTick(function () { expect(watcher.value).toBe(6) // should trigger only once callback, // because it was in the same event loop. - expect(spy.callCount).toBe(2) + expect(spy.calls.count()).toBe(2) expect(spy).toHaveBeenCalledWith(6, 4) done() }) @@ -95,11 +95,11 @@ describe('Watcher', function () { var watcher = new Watcher(vm, 'a > 1 ? b.c : b.d', spy) expect(watcher.value).toBe(4) vm.a = 2 - then(function () { + nextTick(function () { expect(watcher.value).toBe(2) expect(spy).toHaveBeenCalledWith(2, 4) vm.b.c = 3 - then(function () { + nextTick(function () { expect(watcher.value).toBe(3) expect(spy).toHaveBeenCalledWith(3, 2) done() @@ -111,7 +111,7 @@ describe('Watcher', function () { var watcher = new Watcher(vm, 'd.e', spy) expect(watcher.value).toBeUndefined() vm.$scope.$add('d', { e: 123 }) - then(function () { + nextTick(function () { expect(watcher.value).toBe(123) expect(spy).toHaveBeenCalledWith(123, undefined) done() @@ -122,7 +122,7 @@ describe('Watcher', function () { var watcher = new Watcher(vm, 'b.c', spy) expect(watcher.value).toBe(2) vm.$scope.$delete('b') - then(function () { + nextTick(function () { expect(watcher.value).toBeUndefined() expect(spy).toHaveBeenCalledWith(undefined, 2) done() @@ -133,7 +133,7 @@ describe('Watcher', function () { var watcher = new Watcher(vm, 'b.c', spy) expect(watcher.value).toBe(2) vm.$data = { b: { c: 3}} - then(function () { + nextTick(function () { expect(watcher.value).toBe(3) expect(spy).toHaveBeenCalledWith(3, 2) done() @@ -144,11 +144,11 @@ describe('Watcher', function () { var watcher = new Watcher(vm, '$data.b.c', spy) expect(watcher.value).toBe(2) vm.b = { c: 3 } - then(function () { + nextTick(function () { expect(watcher.value).toBe(3) expect(spy).toHaveBeenCalledWith(3, 2) vm.$data = { b: {c: 4}} - then(function () { + nextTick(function () { expect(watcher.value).toBe(4) expect(spy).toHaveBeenCalledWith(4, 3) done() @@ -161,11 +161,11 @@ describe('Watcher', function () { var watcher = new Watcher(vm, '$data', spy) expect(watcher.value).toBe(oldData) vm.a = 2 - then(function () { + nextTick(function () { expect(spy).toHaveBeenCalledWith(oldData, oldData) var newData = {} vm.$data = newData - then(function() { + nextTick(function() { expect(spy).toHaveBeenCalledWith(newData, oldData) expect(watcher.value).toBe(newData) done() @@ -180,7 +180,7 @@ describe('Watcher', function () { expect(this).toBe(context) }, context) vm.b.c = 3 - then(function () { + nextTick(function () { expect(spy).toHaveBeenCalledWith(3, 2) done() }) @@ -199,7 +199,7 @@ describe('Watcher', function () { ]) expect(watcher.value).toBe('6yo') vm.b.c = 3 - then(function () { + nextTick(function () { expect(watcher.value).toBe('9yo') expect(spy).toHaveBeenCalledWith('9yo', '6yo') done() @@ -217,12 +217,12 @@ describe('Watcher', function () { ], true) expect(watcher.value).toBe(2) watcher.set(4) // shoud not change the value - then(function () { + nextTick(function () { expect(vm.b.c).toBe(2) expect(watcher.value).toBe(2) - expect(spy.callCount).toBe(0) + expect(spy.calls.count()).toBe(0) watcher.set(6) - then(function () { + nextTick(function () { expect(vm.b.c).toBe(6) expect(watcher.value).toBe(6) expect(spy).toHaveBeenCalledWith(6, 2) @@ -231,4 +231,15 @@ describe('Watcher', function () { }) }) + it('teardown', function (done) { + var watcher = new Watcher(vm, 'b.c', spy) + watcher.teardown() + vm.b.c = 3 + nextTick(function () { + expect(watcher.active).toBe(false) + expect(spy.calls.count()).toBe(0) + done() + }) + }) + }) \ No newline at end of file From 8a3998648291f4eb56be7da3ab83aa84b07e410b Mon Sep 17 00:00:00 2001 From: Evan You Date: Fri, 8 Aug 2014 19:10:25 -0400 Subject: [PATCH 0117/1534] npmignore --- .npmignore | 17 +++++++---------- 1 file changed, 7 insertions(+), 10 deletions(-) diff --git a/.npmignore b/.npmignore index d5ff25078b4..4e2b8a1a512 100644 --- a/.npmignore +++ b/.npmignore @@ -1,15 +1,12 @@ -test +.* +*.md +benchmarks +coverage +explorations tasks +test examples -explorations -components -.jshintrc -.gitignore -.travis.yml -.npmignore bower.json component.json gruntfile.js -TODO.md -sauce_connect.log -coverage \ No newline at end of file +sauce_connect.log \ No newline at end of file From fd14d5edc701ccbc8fd1f13f1adfc203b12a4b34 Mon Sep 17 00:00:00 2001 From: Evan You Date: Sat, 9 Aug 2014 18:12:06 -0400 Subject: [PATCH 0118/1534] test for emitter --- package.json | 3 +- test/unit/specs/binding_spec.js | 3 -- test/unit/specs/emitter_spec.js | 66 +++++++++++++++++++++++++++++++++ 3 files changed, 67 insertions(+), 5 deletions(-) delete mode 100644 test/unit/specs/binding_spec.js create mode 100644 test/unit/specs/emitter_spec.js diff --git a/package.json b/package.json index 18e90f8d01d..016fdbb7d4c 100644 --- a/package.json +++ b/package.json @@ -17,8 +17,7 @@ "bugs": "https://github.com/yyx990803/vue/issues", "homepage": "http://vuejs.org", "scripts": { - "test": "grunt ci", - "j": "jasmine-node test/unit/ --verbose" + "test": "grunt ci" }, "devDependencies": { "browserify": "^4.2.0", diff --git a/test/unit/specs/binding_spec.js b/test/unit/specs/binding_spec.js deleted file mode 100644 index 487a2065821..00000000000 --- a/test/unit/specs/binding_spec.js +++ /dev/null @@ -1,3 +0,0 @@ -/** - * Test the binding class - */ \ No newline at end of file diff --git a/test/unit/specs/emitter_spec.js b/test/unit/specs/emitter_spec.js new file mode 100644 index 00000000000..73bc688469a --- /dev/null +++ b/test/unit/specs/emitter_spec.js @@ -0,0 +1,66 @@ +var Emitter = require('../../../src/emitter') +var u = undefined + +describe('Emitter', function () { + + var e, spy + beforeEach(function () { + e = new Emitter() + spy = jasmine.createSpy('emitter') + }) + + it('on', function () { + e.on('test', spy) + e.emit('test', 1, 2 ,3) + expect(spy.calls.count()).toBe(1) + expect(spy).toHaveBeenCalledWith(1, 2, 3) + }) + + it('once', function () { + e.once('test', spy) + e.emit('test', 1, 2 ,3) + e.emit('test', 2, 3, 4) + expect(spy.calls.count()).toBe(1) + expect(spy).toHaveBeenCalledWith(1, 2, 3) + }) + + it('off', function () { + e.on('test1', spy) + e.on('test2', spy) + e.off() + e.emit('test1') + e.emit('test2') + expect(spy.calls.count()).toBe(0) + }) + + it('off event', function () { + e.on('test1', spy) + e.on('test2', spy) + e.off('test1') + e.off('test1') // test off something that's already off + e.emit('test1', 1) + e.emit('test2', 2) + expect(spy.calls.count()).toBe(1) + expect(spy).toHaveBeenCalledWith(2, u, u) + }) + + it('off event + fn', function () { + var spy2 = jasmine.createSpy('emitter') + e.on('test', spy) + e.on('test', spy2) + e.off('test', spy) + e.emit('test', 1, 2, 3) + expect(spy.calls.count()).toBe(0) + expect(spy2.calls.count()).toBe(1) + expect(spy2).toHaveBeenCalledWith(1, 2, 3) + }) + + it('apply emit', function () { + e.on('test', spy) + e.applyEmit('test', 1) + e.applyEmit('test', 1, 2, 3, 4, 5) + expect(spy).toHaveBeenCalledWith(1) + expect(spy).toHaveBeenCalledWith(1, 2, 3, 4, 5) + }) + +}) \ No newline at end of file From 043eb6e14cfe4afe712f259307da38100a8330ce Mon Sep 17 00:00:00 2001 From: Evan You Date: Sat, 9 Aug 2014 18:57:46 -0400 Subject: [PATCH 0119/1534] use nextTick with context in batcher --- src/batcher.js | 5 +---- test/unit/specs/watcher_spec.js | 5 +++++ 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/src/batcher.js b/src/batcher.js index 501ec767c36..ec84461d542 100644 --- a/src/batcher.js +++ b/src/batcher.js @@ -30,10 +30,7 @@ p.push = function (job) { this.has[job.id] = job if (!this.waiting) { this.waiting = true - var self = this - _.nextTick(function () { - self.flush() - }) + _.nextTick(this.flush, this) } } else if (job.override) { var oldJob = this.has[job.id] diff --git a/test/unit/specs/watcher_spec.js b/test/unit/specs/watcher_spec.js index 2f3cd53f0ba..571e46fa58c 100644 --- a/test/unit/specs/watcher_spec.js +++ b/test/unit/specs/watcher_spec.js @@ -109,11 +109,16 @@ describe('Watcher', function () { it('non-existent path, $add later', function (done) { var watcher = new Watcher(vm, 'd.e', spy) + var watcher2 = new Watcher(vm, 'b.e', spy) expect(watcher.value).toBeUndefined() + expect(watcher2.value).toBeUndefined() vm.$scope.$add('d', { e: 123 }) + vm.b.$add('e', 234) nextTick(function () { expect(watcher.value).toBe(123) + expect(watcher2.value).toBe(234) expect(spy).toHaveBeenCalledWith(123, undefined) + expect(spy).toHaveBeenCalledWith(234, undefined) done() }) }) From d4accea3c62aeac560e50d8f754fda83e650ebb7 Mon Sep 17 00:00:00 2001 From: Evan You Date: Sun, 10 Aug 2014 13:32:54 -0400 Subject: [PATCH 0120/1534] working on instance apis --- src/api/data.js | 105 +++++++++++++++++++++++++++++++++------ src/api/lifecycle.js | 35 ++++++++++++- src/directive.js | 59 +++++++++++----------- src/instance/bindings.js | 4 +- src/instance/events.js | 13 +++-- src/instance/init.js | 11 ++-- src/instance/scope.js | 19 +++++-- src/parse/expression.js | 2 +- src/watcher.js | 14 +++--- 9 files changed, 197 insertions(+), 65 deletions(-) diff --git a/src/api/data.js b/src/api/data.js index 9d4c0376726..8a19be32a6b 100644 --- a/src/api/data.js +++ b/src/api/data.js @@ -1,31 +1,106 @@ -exports.$get = function () { - +var expParser = require('../parse/expression') +var textParser = require('../parse/text') +var Watcher = require('../watcher') + +/** + * Get the value from an expression on this vm. + * + * @param {String} exp + * @return {*} + */ + +exports.$get = function (exp) { + var res = expParser.parse(exp) + if (res) { + return res.get.call(this, this.$scope) + } } -exports.$set = function () { - +/** + * Set the value from an expression on this vm. + * The expression must be a valid left-hand expression + * in an assignment. + * + * @param {String} exp + * @param {*} val + */ + +exports.$set = function (exp, val) { + var res = expParser.parse(exp, true) + if (res && res.set) { + res.set.call(this, this.$scope, val) + } } -exports.$add = function () { - +/** + * Add a property on the VM (and also on $scope and $data) + * + * @param {String} key + * @param {*} val + */ + +exports.$add = function (key, val) { + this.$scope.$add(key, val) } -exports.$delete = function () { - +/** + * Delete a property on the VM (and also on $scope and $data) + * + * @param {String} key + */ + +exports.$delete = function (key) { + this.$scope.$delete(key) } -exports.$watch = function () { - +/** + * Watch an expression, trigger callback when its value changes. + * Returns the created watcher's id so it can be teardown later. + * + * @param {String} exp + * @param {Function} cb + * @return {Number} + */ + +exports.$watch = function (exp, cb) { + var watcher = new Watcher(this, exp, cb, this) + this._watchers[watcher.id] = watcher + return watcher.id } -exports.$unwatch = function () { - +/** + * Teardown a watcher with given id. + * + * @param {Number} id + */ + +exports.$unwatch = function (id) { + var watcher = this._watchers[id] + if (watcher) { + watcher.teardown() + } + this._watchers[id] = null } -exports.$toJSON = function () { +/** + * Interpolate a piece of template string. + * + * @param {String} string + */ + +exports.$interpolate = function (string) { } -exports.$log = function () { - +/** + * Log instance data as a plain JS object + * so that it is easier to inspect in console. + * This method assumes console is available. + * + * @param {String} [key] + */ + +exports.$log = function (key) { + var data = this[key || '$data'] + console.log(JSON.parse(JSON.stringify(data))) } \ No newline at end of file diff --git a/src/api/lifecycle.js b/src/api/lifecycle.js index c2514cfca5d..2ae3b922aa9 100644 --- a/src/api/lifecycle.js +++ b/src/api/lifecycle.js @@ -23,9 +23,42 @@ exports.$mount = function (el) { */ exports.$destroy = function (remove) { + if (this._isDestroyed) { + return + } + this._isDestroyed = true this._callHook('beforeDestroy') + // remove DOM element if (remove) { - // TODO + if (this.$el === document.body) { + this.$el.innerHTML = '' + } else { + this.$remove() + } + } + var i + // remove self from parent. only necessary + // if this is called by the user. + var parent = this.$parent + if (parent && !parent._isDestroyed) { + i = parent._children.indexOf(this) + parent._children.splice(i) + } + // destroy all children. + i = this._children.length + while (i--) { + this._children[i].$destroy() + } + // teardown data/scope + this._teardownScope() + // teardown all user watchers + for (var id in this._watchers) { + this.$unwatch(id) + } + // teardown all directives + i = this._directives.length + while (i--) { + this._directives[i]._teardown() } this._callHook('afterDestroy') } \ No newline at end of file diff --git a/src/directive.js b/src/directive.js index 590a2e22751..a05e60a69e2 100644 --- a/src/directive.js +++ b/src/directive.js @@ -28,20 +28,9 @@ function Directive (name, el, vm, descriptor) { // private this._locked = false - this._unbound = false + this._bound = false // init definition this._initDef() - - if (this.expression && !this.isLiteral && this.update) { - this._watcher = new Watcher( - vm, - this.expression, - this._update, - this, // callback context - this.filters, - this.twoway - ) - } } var p = Directive.prototype @@ -66,20 +55,33 @@ p._initDef = function () { } /** - * Apply the directive, call definition bind() if present, - * and call first update(). - * - * @private + * Initialize the directive, setup the watcher, + * call definition bind() and update() if present. */ p._bind = function () { - var value = this._watcher.value - if (this.bind) { - this.bind(value) - } - if (this.update) { - this.update(value) + if (this.expression && !this.isLiteral && this.update) { + this._watcher = new Watcher( + this.vm, + this.expression, + this._update, // callback + this, // callback context + this.filters, + this.twoway // need setter + ) + var value = this._watcher.value + if (this.bind) { + this.bind(value) + } + if (this.update) { + this.update(value) + } + } else { + if (this.bind) { + this.bind() + } } + this._bound = true } /** @@ -87,7 +89,6 @@ p._bind = function () { * Check locked or not before calling definition update. * * @param {*} value - * @private */ p._update = function (value) { @@ -98,16 +99,16 @@ p._update = function (value) { /** * Teardown the watcher and call unbind. - * - * @private */ p._teardown = function () { - if (this.unbind) { - this.unbind() + if (this._bound) { + if (this.unbind) { + this.unbind() + } + this._watcher.teardown() + this._bound = false } - this._watcher.teardown() - this._unbound = true } /** diff --git a/src/instance/bindings.js b/src/instance/bindings.js index ee77927fc74..7abe08f1e0a 100644 --- a/src/instance/bindings.js +++ b/src/instance/bindings.js @@ -112,9 +112,9 @@ exports._updateAdd = function (path) { */ exports._collectDep = function (path) { - var watcher = this._currentWatcher + var watcher = this._activeWatcher // the get event might have come from a child vm's watcher - // so this._currentWatcher is not guarunteed to be defined + // so this._activeWatcher is not guarunteed to be defined if (watcher) { watcher.addDep(path) } diff --git a/src/instance/events.js b/src/instance/events.js index 6f0470b0a98..d3658415332 100644 --- a/src/instance/events.js +++ b/src/instance/events.js @@ -1,15 +1,22 @@ /** - * Setup the instance's option events + * Setup the instance's option events. + * If the value is a string, we pull it from the + * instance's methods by name. */ exports._initEvents = function () { - var events = this.$options.events + var options = this.$options + var events = options.events + var methods = options.methods if (events) { var handlers, e, i, j for (e in events) { handlers = events[e] for (i = 0, j = handlers.length; i < j; i++) { - this.$on(e, handlers[i]) + var handler = (methods && typeof handlers[i] === 'string') + ? methods[handlers[i]] + : handlers[i] + this.$on(e, handler) } } } diff --git a/src/instance/init.js b/src/instance/init.js index 7caf3300251..6d9e785b97f 100644 --- a/src/instance/init.js +++ b/src/instance/init.js @@ -21,8 +21,9 @@ exports._init = function (options) { this._isDestroyed = false this._rawContent = null this._emitter = new Emitter(this) - // the current target watcher for dependency collection - this._currentWatcher = null + this._watchers = {} + this._activeWatcher = null + this._directives = [] // setup parent relationship this.$parent = options.parent @@ -42,9 +43,6 @@ exports._init = function (options) { // been set up & before data observation happens. this._callHook('created') - // setup event system and option events - this._initEvents() - // create scope. // @creates this.$scope this._initScope() @@ -65,6 +63,9 @@ exports._init = function (options) { // @creates this._rootBinding this._initBindings() + // setup event system and option events + this._initEvents() + // if `el` option is passed, start compilation. if (options.el) { this.$mount(options.el) diff --git a/src/instance/scope.js b/src/instance/scope.js index 10f7457adec..745d1b390b9 100644 --- a/src/instance/scope.js +++ b/src/instance/scope.js @@ -43,11 +43,14 @@ exports._initScope = function () { } /** - * Teardown scope and remove listeners attached to parent scope. + * Teardown scope, unsync data, and remove all listeners + * including ones attached to parent's observer. * Only called once during $destroy(). */ exports._teardownScope = function () { + this._observer.off() + this._unsyncData() this.$scope = null if (this.$parent) { var pob = this.$parent._observer @@ -251,12 +254,22 @@ exports._syncData = function () { /** * The guard function prevents infinite loop - * when syncing between two observers. + * when syncing between two observers. Also + * filters out properties prefixed with $ or _. + * + * @param {Function} fn + * @return {Function} */ function guard (fn) { return function (key, val) { - if (locked) return + if (locked) { + return + } + var c = key.charAt(0) + if (c === '$' || c === '_') { + return + } locked = true fn(key, val) locked = false diff --git a/src/parse/expression.js b/src/parse/expression.js index 3793b2001d8..81af742dd33 100644 --- a/src/parse/expression.js +++ b/src/parse/expression.js @@ -209,6 +209,7 @@ function checkSetter (hit) { */ exports.parse = function (exp, needSet) { + exp = exp.trim() // try cache var hit = expressionCache.get(exp) if (hit) { @@ -217,7 +218,6 @@ exports.parse = function (exp, needSet) { } return hit } - exp = exp.trim() // we do a simple path check to optimize for that scenario. // the check fails valid paths with unusal whitespaces, but // that's too rare and we don't care. diff --git a/src/watcher.js b/src/watcher.js index 1a6dba0fb46..2c2ddc74cad 100644 --- a/src/watcher.js +++ b/src/watcher.js @@ -167,7 +167,7 @@ p.set = function (value) { p.beforeGet = function () { Observer.emitGet = true - this.vm._currentWatcher = this + this.vm._activeWatcher = this this.newDeps = Object.create(null) } @@ -176,7 +176,7 @@ p.beforeGet = function () { */ p.afterGet = function () { - this.vm._currentWatcher = null + this.vm._activeWatcher = null Observer.emitGet = false _.extend(this.newDeps, this.deps) this.deps = this.newDeps @@ -215,10 +215,12 @@ p.run = function () { */ p.teardown = function () { - this.active = false - var vm = this.vm - for (var path in this.deps) { - vm._getBindingAt(path)._removeSub(this) + if (this.active) { + this.active = false + var vm = this.vm + for (var path in this.deps) { + vm._getBindingAt(path)._removeSub(this) + } } } From 779ec3417f9bcb483a7a72f58db0985923613435 Mon Sep 17 00:00:00 2001 From: Evan You Date: Sun, 10 Aug 2014 14:12:22 -0400 Subject: [PATCH 0121/1534] comform source code width --- src/api/data.js | 16 +++++++----- src/api/events.js | 25 ++++++++++++++---- src/api/global.js | 6 ++--- src/api/lifecycle.js | 7 ++--- src/binding.js | 6 ++--- src/cache.js | 14 +++++----- src/directive.js | 11 ++++---- src/instance/bindings.js | 33 +++++++++++------------- src/instance/element.js | 26 ++++++++++--------- src/instance/events.js | 4 +-- src/instance/init.js | 17 +++++++------ src/instance/scope.js | 37 ++++++++++++++------------- src/observe/observer.js | 16 +++++++----- src/parse/directive.js | 24 +++++++++++++----- src/parse/expression.js | 55 ++++++++++++++++++++++++++-------------- src/parse/path.js | 7 +++-- src/parse/template.js | 18 ++++++++----- src/util/debug.js | 6 ++--- src/util/env.js | 7 ++--- src/util/lang.js | 11 ++++---- src/util/option.js | 19 +++++++++----- src/vue.js | 6 +++-- 22 files changed, 224 insertions(+), 147 deletions(-) diff --git a/src/api/data.js b/src/api/data.js index 8a19be32a6b..fbf9f02fc95 100644 --- a/src/api/data.js +++ b/src/api/data.js @@ -18,8 +18,8 @@ exports.$get = function (exp) { /** * Set the value from an expression on this vm. - * The expression must be a valid left-hand expression - * in an assignment. + * The expression must be a valid left-hand + * expression in an assignment. * * @param {String} exp * @param {*} val @@ -33,7 +33,8 @@ exports.$set = function (exp, val) { } /** - * Add a property on the VM (and also on $scope and $data) + * Add a property on the VM + * (and also on $scope and $data) * * @param {String} key * @param {*} val @@ -44,7 +45,8 @@ exports.$add = function (key, val) { } /** - * Delete a property on the VM (and also on $scope and $data) + * Delete a property on the VM + * (and also on $scope and $data) * * @param {String} key */ @@ -54,8 +56,9 @@ exports.$delete = function (key) { } /** - * Watch an expression, trigger callback when its value changes. - * Returns the created watcher's id so it can be teardown later. + * Watch an expression, trigger callback when its + * value changes. Returns the created watcher's + * id so it can be teardown later. * * @param {String} exp * @param {Function} cb @@ -86,6 +89,7 @@ exports.$unwatch = function (id) { * Interpolate a piece of template string. * * @param {String} string + * @return {String} */ exports.$interpolate = function (string) { diff --git a/src/api/events.js b/src/api/events.js index 07ec71685f9..541d3c61741 100644 --- a/src/api/events.js +++ b/src/api/events.js @@ -7,7 +7,10 @@ ? 'applyEmit' : method exports['$' + method] = function () { - this._emitter[realMethod].apply(this._emitter, arguments) + this._emitter[realMethod].apply( + this._emitter, + arguments + ) } }) @@ -22,8 +25,14 @@ exports.$broadcast = function () { var children = this.children for (var i = 0, l = children.length; i < l; i++) { var child = children[i] - child._emitter.applyEmit.apply(child._emitter, arguments) - child.$broadcast.apply(child, arguments) + child._emitter.applyEmit.apply( + child._emitter, + arguments + ) + child.$broadcast.apply( + child, + arguments + ) } } @@ -35,9 +44,15 @@ exports.$broadcast = function () { */ exports.$dispatch = function () { - this._emitter.applyEmit.apply(this._emitter, arguments) + this._emitter.applyEmit.apply( + this._emitter, + arguments + ) var parent = this.$parent if (parent) { - parent.$dispatch.apply(parent, arguments) + parent.$dispatch.apply( + parent, + arguments + ) } } \ No newline at end of file diff --git a/src/api/global.js b/src/api/global.js index 18025b5aa05..c7976f52b45 100644 --- a/src/api/global.js +++ b/src/api/global.js @@ -8,9 +8,9 @@ var assetTypes = [ ] /** - * Vue and every constructor that extends Vue has an associated - * options object, which can be accessed during compilation steps - * as `this.constructor.options`. + * Vue and every constructor that extends Vue has an + * associated options object, which can be accessed during + * compilation steps as `this.constructor.options`. */ exports.options = { diff --git a/src/api/lifecycle.js b/src/api/lifecycle.js index 2ae3b922aa9..9e9f7099232 100644 --- a/src/api/lifecycle.js +++ b/src/api/lifecycle.js @@ -1,7 +1,8 @@ /** - * Set instance target element and kick off the compilation process. - * The passed in `el` can be a selector string, an existing Element, - * or a DocumentFragment (for block instances). + * Set instance target element and kick off the compilation + * process. The passed in `el` can be a selector string, an + * existing Element, or a DocumentFragment (for block + * instances). * * @param {Element|DocumentFragment|string} el * @public diff --git a/src/binding.js b/src/binding.js index 506aeff82c2..de26f037f75 100644 --- a/src/binding.js +++ b/src/binding.js @@ -1,7 +1,7 @@ /** - * A binding is an observable that can have multiple directives - * subscribing to it. It can also have multiple other bindings - * as children to form a trie-like structure. + * A binding is an observable that can have multiple + * directives subscribing to it. It can also have multiple + * other bindings as children to form a trie-like structure. * * All binding properties are prefixed with `_` so that they * don't conflict with children keys. diff --git a/src/cache.js b/src/cache.js index 63805e9c118..13a2b12fe59 100644 --- a/src/cache.js +++ b/src/cache.js @@ -1,8 +1,9 @@ /** - * A doubly linked list-based Least Recently Used (LRU) cache. - * Will keep most recently used items while discarding least - * recently used items when its limit is reached. This is a - * bare-bone version of Rasmus Andersson's js-lru: + * A doubly linked list-based Least Recently Used (LRU) + * cache. Will keep most recently used items while + * discarding least recently used items when its limit is + * reached. This is a bare-bone version of + * Rasmus Andersson's js-lru: * * https://github.com/rsms/js-lru * @@ -51,8 +52,9 @@ p.put = function (key, value) { } /** - * Purge the least recently used (oldest) entry from the cache. - * Returns the removed entry or undefined if the cache was empty. + * Purge the least recently used (oldest) entry from the + * cache. Returns the removed entry or undefined if the + * cache was empty. */ p.shift = function () { diff --git a/src/directive.js b/src/directive.js index a05e60a69e2..5465344b094 100644 --- a/src/directive.js +++ b/src/directive.js @@ -2,10 +2,10 @@ var _ = require('./util') var Watcher = require('./watcher') /** - * A directive links a DOM element with a piece of data, which can - * be either simple paths or computed properties. It subscribes to - * a list of dependencies (Bindings) and refreshes the list during - * its getter evaluation. + * A directive links a DOM element with a piece of data, + * which is the result of evaluating an expression. + * It registers a watcher with the expression and calls + * the DOM update function when a change is triggered. * * @param {String} name * @param {Node} el @@ -113,7 +113,8 @@ p._teardown = function () { /** * Set the corresponding value with the setter. - * This should only be used in two-way bindings like v-model. + * This should only be used in two-way directives + * e.g. v-model. * * @param {*} value * @param {Boolean} lock - prevent wrtie triggering update. diff --git a/src/instance/bindings.js b/src/instance/bindings.js index 7abe08f1e0a..d8e12363877 100644 --- a/src/instance/bindings.js +++ b/src/instance/bindings.js @@ -5,18 +5,13 @@ var Observer = require('../observe/observer') /** * Setup the binding tree. * - * Bindings form a tree-like structure that maps the Object structure - * of observed data. However, only paths present in the templates are - * created in the binding tree. When a change event from the data - * observer arrives on the instance, we traverse the binding tree - * along the changed path, triggering binding updates along the way. - * When we reach the path endpoint, if it has any children, we also - * trigger updates on the entire sub-tree. - * - * Each instance has a root binding and it has three special children: - * `$data`, `$parent` & `$root`. `$data` points to the root binding - * itself. `$parent` and `$root` point to the instance's parent and - * root's root bindings, respectively. + * Bindings form a tree-like structure that maps the Object + * structure of observed data. However, only paths present + * in the templates are created in the binding tree. When a + * change event from the data observer arrives on the + * instance, we traverse the binding tree along the changed + * path to find the corresponding binding, and trigger + * change for all its subscribers. */ exports._initBindings = function () { @@ -90,11 +85,12 @@ exports._updateBindingAt = function (path) { } /** - * For newly added properties, since its binding has not been - * created yet, directives will not have it as a dependency yet. - * However, they will have its parent as a dependency. Therefore - * here we remove the last segment from the path and notify the - * added property's parent instead. + * For newly added properties, since its binding has not + * been created yet, directives will not have it as a + * dependency yet. However, they will have its parent as a + * dependency. Therefore here we remove the last segment + * from the path and notify the added property's parent + * instead. * * @param {String} path */ @@ -106,7 +102,8 @@ exports._updateAdd = function (path) { } /** - * Collect dependency for the target directive being evaluated. + * Collect dependency for the target directive being + * evaluated. * * @param {String} path */ diff --git a/src/instance/element.js b/src/instance/element.js index 761e686b005..13ddda847d7 100644 --- a/src/instance/element.js +++ b/src/instance/element.js @@ -18,10 +18,10 @@ exports._initElement = function (el) { _.warn('Cannot find element: ' + selector) } } - // If the passed in `el` is a DocumentFragment, the instance is - // considered a "block instance" which manages not a single element, - // but multiple elements. A block instance's `$el` is an Array of - // the elements it manages. + // If the passed in `el` is a DocumentFragment, the + // instance is considered a "block instance" which manages + // not a single element, but multiple elements. A block + // instance's `$el` is an Array of the elements it manages. if (el instanceof window.DocumentFragment) { this._isBlock = true this.$el = _.toArray(el.childNodes) @@ -34,7 +34,7 @@ exports._initElement = function (el) { /** * Process the template option. - * If the replace option is true this will also modify the $el. + * If the replace option is true this will swap the $el. */ exports._initTemplate = function () { @@ -46,7 +46,7 @@ exports._initTemplate = function () { if (!frag) { _.warn('Invalid template option: ' + template) } else { - // collect raw content. this wipes out the container el. + // collect raw content. this wipes out $el. this._collectRawContent() frag = frag.cloneNode(true) if (options.replace) { @@ -93,10 +93,10 @@ exports._collectRawContent = function () { } /** - * Resolve insertion points per W3C Web Components - * working draft: + * Resolve insertion points mimicking the behavior + * of the Shadow DOM spec: * - * http://www.w3.org/TR/2013/WD-components-intro-20130606/#insertion-points + * http://w3c.github.io/webcomponents/spec/shadow/#insertion-points */ exports._initContent = function () { @@ -112,7 +112,9 @@ exports._initContent = function () { if (raw) { select = outlet.getAttribute('select') if (select) { // select content - outlet.content = _.toArray(raw.querySelectorAll(select)) + outlet.content = _.toArray( + raw.querySelectorAll(select) + ) } else { // default content main = outlet } @@ -142,7 +144,6 @@ exports._initContent = function () { */ var concat = [].concat - function getOutlets (el) { return _.isArray(el) ? concat.apply([], el.map(getOutlets)) @@ -150,7 +151,8 @@ function getOutlets (el) { } /** - * Insert an array of nodes at outlet, then remove the outlet. + * Insert an array of nodes at outlet, + * then remove the outlet. * * @param {Element} outlet * @param {Array} contents diff --git a/src/instance/events.js b/src/instance/events.js index d3658415332..8ddb7f92d6b 100644 --- a/src/instance/events.js +++ b/src/instance/events.js @@ -13,8 +13,8 @@ exports._initEvents = function () { for (e in events) { handlers = events[e] for (i = 0, j = handlers.length; i < j; i++) { - var handler = (methods && typeof handlers[i] === 'string') - ? methods[handlers[i]] + var handler = typeof handlers[i] === 'string' + ? methods && methods[handlers[i]] : handlers[i] this.$on(e, handler) } diff --git a/src/instance/init.js b/src/instance/init.js index 6d9e785b97f..1c149a00624 100644 --- a/src/instance/init.js +++ b/src/instance/init.js @@ -2,13 +2,14 @@ var Emitter = require('../emitter') var mergeOptions = require('../util').mergeOptions /** - * The main init sequence. This is called for every instance, - * including ones that are created from extended constructors. + * The main init sequence. This is called for every + * instance, including ones that are created from extended + * constructors. * - * @param {Object} options - this options object should be the - * result of merging class options - * and the options passed in to the - * constructor. + * @param {Object} options - this options object should be + * the result of merging class + * options and the options passed + * in to the constructor. */ exports._init = function (options) { @@ -39,8 +40,8 @@ exports._init = function (options) { this ) - // the `created` hook is called after basic properties have - // been set up & before data observation happens. + // the `created` hook is called after basic properties + // have been set up & before data observation happens. this._callHook('created') // create scope. diff --git a/src/instance/scope.js b/src/instance/scope.js index 745d1b390b9..51d92ce7955 100644 --- a/src/instance/scope.js +++ b/src/instance/scope.js @@ -6,7 +6,8 @@ var scopeEvents = ['set', 'mutate', 'add', 'delete'] * Setup instance scope. * The scope is reponsible for prototypal inheritance of * parent instance propertiesm abd all binding paths and - * expressions of the current instance are evaluated against its scope. + * expressions of the current instance are evaluated against + * its scope. * * This should only be called once during _init(). */ @@ -64,20 +65,22 @@ exports._teardownScope = function () { /** * Setup the instances data object. * - * Properties are copied into the scope object to take advantage of - * prototypal inheritance. + * Properties are copied into the scope object to take + * advantage of prototypal inheritance. * - * If the `syncData` option is true, Vue will maintain property - * syncing between the scope and the original data object, so that - * any changes to the scope are synced back to the passed in object. - * This is useful internally when e.g. creating v-repeat instances - * with no alias. + * If the `syncData` option is true, Vue will maintain + * property syncing between the scope and the original data + * object, so that any changes to the scope are synced back + * to the passed in object. This is useful internally when + * e.g. creating v-repeat instances with no alias. * - * If swapping data object with the `$data` accessor, teardown - * previous sync listeners and delete keys not present in new data. + * If swapping data object with the `$data` accessor, + * teardown previous sync listeners and delete keys not + * present in new data. * * @param {Object} data - * @param {Boolean} init - if not ture, indicates its a `$data` swap. + * @param {Boolean} init - if not ture, indicates its a + * `$data` swap. */ exports._initData = function (data, init) { @@ -120,13 +123,13 @@ exports._initData = function (data, init) { * Proxy the scope properties on the instance itself, * so that vm.a === vm.$scope.a. * - * Note this only proxies *local* scope properties. We want to - * prevent child instances accidentally modifying properties - * with the same name up in the scope chain because scope - * perperties are all getter/setters. + * Note this only proxies *local* scope properties. We want + * to prevent child instances accidentally modifying + * properties with the same name up in the scope chain + * because scope perperties are all getter/setters. * - * To access parent properties through prototypal fall through, - * access it on the instance's $scope. + * To access parent properties through prototypal fall + * through, access it on the instance's $scope. * * This should only be called once during _init(). */ diff --git a/src/observe/observer.js b/src/observe/observer.js index 1bbaacd35f4..67b4bf1a80e 100644 --- a/src/observe/observer.js +++ b/src/observe/observer.js @@ -27,8 +27,8 @@ var OBJECT = 1 * @param {Array|Object} value * @param {Number} type * @param {Object} [options] - * - doNotAlterProto: if true, do not alter object's __proto__ - * - callbackContext: `this` context for callbacks + * - doNotAlterProto + * - callbackContext */ function Observer (value, type, options) { @@ -90,15 +90,19 @@ Observer.create = function (value, options) { return value.$observer } if (_.isArray(value)) { return new Observer(value, ARRAY, options) - } else if (_.isObject(value) && !value.$scope) { // avoid Vue instance + } else if ( + _.isObject(value) && + !value.$scope // avoid Vue instance + ) { return new Observer(value, OBJECT, options) } } /** - * Walk through each property, converting them and adding them as child. - * This method should only be called when value type is Object. - * Properties prefixed with `$` or `_` and accessor properties are ignored. + * Walk through each property, converting them and adding + * them as child. This method should only be called when + * value type is Object. Properties prefixed with `$` or `_` + * and accessor properties are ignored. * * @param {Object} obj */ diff --git a/src/parse/directive.js b/src/parse/directive.js index 5470c631061..0abfb6a3a13 100644 --- a/src/parse/directive.js +++ b/src/parse/directive.js @@ -58,8 +58,8 @@ function pushFilter () { } /** - * Parse a directive string into an Array of AST-like objects - * representing directives. + * Parse a directive string into an Array of AST-like + * objects representing directives. * * Example: * @@ -86,7 +86,8 @@ exports.parse = function (s) { // reset parser state str = s inSingle = inDouble = false - curly = square = paren = begin = argIndex = lastFilterIndex = 0 + curly = square = paren = begin = argIndex = 0 + lastFilterIndex = 0 dirs = [] dir = {} arg = null @@ -99,13 +100,20 @@ exports.parse = function (s) { } else if (inDouble) { // check double quote if (c === '"') inDouble = !inDouble - } else if (c === ',' && !paren && !curly && !square) { + } else if ( + c === ',' && + !paren && !curly && !square + ) { // reached the end of a directive pushDir() // reset & skip the comma dir = {} begin = argIndex = lastFilterIndex = i + 1 - } else if (c === ':' && !dir.expression && !dir.arg) { + } else if ( + c === ':' && + !dir.expression && + !dir.arg + ) { // argument arg = str.slice(begin, i).trim() // test for valid argument here @@ -119,7 +127,11 @@ exports.parse = function (s) { ? arg.slice(1, -1) : arg } - } else if (c === '|' && str.charAt(i + 1) !== '|' && str.charAt(i - 1) !== '|') { + } else if ( + c === '|' && + str.charAt(i + 1) !== '|' && + str.charAt(i - 1) !== '|' + ) { if (dir.expression === undefined) { // first filter, end of expression lastFilterIndex = i + 1 diff --git a/src/parse/expression.js b/src/parse/expression.js index 81af742dd33..d48f489d3c7 100644 --- a/src/parse/expression.js +++ b/src/parse/expression.js @@ -3,26 +3,36 @@ var Path = require('./path') var Cache = require('../cache') var expressionCache = new Cache(1000) +var keywords = + 'Math,break,case,catch,continue,debugger,default,' + + 'delete,do,else,false,finally,for,function,if,in,' + + 'instanceof,new,null,return,switch,this,throw,true,try,' + + 'typeof,var,void,while,with,undefined,abstract,boolean,' + + 'byte,char,class,const,double,enum,export,extends,' + + 'final,float,goto,implements,import,int,interface,long,' + + 'native,package,private,protected,public,short,static,' + + 'super,synchronized,throws,transient,volatile,' + + 'arguments,let,yield' + var wsRE = /\s/g var newlineRE = /\n/g var saveRE = /[\{,]\s*[\w\$_]+\s*:|'[^']*'|"[^"]*"/g var restoreRE = /"(\d+)"/g var pathTestRE = /^[A-Za-z_$][\w$]*(\.[A-Za-z_$][\w$]*|\['.*?'\]|\[".*?"\])*$/ var pathReplaceRE = /[^\w$\.]([A-Za-z_$][\w$]*(\.[A-Za-z_$][\w$]*|\['.*?'\]|\[".*?"\])*)/g -var keywords = 'Math,break,case,catch,continue,debugger,default,delete,do,else,false,finally,for,function,if,in,instanceof,new,null,return,switch,this,throw,true,try,typeof,var,void,while,with,undefined,abstract,boolean,byte,char,class,const,double,enum,export,extends,final,float,goto,implements,import,int,interface,long,native,package,private,protected,public,short,static,super,synchronized,throws,transient,volatile,arguments,let,yield' var keywordsRE = new RegExp('^(' + keywords.replace(/,/g, '\\b|') + '\\b)') -// note the following regex is only used on valid paths -// so no need to exclude number for first char -var rootPathRE = /^[\w$]+/ +var rootPathRE = /^[\w$]+/ // this is only used on valid + // paths so no need to exclude + // number for first char /** * Save / Rewrite / Restore * - * When rewriting paths found in an expression, it is possible - * for the same letter sequences to be found in strings and Object - * literal property keys. Therefore we remove and store these - * parts in a temporary array, and restore them after the path - * rewrite. + * When rewriting paths found in an expression, it is + * possible for the same letter sequences to be found in + * strings and Object literal property keys. Therefore we + * remove and store these parts in a temporary array, and + * restore them after the path rewrite. */ var saved = [] @@ -84,8 +94,8 @@ function restore (str, i) { } /** - * Rewrite an expression, prefixing all path accessors with `scope.` - * and generate getter/setter functions. + * Rewrite an expression, prefixing all path accessors with + * `scope.` and generate getter/setter functions. * * @param {String} exp * @param {Boolean} needSet @@ -151,8 +161,8 @@ function compilePathFns (exp) { /** * Build a getter function. Requires eval. * - * We isolate the try/catch so it doesn't affect the optimization - * of the parse function when it is not called. + * We isolate the try/catch so it doesn't affect the + * optimization of the parse function when it is not called. * * @param {String} body * @return {Function|undefined} @@ -162,7 +172,10 @@ function makeGetter (body) { try { return new Function('scope', 'return ' + body + ';') } catch (e) { - _.warn('Invalid expression. Generated function body: ' + body) + _.warn( + 'Invalid expression. ' + + 'Generated function body: ' + body + ) } } @@ -182,7 +195,11 @@ function makeGetter (body) { function makeSetter (body) { try { - return new Function('scope', 'value', body + ' = value;') + return new Function( + 'scope', + 'value', + body + ' = value;' + ) } catch (e) { _.warn('Invalid setter function body: ' + body) } @@ -201,7 +218,7 @@ function checkSetter (hit) { } /** - * Parse an expression and rewrite into a getter/setter functions + * Parse an expression into re-written getter/setters. * * @param {String} exp * @param {Boolean} needSet @@ -218,9 +235,9 @@ exports.parse = function (exp, needSet) { } return hit } - // we do a simple path check to optimize for that scenario. - // the check fails valid paths with unusal whitespaces, but - // that's too rare and we don't care. + // we do a simple path check to optimize for them. + // the check fails valid paths with unusal whitespaces, + // but that's too rare and we don't care. var res = pathTestRE.test(exp) ? compilePathFns(exp) : compileExpFns(exp, needSet) diff --git a/src/parse/path.js b/src/parse/path.js index b5f2071e533..492870d3aa4 100644 --- a/src/parse/path.js +++ b/src/parse/path.js @@ -140,7 +140,8 @@ function getPathCharType (char) { function parsePath (path) { var keys = [] var index = -1 - var c, newChar, key, type, transition, action, typeMap, mode = 'beforePath' + var mode = 'beforePath' + var c, newChar, key, type, transition, action, typeMap var actions = { push: function() { @@ -188,7 +189,9 @@ function parsePath (path) { mode = transition[0] action = actions[transition[1]] || noop - newChar = transition[2] === undefined ? c : transition[2] + newChar = transition[2] === undefined + ? c + : transition[2] action() if (mode === 'afterPath') { diff --git a/src/parse/template.js b/src/parse/template.js index 40acc831cad..119a9f09eb7 100644 --- a/src/parse/template.js +++ b/src/parse/template.js @@ -37,8 +37,8 @@ var TAG_RE = /<([\w:]+)/ /** * Convert a string template to a DocumentFragment. - * Determines correct wrapping by tag types. Wrapping strategy - * originally from jQuery, scooped from component/domify. + * Determines correct wrapping by tag types. Wrapping + * strategy found in jQuery & component/domify. * * @param {String} templateString * @return {DocumentFragment} @@ -56,7 +56,9 @@ function stringToFragment (templateString) { if (!tagMatch) { // text only, return a single text node. - frag.appendChild(document.createTextNode(templateString)) + frag.appendChild( + document.createTextNode(templateString) + ) } else { var tag = tagMatch[1] @@ -93,7 +95,10 @@ function nodeToFragment (node) { var tag = node.tagName // if its a template tag and the browser supports it, // its content is already a document fragment. - if (tag === 'TEMPLATE' && node.content instanceof DocumentFragment) { + if ( + tag === 'TEMPLATE' && + node.content instanceof DocumentFragment + ) { return node.content } return tag === 'SCRIPT' @@ -111,14 +116,15 @@ function nodeToFragment (node) { * - DocumentFragment object * - Node object of type Template * - id selector: '#some-template-id' - * - template string: '
my template
' + * - template string: '
{{msg}}
' * @return {DocumentFragment|undefined} */ exports.parse = function (template) { var node, frag - // if the template is already a document fragment -- do nothing + // if the template is already a document fragment, + // do nothing if (template instanceof DocumentFragment) { return template } diff --git a/src/util/debug.js b/src/util/debug.js index dfe707306bb..7ce894db8b7 100644 --- a/src/util/debug.js +++ b/src/util/debug.js @@ -1,9 +1,9 @@ var config = require('../config') /** - * Enable debug utilities. The enableDebug() function and all - * _.log() & _.warn() calls will be dropped in the minified - * production build. + * Enable debug utilities. The enableDebug() function and + * all _.log() & _.warn() calls will be dropped in the + * minified production build. */ enableDebug() diff --git a/src/util/env.js b/src/util/env.js index c23ab92cfa9..e00450618d2 100644 --- a/src/util/env.js +++ b/src/util/env.js @@ -1,14 +1,15 @@ /** * Are we in a browser or in Node? - * Calling toString on window has inconsistent results in browsers - * so we do it on the document instead. + * Calling toString on window has inconsistent results in + * browsers so we do it on the document instead. * * @type {Boolean} */ +var toString = Object.prototype.toString var inBrowser = exports.inBrowser = typeof window !== 'undefined' && - Object.prototype.toString.call(window) !== '[object Object]' + toString.call(window) !== '[object Object]' /** * Defer a task to the start of the next event loop diff --git a/src/util/lang.js b/src/util/lang.js index a2a02e7e468..d6aa11f9c82 100644 --- a/src/util/lang.js +++ b/src/util/lang.js @@ -19,7 +19,6 @@ exports.bind = function (fn, ctx) { */ var slice = [].slice - exports.toArray = function (list, i) { return slice.call(list, i || 0) } @@ -38,7 +37,8 @@ exports.extend = function (to, from) { } /** - * Mixin including non-enumerables, and copy property descriptors. + * Mixin including non-enumerables, and copy property + * descriptors. * * @param {Object} to * @param {Object} from @@ -46,8 +46,8 @@ exports.extend = function (to, from) { exports.deepMixin = function (to, from) { Object.getOwnPropertyNames(from).forEach(function (key) { - var descriptor = Object.getOwnPropertyDescriptor(from, key) - Object.defineProperty(to, key, descriptor) + var desc =Object.getOwnPropertyDescriptor(from, key) + Object.defineProperty(to, key, desc) }) } @@ -81,8 +81,9 @@ exports.proxy = function (to, from, key) { * @return {Boolean} */ +var toString = Object.prototype.toString exports.isObject = function (obj) { - return Object.prototype.toString.call(obj) === '[object Object]' + return toString.call(obj) === '[object Object]' } /** diff --git a/src/util/option.js b/src/util/option.js index 16cea223cd0..1e728574a38 100644 --- a/src/util/option.js +++ b/src/util/option.js @@ -1,4 +1,4 @@ -// alias debug as _ so we can drop _.warn during uglification +// alias debug as _ so we can drop _.warn during uglify var _ = require('./debug') var extend = require('./lang').extend @@ -35,8 +35,8 @@ strats.paramAttributes = function (parentVal, childVal) { * Assets * * When a vm is present (instance creation), we need to do a - * 3-way merge for assets: constructor assets, instance assets, - * and instance scope assets. + * 3-way merge for assets: constructor assets, instance + * assets, and instance scope assets. */ strats.directives = @@ -76,7 +76,8 @@ strats.events = function (parentVal, childVal) { /** * Other object hashes. - * These are instance-specific and do not inehrit from nested parents. + * These are instance-specific and do not inehrit from + * nested parents. */ strats.methods = @@ -124,10 +125,14 @@ exports.mergeOptions = function (parent, child, vm) { } } function merge (key) { - if (!vm && (key === 'el' || key === 'data' || key === 'parent')) { + if ( + !vm && + (key === 'el' || key === 'data' || key === 'parent') + ) { _.warn( - 'The "' + key + '" option can only be used as an instantiation ' + - 'option and will be ignored in Vue.extend().' + 'The "' + key + '" option can only be used as an' + + 'instantiation option and will be ignored in' + + 'Vue.extend().' ) return } diff --git a/src/vue.js b/src/vue.js index 55d7964cece..3cfe7f608f8 100644 --- a/src/vue.js +++ b/src/vue.js @@ -6,7 +6,8 @@ var extend = require('./util').extend * API conventions: * - public API methods/properties are prefiexed with `$` * - internal methods/properties are prefixed with `_` - * - non-prefixed properties are assumed to be proxied user data. + * - non-prefixed properties are assumed to be proxied user + * data. * * @constructor * @param {Object} [options] @@ -38,7 +39,8 @@ Object.defineProperty(p, '$root', { }) /** - * $data has a setter which does a bunch of teardown/setup work + * $data has a setter which does a bunch of + * teardown/setup work */ Object.defineProperty(p, '$data', { From c9414728ab189a573d1c4fb41b6ddf81d0b46ac3 Mon Sep 17 00:00:00 2001 From: Evan You Date: Sun, 10 Aug 2014 14:15:04 -0400 Subject: [PATCH 0122/1534] minor $unwatch change --- src/api/data.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/api/data.js b/src/api/data.js index fbf9f02fc95..e7c935b93ed 100644 --- a/src/api/data.js +++ b/src/api/data.js @@ -81,8 +81,8 @@ exports.$unwatch = function (id) { var watcher = this._watchers[id] if (watcher) { watcher.teardown() + this._watchers[id] = null } - this._watchers[id] = null } /** From 79a87d9cc68f06c2bbc568b6286d90de2e498fe6 Mon Sep 17 00:00:00 2001 From: Evan You Date: Sun, 10 Aug 2014 15:07:53 -0400 Subject: [PATCH 0123/1534] changes --- changes.md | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/changes.md b/changes.md index 434d2761a47..651d9cf074e 100644 --- a/changes.md +++ b/changes.md @@ -102,6 +102,36 @@ Vue.filter('format', { ``` +``` html + +``` + +**Note** The old inline partial syntax `{{> partial}}` has been removed. This is to keep the semantics of interpolation tags purely for interpolation purposes; flow control and partials are now either used in the form of attribute directives or comment directives. + +## Config API change + +Instead of the old `Vue.config()` with a heavily overloaded API, the config object is now available globally as `Vue.config`, and you can simply change its properties: + +``` js +// old +// Vue.config('debug', true) + +// new +Vue.config.debug = true +``` + +## Interpolation Delimiters + +In the old version the interpolation delimiters are limited to the same base character (i.e. `['{','}']` translates into `{{}}` for text and `{{{}}}` for HTML). Now you can set them to whatever you like (*almost), and to indicate HTML interpolation, simply wrap the tag with one extra outer most character on each end. Example: + +``` js +Vue.config.delimiters = ['(%', '%)'] +// tags now are (% %) for text +// and ((% %)) for HTML +``` + +* Note you still cannot use `<` or `>` in delimiters because Vue uses DOM-based templating. + ## (Experimental) Validators This is largely write filters that accept a Boolean return value. Probably should live as a plugin. From 83749bbb858bce915ccca7f515e71fbc20cb07c8 Mon Sep 17 00:00:00 2001 From: Evan You Date: Sun, 10 Aug 2014 16:38:02 -0400 Subject: [PATCH 0124/1534] working on text parser --- src/config.js | 36 +++++++++++++- src/parse/text.js | 117 ++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 151 insertions(+), 2 deletions(-) diff --git a/src/config.js b/src/config.js index 4474e94ab51..1a18f18db76 100644 --- a/src/config.js +++ b/src/config.js @@ -2,6 +2,7 @@ module.exports = { /** * The prefix to look for when parsing directives. + * * @type {String} */ @@ -10,6 +11,7 @@ module.exports = { /** * Whether to print debug messages. * Also enables stack trace for warnings. + * * @type {Boolean} */ @@ -17,6 +19,7 @@ module.exports = { /** * Whether to suppress warnings. + * * @type {Boolean} */ @@ -24,9 +27,38 @@ module.exports = { /** * Whether to parse mustache tags in templates. + * * @type {Boolean} */ - interpolate: true + interpolate: true, -} \ No newline at end of file + /** + * Internal flag to indicate the delimiters have been + * changed. + * + * @type {Boolean} + */ + + _delimitersChanged: true + +} + +/** + * Interpolation delimiters. + * We need to mark the changed flag so that the text parser + * knows it needs to recompile the regex. + * + * @type {Array} + */ + +var delimiters = ['{{', '}}'] +Object.defineProperty(module.exports, 'delimiters', { + get: function () { + return delimiters + }, + set: function (val) { + delimiters = val + this._delimitersChanged = true + } +}) \ No newline at end of file diff --git a/src/parse/text.js b/src/parse/text.js index e69de29bb2d..56ea74b81eb 100644 --- a/src/parse/text.js +++ b/src/parse/text.js @@ -0,0 +1,117 @@ +var Cache = require('../cache') +var config = require('../config') +var regexEscapeRE = /[-.*+?^${}()|[\]\/\\]/g +var cache, tagRE, htmlRE, firstChar, lastChar + +/** + * Escape a string so it can be used in a RegExp + * constructor. + * + * @param {String} str + */ + +function escapeRegex (str) { + return str.replace(regexEscapeRE, '\\$&') +} + +/** + * Compile the interpolation tag regex. + * + * @return {RegExp} + */ + +function compileRegex () { + config._delimitersChanged = false + var open = config.delimiters[0] + var close = config.delimiters[1] + firstChar = open.charAt(0) + lastChar = close.charAt(close.length - 1) + var firstCharRE = escapeRegex(firstChar) + var lastCharRE = escapeRegex(lastChar) + var openRE = escapeRegex(open) + var closeRE = escapeRegex(close) + tagRE = new RegExp( + firstCharRE + '?' + openRE + + '(.+?)' + + closeRE + lastCharRE + '?', + 'g' + ) + htmlRE = new RegExp( + '^' + firstCharRE + openRE + + '.*' + + closeRE + lastCharRE + '$' + ) + // reset cache + cache = new Cache(1000) +} + +/** + * Parse a template text string into an array of tokens. + * + * @param {String} text + * @return {Array | null} + * - {String} type + * - {String} value + * - {Boolean} [html] + * - {Boolean} [oneTime] + */ + +exports.parse = function (text) { + if (config._delimitersChanged) { + compileRegex() + } + var hit = cache.get(text) + if (hit) { + return hit + } + if (!tagRE.test(text)) { + return null + } + var tokens = [] + var lastIndex = tagRE.lastIndex = 0 + var match, index, value, oneTime + while (match = tagRE.exec(text)) { + index = match.index + // push text token + if (index > lastIndex) { + tokens.push({ + value: text.slice(lastIndex, index) + }) + } + // tag token + value = match[1].trim() + oneTime = value.charAt(0) === '*' + tokens.push({ + tag: true, + value: oneTime ? value.slice(1) : value, + html: htmlRE.test(match[0]), + oneTime: oneTime + }) + lastIndex = index + match[0].length + } + if (lastIndex < text.length - 1) { + tokens.push({ + value: text.slice(lastIndex) + }) + } + cache.put(text, tokens) + return tokens +} + +/** + * Parse a template text string into an expression + * + * @param {String} text + * @return {String} + */ + +exports.textToExpression = function (text) { + var tokens = exports.parse(text) + if (tokens) { + return tokens.map(function (token) { + return token.tag + ? token.value + : ('"' + token.value + '"') + }).join('+') + } +} \ No newline at end of file From 94c3c5773f94e36d7c6661c07e89b0356cd0ab1c Mon Sep 17 00:00:00 2001 From: Evan You Date: Sun, 10 Aug 2014 16:46:20 -0400 Subject: [PATCH 0125/1534] $interpolate --- src/api/data.js | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/src/api/data.js b/src/api/data.js index e7c935b93ed..2ef8beac633 100644 --- a/src/api/data.js +++ b/src/api/data.js @@ -86,14 +86,17 @@ exports.$unwatch = function (id) { } /** - * Interpolate a piece of template string. + * Interpolate a piece of template text. * - * @param {String} string + * @param {String} text * @return {String} */ -exports.$interpolate = function (string) { - +exports.$interpolate = function (text) { + var exp = textParser.textToExpression(text) + return exp + ? this.$get(exp) + : text } /** From b6a3a13b22b660d4f8cfe509d942f1f7e5208ee8 Mon Sep 17 00:00:00 2001 From: Evan You Date: Sun, 10 Aug 2014 22:17:30 -0400 Subject: [PATCH 0126/1534] test for text parser --- src/api/data.js | 48 ++++++++++++++--- src/parse/text.js | 30 ++++------- src/util/filter.js | 75 +++++++++++++++++++++++++++ src/util/index.js | 1 + src/watcher.js | 80 ++++------------------------- test/unit/specs/text_parser_spec.js | 67 ++++++++++++++++++++++++ test/unit/specs/util_spec.js | 70 +++++++++++++++++++++++++ 7 files changed, 273 insertions(+), 98 deletions(-) create mode 100644 src/util/filter.js create mode 100644 test/unit/specs/text_parser_spec.js diff --git a/src/api/data.js b/src/api/data.js index 2ef8beac633..91308241120 100644 --- a/src/api/data.js +++ b/src/api/data.js @@ -1,6 +1,9 @@ -var expParser = require('../parse/expression') -var textParser = require('../parse/text') +var _ = require('../util') var Watcher = require('../watcher') +var textParser = require('../parse/text') +var dirParser = require('../parse/directive') +var expParser = require('../parse/expression') +var filterRE = /[^|]\|[^|]/ /** * Get the value from an expression on this vm. @@ -85,6 +88,32 @@ exports.$unwatch = function (id) { } } +/** + * Evaluate a text directive, including filters. + * + * @param {String} text + * @return {String} + */ + +exports.$eval = function (text) { + // check for filters. + if (filterRE.test(text)) { + var dir = dirParser.parse(text)[0] + // the filter regex check might give false positive + // for pipes inside strings, so it's possible that + // we don't get any filters here + return dir.filters + ? _.applyFilters( + this.$get(dir.expression), + _.resolveFilters(this, dir.filters).read + ) + : this.$get(dir.expression) + } else { + // no filter + return this.$get(text) + } +} + /** * Interpolate a piece of template text. * @@ -93,10 +122,17 @@ exports.$unwatch = function (id) { */ exports.$interpolate = function (text) { - var exp = textParser.textToExpression(text) - return exp - ? this.$get(exp) - : text + var tokens = textParser.parse(text) + var vm = this + if (tokens) { + return tokens.map(function (token) { + return token.tag + ? vm.$eval(token.value) + : token.value + }).join('') + } else { + return text + } } /** diff --git a/src/parse/text.js b/src/parse/text.js index 56ea74b81eb..63e6880cfbd 100644 --- a/src/parse/text.js +++ b/src/parse/text.js @@ -1,5 +1,8 @@ +var _ = require('../util') var Cache = require('../cache') var config = require('../config') +var dirParser = require('./directive') + var regexEscapeRE = /[-.*+?^${}()|[\]\/\\]/g var cache, tagRE, htmlRE, firstChar, lastChar @@ -70,6 +73,7 @@ exports.parse = function (text) { var tokens = [] var lastIndex = tagRE.lastIndex = 0 var match, index, value, oneTime + /* jshint boss:true */ while (match = tagRE.exec(text)) { index = match.index // push text token @@ -79,11 +83,13 @@ exports.parse = function (text) { }) } // tag token - value = match[1].trim() - oneTime = value.charAt(0) === '*' + oneTime = match[1].charAt(0) === '*' + value = oneTime + ? match[1].slice(1) + : match[1] tokens.push({ tag: true, - value: oneTime ? value.slice(1) : value, + value: value.trim(), html: htmlRE.test(match[0]), oneTime: oneTime }) @@ -96,22 +102,4 @@ exports.parse = function (text) { } cache.put(text, tokens) return tokens -} - -/** - * Parse a template text string into an expression - * - * @param {String} text - * @return {String} - */ - -exports.textToExpression = function (text) { - var tokens = exports.parse(text) - if (tokens) { - return tokens.map(function (token) { - return token.tag - ? token.value - : ('"' + token.value + '"') - }).join('+') - } } \ No newline at end of file diff --git a/src/util/filter.js b/src/util/filter.js new file mode 100644 index 00000000000..374925fae3b --- /dev/null +++ b/src/util/filter.js @@ -0,0 +1,75 @@ +var _ = require('./debug') + +/** + * Resolve read & write filters for a vm instance. The + * filters descriptor Array comes from the directive parser. + * + * This is extracted into its own utility so it can + * be used in multiple scenarios. + * + * @param {Vue} vm + * @param {Array} filters + * @param {Watcher} [target] + * @return {Object} + */ + +exports.resolveFilters = function (vm, filters, target) { + if (!filters) { + return + } + var res = target || {} + var registry = vm.$options.filters + filters.forEach(function (f) { + var def = registry[f.name] + var args = f.args + var reader, writer + if (!def) { + _.warn('Failed to resolve filter: ' + f.name) + } else if (typeof def === 'function') { + reader = def + } else { + reader = def.read + writer = def.write + } + if (reader) { + if (!res.read) { + res.read = [] + } + res.read.push(function (value) { + return args + ? reader.apply(vm, [value].concat(args)) + : reader.call(vm, value) + }) + } + // only watchers needs write filters + if (target && writer) { + if (!res.write) { + res.write = [] + } + res.write.push(function (value) { + return args + ? writer.apply(vm, [value, res.value].concat(args)) + : writer.call(vm, value, res.value) + }) + } + }) + return res +} + +/** + * Apply filters to a value + * + * @param {*} value + * @param {Array} filters + * @return {*} + */ + +exports.applyFilters = function (value, filters) { + if (!filters) { + return value + } + for (var i = 0, l = filters.length; i < l; i++) { + value = filters[i](value) + } + return value +} \ No newline at end of file diff --git a/src/util/index.js b/src/util/index.js index 9831fa9e4b2..bb65dcb040a 100644 --- a/src/util/index.js +++ b/src/util/index.js @@ -5,4 +5,5 @@ extend(exports, lang) extend(exports, require('./env')) extend(exports, require('./dom')) extend(exports, require('./option')) +extend(exports, require('./filter')) extend(exports, require('./debug')) \ No newline at end of file diff --git a/src/watcher.js b/src/watcher.js index 2c2ddc74cad..ecd9b855081 100644 --- a/src/watcher.js +++ b/src/watcher.js @@ -31,9 +31,14 @@ function Watcher (vm, expression, cb, ctx, filters, needSet) { this.deps = Object.create(null) this.newDeps = Object.create(null) // setup filters if any. - this.initFilters(filters) + // We delegate directive filters here to the watcher + // because they need to be included in the dependency + // collection process. + var res = _.resolveFilters(vm, filters, this) + this.readFilters = res && res.read + this.writeFilters = res && res.write // parse expression for getter/setter - var res = expParser.parse(expression, needSet) + res = expParser.parse(expression, needSet) this.getter = res.get this.setter = res.set this.initDeps(res.paths) @@ -64,55 +69,6 @@ p.initDeps = function (paths) { this.value = this.get() } -/** - * Initialize read and write filters. - * We delegate directive filters here to the watcher - * because they need to be included in the dependency - * collection process. - * - * @param {Array} filters - */ - -p.initFilters = function (filters) { - if (!filters) { - return - } - var self = this - var vm = this.vm - var registry = vm.$options.filters - filters.forEach(function (f) { - var def = registry[f.name] - var args = f.args - var read, write - if (typeof def === 'function') { - read = def - } else { - read = def.read - write = def.write - } - if (read) { - if (!self.readFilters) { - self.readFilters = [] - } - self.readFilters.push(function (value) { - return args - ? read.apply(vm, [value].concat(args)) - : read.call(vm, value) - }) - } - if (write) { - if (!self.writeFilters) { - self.writeFilters = [] - } - self.writeFilters.push(function (value) { - return args - ? write.apply(vm, [value, self.value].concat(args)) - : write.call(vm, value, self.value) - }) - } - }) -} - /** * Add a binding dependency to this directive. * @@ -141,9 +97,7 @@ p.addDep = function (path) { p.get = function () { this.beforeGet() var value = this.getter.call(this.vm, this.vm.$scope) - if (this.readFilters) { - value = applyFilters(value, this.readFilters) - } + value = _.applyFilters(value, this.readFilters) this.afterGet() return value } @@ -155,9 +109,7 @@ p.get = function () { */ p.set = function (value) { - if (this.writeFilters) { - value = applyFilters(value, this.writeFilters) - } + value = _.applyFilters(value, this.writeFilters) this.setter.call(this.vm, this.vm.$scope, value) } @@ -224,18 +176,4 @@ p.teardown = function () { } } -/** - * Apply filters to a value - * - * @param {*} value - * @param {Array} filters - */ - -function applyFilters (value, filters) { - for (var i = 0, l = filters.length; i < l; i++) { - value = filters[i](value) - } - return value -} - module.exports = Watcher \ No newline at end of file diff --git a/test/unit/specs/text_parser_spec.js b/test/unit/specs/text_parser_spec.js new file mode 100644 index 00000000000..9a8a338281d --- /dev/null +++ b/test/unit/specs/text_parser_spec.js @@ -0,0 +1,67 @@ +var textParser = require('../../../src/parse/text') +var config = require('../../../src/config') + +var testCases = [ + { + // no tags + text: 'haha', + expected: null + }, + { + // basic + text: 'a {{ a }} c', + expected: [ + { value: 'a ' }, + { tag: true, value: 'a', html: false, oneTime: false }, + { value: ' c' } + ] + }, + { + // html + text: '{{ text }} and {{{ html }}}', + expected: [ + { tag: true, value: 'text', html: false, oneTime: false }, + { value: ' and ' }, + { tag: true, value: 'html', html: true, oneTime: false }, + ] + }, + { + // one time + text: '{{* text }} and {{{* html }}}', + expected: [ + { tag: true, value: 'text', html: false, oneTime: true }, + { value: ' and ' }, + { tag: true, value: 'html', html: true, oneTime: true }, + ] + } +] + +function assertParse (test) { + var res = textParser.parse(test.text) + var exp = test.expected + if (!Array.isArray(exp)) { + expect(res).toBe(exp) + } else { + expect(res.length).toBe(exp.length) + res.forEach(function (r, i) { + var e = exp[i] + for (var key in e) { + expect(e[key]).toEqual(r[key]) + } + }) + } +} + +describe('Text Parser', function () { + + it('parse', function () { + testCases.forEach(assertParse) + }) + + it('cache', function () { + var res1 = textParser.parse('{{a}}') + var res2 = textParser.parse('{{a}}') + expect(res1).toBe(res2) + }) + +}) \ No newline at end of file diff --git a/test/unit/specs/util_spec.js b/test/unit/specs/util_spec.js index c14b77a50e0..4bdcd441c92 100644 --- a/test/unit/specs/util_spec.js +++ b/test/unit/specs/util_spec.js @@ -134,6 +134,76 @@ describe('Util', function () { }) + describe('Filter', function () { + + var debug = require('../../../src/util/debug') + beforeEach(function () { + spyOn(debug, 'warn') + }) + + it('resolveFilters', function () { + var filters = [ + { name: 'a', args: ['a'] }, + { name: 'b', args: ['b']}, + { name: 'c' } + ] + var vm = { + $options: { + filters: { + a: function (v, arg) { + return { id: 'a', value: v, arg: arg } + }, + b: { + read: function (v, arg) { + return { id: 'b', value: v, arg: arg } + }, + write: function (v, oldVal, arg) { + return { id: 'bw', value: v, arg: arg } + } + } + } + } + } + var target = { + value: 'v' + } + var res = _.resolveFilters(vm, filters, target) + expect(res.read.length).toBe(2) + expect(res.write.length).toBe(1) + + var readA = res.read[0](1) + expect(readA.id).toBe('a') + expect(readA.value).toBe(1) + expect(readA.arg).toBe('a') + + var readB = res.read[1](2) + expect(readB.id).toBe('b') + expect(readB.value).toBe(2) + expect(readB.arg).toBe('b') + + var writeB = res.write[0](3) + expect(writeB.id).toBe('bw') + expect(writeB.value).toBe(3) + expect(writeB.arg).toBe('b') + + expect(debug.warn).toHaveBeenCalled() + }) + + it('applyFilters', function () { + var filters = [ + function (v) { + return v + 2 + }, + function (v) { + return v + 3 + } + ] + var res = _.applyFilters(1, filters) + expect(res).toBe(6) + }) + + }) + if (_.inBrowser) { describe('DOM', function () { From 08f0ca8f39cfdecc81aceb2a38008c49360db98a Mon Sep 17 00:00:00 2001 From: Evan You Date: Sun, 10 Aug 2014 22:24:20 -0400 Subject: [PATCH 0127/1534] move Vue.options to src/vue --- src/api/global.js | 14 -------------- src/directives/index.js | 1 + src/filters/index.js | 1 + src/vue.js | 26 ++++++++++++++++++++------ 4 files changed, 22 insertions(+), 20 deletions(-) diff --git a/src/api/global.js b/src/api/global.js index c7976f52b45..1d05e706802 100644 --- a/src/api/global.js +++ b/src/api/global.js @@ -7,20 +7,6 @@ var assetTypes = [ 'component' ] -/** - * Vue and every constructor that extends Vue has an - * associated options object, which can be accessed during - * compilation steps as `this.constructor.options`. - */ - -exports.options = { - directives : require('../directives'), - filters : require('../filters'), - partials : {}, - effects : {}, - components : {} -} - /** * Expose useful internals */ diff --git a/src/directives/index.js b/src/directives/index.js index e69de29bb2d..2efb8b8eeda 100644 --- a/src/directives/index.js +++ b/src/directives/index.js @@ -0,0 +1 @@ +module.exports = Object.create(null) \ No newline at end of file diff --git a/src/filters/index.js b/src/filters/index.js index e69de29bb2d..2efb8b8eeda 100644 --- a/src/filters/index.js +++ b/src/filters/index.js @@ -0,0 +1 @@ +module.exports = Object.create(null) \ No newline at end of file diff --git a/src/vue.js b/src/vue.js index 3cfe7f608f8..cb3fac43084 100644 --- a/src/vue.js +++ b/src/vue.js @@ -18,6 +18,26 @@ function Vue (options) { this._init(options) } +/** + * Mixin global API + */ + +extend(Vue, require('./api/global')) + +/** + * Vue and every constructor that extends Vue has an + * associated options object, which can be accessed during + * compilation steps as `this.constructor.options`. + */ + +Vue.options = { + directives : require('./directives'), + filters : require('./filters'), + partials : {}, + effects : {}, + components : {} +} + /** * Build up the prototype */ @@ -72,10 +92,4 @@ extend(p, require('./api/dom')) extend(p, require('./api/events')) extend(p, require('./api/lifecycle')) -/** - * Mixin global API - */ - -extend(Vue, require('./api/global')) - module.exports = Vue \ No newline at end of file From ceb415cd4aeb2cb05aab2e40dcfca405996100a5 Mon Sep 17 00:00:00 2001 From: Evan You Date: Sun, 10 Aug 2014 22:36:18 -0400 Subject: [PATCH 0128/1534] remove unused deps --- src/parse/text.js | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/parse/text.js b/src/parse/text.js index 63e6880cfbd..af252989201 100644 --- a/src/parse/text.js +++ b/src/parse/text.js @@ -1,8 +1,5 @@ -var _ = require('../util') var Cache = require('../cache') var config = require('../config') -var dirParser = require('./directive') - var regexEscapeRE = /[-.*+?^${}()|[\]\/\\]/g var cache, tagRE, htmlRE, firstChar, lastChar From 4b5e3e3fb78307a4429db989cde068b6b27a8ac6 Mon Sep 17 00:00:00 2001 From: Evan You Date: Mon, 11 Aug 2014 11:04:35 -0400 Subject: [PATCH 0129/1534] rename test files --- test/unit/specs/api_data_spec.js | 0 .../{scope_spec.js => instance_scope_spec.js} | 0 ...parser_spec.js => parse_directive_spec.js} | 0 ...arser_spec.js => parse_expression_spec.js} | 0 ...path_parser_spec.js => parse_path_spec.js} | 0 test/unit/specs/parse_template_spec.js | 114 +++++ ...text_parser_spec.js => parse_text_spec.js} | 0 test/unit/specs/template.js | 111 ----- test/unit/specs/util_debug_spec.js | 52 ++ test/unit/specs/util_dom_spec.js | 68 +++ test/unit/specs/util_filter_spec.js | 71 +++ test/unit/specs/util_lang_spec.js | 131 +++++ test/unit/specs/util_option_spec.js | 127 +++++ test/unit/specs/util_spec.js | 449 ------------------ 14 files changed, 563 insertions(+), 560 deletions(-) create mode 100644 test/unit/specs/api_data_spec.js rename test/unit/specs/{scope_spec.js => instance_scope_spec.js} (100%) rename test/unit/specs/{directive_parser_spec.js => parse_directive_spec.js} (100%) rename test/unit/specs/{expression_parser_spec.js => parse_expression_spec.js} (100%) rename test/unit/specs/{path_parser_spec.js => parse_path_spec.js} (100%) create mode 100644 test/unit/specs/parse_template_spec.js rename test/unit/specs/{text_parser_spec.js => parse_text_spec.js} (100%) delete mode 100644 test/unit/specs/template.js create mode 100644 test/unit/specs/util_debug_spec.js create mode 100644 test/unit/specs/util_dom_spec.js create mode 100644 test/unit/specs/util_filter_spec.js create mode 100644 test/unit/specs/util_lang_spec.js create mode 100644 test/unit/specs/util_option_spec.js delete mode 100644 test/unit/specs/util_spec.js diff --git a/test/unit/specs/api_data_spec.js b/test/unit/specs/api_data_spec.js new file mode 100644 index 00000000000..e69de29bb2d diff --git a/test/unit/specs/scope_spec.js b/test/unit/specs/instance_scope_spec.js similarity index 100% rename from test/unit/specs/scope_spec.js rename to test/unit/specs/instance_scope_spec.js diff --git a/test/unit/specs/directive_parser_spec.js b/test/unit/specs/parse_directive_spec.js similarity index 100% rename from test/unit/specs/directive_parser_spec.js rename to test/unit/specs/parse_directive_spec.js diff --git a/test/unit/specs/expression_parser_spec.js b/test/unit/specs/parse_expression_spec.js similarity index 100% rename from test/unit/specs/expression_parser_spec.js rename to test/unit/specs/parse_expression_spec.js diff --git a/test/unit/specs/path_parser_spec.js b/test/unit/specs/parse_path_spec.js similarity index 100% rename from test/unit/specs/path_parser_spec.js rename to test/unit/specs/parse_path_spec.js diff --git a/test/unit/specs/parse_template_spec.js b/test/unit/specs/parse_template_spec.js new file mode 100644 index 00000000000..ebd161be995 --- /dev/null +++ b/test/unit/specs/parse_template_spec.js @@ -0,0 +1,114 @@ +var _ = require('../../../src/util') +var templateParser = require('../../../src/parse/template') +var parse = templateParser.parse +var testString = '
hello

world

' + +if (_.inBrowser) { + + describe('Template Parser', function () { + + it('should return same if argument is already a fragment', function () { + var frag = document.createDocumentFragment() + var res = parse(frag) + expect(res).toBe(frag) + }) + + it('should return content if argument is a valid template node', function () { + var templateNode = document.createElement('template') + if (!templateNode.content) { + // mock the content + templateNode.content = document.createDocumentFragment() + } + var res = parse(templateNode) + expect(res).toBe(templateNode.content) + }) + + it('should parse if argument is a template string', function () { + var res = parse(testString) + expect(res instanceof DocumentFragment).toBeTruthy() + expect(res.childNodes.length).toBe(2) + expect(res.querySelector('.test').textContent).toBe('world') + }) + + it('should work if the template string doesn\'t contain tags', function () { + var res = parse('hello!') + expect(res instanceof DocumentFragment).toBeTruthy() + expect(res.childNodes.length).toBe(1) + expect(res.firstChild.nodeType).toBe(3) // Text node + }) + + it('should parse textContent if argument is a script node', function () { + var node = document.createElement('script') + node.textContent = testString + var res = parse(node) + expect(res instanceof DocumentFragment).toBeTruthy() + expect(res.childNodes.length).toBe(2) + expect(res.querySelector('.test').textContent).toBe('world') + }) + + it('should parse innerHTML if argument is a normal node', function () { + var node = document.createElement('div') + node.innerHTML = testString + var res = parse(node) + expect(res instanceof DocumentFragment).toBeTruthy() + expect(res.childNodes.length).toBe(2) + expect(res.querySelector('.test').textContent).toBe('world') + }) + + it('should retrieve and parse if argument is an id selector', function () { + var node = document.createElement('script') + node.setAttribute('id', 'template-test') + node.setAttribute('type', 'x/template') + node.textContent = testString + document.head.appendChild(node) + var res = parse('#template-test') + expect(res instanceof DocumentFragment).toBeTruthy() + expect(res.childNodes.length).toBe(2) + expect(res.querySelector('.test').textContent).toBe('world') + document.head.removeChild(node) + }) + + it('should work for table elements', function () { + var res = parse('hello') + expect(res instanceof DocumentFragment).toBeTruthy() + expect(res.childNodes.length).toBe(1) + expect(res.firstChild.tagName).toBe('TD') + expect(res.firstChild.textContent).toBe('hello') + }) + + it('should work for option elements', function () { + var res = parse('') + expect(res instanceof DocumentFragment).toBeTruthy() + expect(res.childNodes.length).toBe(1) + expect(res.firstChild.tagName).toBe('OPTION') + expect(res.firstChild.textContent).toBe('hello') + }) + + it('should work for svg elements', function () { + var res = parse('') + expect(res instanceof DocumentFragment).toBeTruthy() + expect(res.childNodes.length).toBe(1) + // SVG tagNames should be lowercase because they are XML nodes not HTML + expect(res.firstChild.tagName).toBe('circle') + expect(res.firstChild.namespaceURI).toBe('http://www.w3.org/2000/svg') + }) + + it('should cache template strings', function () { + var res1 = parse(testString) + var res2 = parse(testString) + expect(res1).toBe(res2) + }) + + it('should cache id selectors', function () { + var node = document.createElement('script') + node.setAttribute('id', 'template-test') + node.setAttribute('type', 'x/template') + node.textContent = '
never seen before content
' + document.head.appendChild(node) + var res1 = parse('#template-test') + var res2 = parse('#template-test') + expect(res1).toBe(res2) + document.head.removeChild(node) + }) + }) +} \ No newline at end of file diff --git a/test/unit/specs/text_parser_spec.js b/test/unit/specs/parse_text_spec.js similarity index 100% rename from test/unit/specs/text_parser_spec.js rename to test/unit/specs/parse_text_spec.js diff --git a/test/unit/specs/template.js b/test/unit/specs/template.js deleted file mode 100644 index c7ec5b36834..00000000000 --- a/test/unit/specs/template.js +++ /dev/null @@ -1,111 +0,0 @@ -var templateParser = require('../../../src/parse/template') -var parse = templateParser.parse -var testString = '
hello

world

' - -describe('Template Parser', function () { - - it('should return same if argument is already a fragment', function () { - var frag = document.createDocumentFragment() - var res = parse(frag) - expect(res).toBe(frag) - }) - - it('should return content if argument is a valid template node', function () { - var templateNode = document.createElement('template') - if (!templateNode.content) { - // mock the content - templateNode.content = document.createDocumentFragment() - } - var res = parse(templateNode) - expect(res).toBe(templateNode.content) - }) - - it('should parse if argument is a template string', function () { - var res = parse(testString) - expect(res instanceof DocumentFragment).toBeTruthy() - expect(res.childNodes.length).toBe(2) - expect(res.querySelector('.test').textContent).toBe('world') - }) - - it('should work if the template string doesn\'t contain tags', function () { - var res = parse('hello!') - expect(res instanceof DocumentFragment).toBeTruthy() - expect(res.childNodes.length).toBe(1) - expect(res.firstChild.nodeType).toBe(3) // Text node - }) - - it('should parse textContent if argument is a script node', function () { - var node = document.createElement('script') - node.textContent = testString - var res = parse(node) - expect(res instanceof DocumentFragment).toBeTruthy() - expect(res.childNodes.length).toBe(2) - expect(res.querySelector('.test').textContent).toBe('world') - }) - - it('should parse innerHTML if argument is a normal node', function () { - var node = document.createElement('div') - node.innerHTML = testString - var res = parse(node) - expect(res instanceof DocumentFragment).toBeTruthy() - expect(res.childNodes.length).toBe(2) - expect(res.querySelector('.test').textContent).toBe('world') - }) - - it('should retrieve and parse if argument is an id selector', function () { - var node = document.createElement('script') - node.setAttribute('id', 'template-test') - node.setAttribute('type', 'x/template') - node.textContent = testString - document.head.appendChild(node) - var res = parse('#template-test') - expect(res instanceof DocumentFragment).toBeTruthy() - expect(res.childNodes.length).toBe(2) - expect(res.querySelector('.test').textContent).toBe('world') - document.head.removeChild(node) - }) - - it('should work for table elements', function () { - var res = parse('hello') - expect(res instanceof DocumentFragment).toBeTruthy() - expect(res.childNodes.length).toBe(1) - expect(res.firstChild.tagName).toBe('TD') - expect(res.firstChild.textContent).toBe('hello') - }) - - it('should work for option elements', function () { - var res = parse('') - expect(res instanceof DocumentFragment).toBeTruthy() - expect(res.childNodes.length).toBe(1) - expect(res.firstChild.tagName).toBe('OPTION') - expect(res.firstChild.textContent).toBe('hello') - }) - - it('should work for svg elements', function () { - var res = parse('') - expect(res instanceof DocumentFragment).toBeTruthy() - expect(res.childNodes.length).toBe(1) - // SVG tagNames should be lowercase because they are XML nodes not HTML - expect(res.firstChild.tagName).toBe('circle') - expect(res.firstChild.namespaceURI).toBe('http://www.w3.org/2000/svg') - }) - - it('should cache template strings', function () { - var res1 = parse(testString) - var res2 = parse(testString) - expect(res1).toBe(res2) - }) - - it('should cache id selectors', function () { - var node = document.createElement('script') - node.setAttribute('id', 'template-test') - node.setAttribute('type', 'x/template') - node.textContent = '
never seen before content
' - document.head.appendChild(node) - var res1 = parse('#template-test') - var res2 = parse('#template-test') - expect(res1).toBe(res2) - document.head.removeChild(node) - }) - -}) \ No newline at end of file diff --git a/test/unit/specs/util_debug_spec.js b/test/unit/specs/util_debug_spec.js new file mode 100644 index 00000000000..6c7eb371eaf --- /dev/null +++ b/test/unit/specs/util_debug_spec.js @@ -0,0 +1,52 @@ +var _ = require('../../../src/util') +var config = require('../../../src/config') +config.silent = true + +if (typeof console !== 'undefined') { + + describe('Util - Debug', function () { + + beforeEach(function () { + spyOn(console, 'log') + spyOn(console, 'warn') + if (console.trace) { + spyOn(console, 'trace') + } + }) + + it('log when debug is true', function () { + config.debug = true + _.log('hello', 'world') + expect(console.log).toHaveBeenCalledWith('hello', 'world') + }) + + it('not log when debug is false', function () { + config.debug = false + _.log('bye', 'world') + expect(console.log.calls.count()).toBe(0) + }) + + it('warn when silent is false', function () { + config.silent = false + _.warn('oops', 'ops') + expect(console.warn).toHaveBeenCalledWith('oops', 'ops') + }) + + it('not warn when silent is ture', function () { + config.silent = true + _.warn('oops', 'ops') + expect(console.warn.calls.count()).toBe(0) + }) + + if (console.trace) { + it('trace when not silent and debugging', function () { + config.debug = true + config.silent = false + _.warn('haha') + expect(console.trace).toHaveBeenCalled() + config.debug = false + config.silent = true + }) + } + }) +} \ No newline at end of file diff --git a/test/unit/specs/util_dom_spec.js b/test/unit/specs/util_dom_spec.js new file mode 100644 index 00000000000..57fc1781b1a --- /dev/null +++ b/test/unit/specs/util_dom_spec.js @@ -0,0 +1,68 @@ +var _ = require('../../../src/util') + +if (_.inBrowser) { + + describe('Util - DOM', function () { + + var parent, child, target + + function div () { + return document.createElement('div') + } + + beforeEach(function () { + parent = div() + child = div() + target = div() + parent.appendChild(child) + }) + + it('before', function () { + _.before(target, child) + expect(target.parentNode).toBe(parent) + expect(target.nextSibling).toBe(child) + }) + + it('after', function () { + _.after(target, child) + expect(target.parentNode).toBe(parent) + expect(child.nextSibling).toBe(target) + }) + + it('after with sibling', function () { + var sibling = div() + parent.appendChild(sibling) + _.after(target, child) + expect(target.parentNode).toBe(parent) + expect(child.nextSibling).toBe(target) + }) + + it('remove', function () { + _.remove(child) + expect(child.parentNode).toBeNull() + expect(parent.childNodes.length).toBe(0) + }) + + it('prepend', function () { + _.prepend(target, parent) + expect(target.parentNode).toBe(parent) + expect(parent.firstChild).toBe(target) + }) + + it('prepend to empty node', function () { + parent.removeChild(child) + _.prepend(target, parent) + expect(target.parentNode).toBe(parent) + expect(parent.firstChild).toBe(target) + }) + + it('copyAttributes', function () { + parent.setAttribute('test1', 1) + parent.setAttribute('test2', 2) + _.copyAttributes(parent, target) + expect(target.attributes.length).toBe(2) + expect(target.getAttribute('test1')).toBe('1') + expect(target.getAttribute('test2')).toBe('2') + }) + }) +} \ No newline at end of file diff --git a/test/unit/specs/util_filter_spec.js b/test/unit/specs/util_filter_spec.js new file mode 100644 index 00000000000..642207e990e --- /dev/null +++ b/test/unit/specs/util_filter_spec.js @@ -0,0 +1,71 @@ +var _ = require('../../../src/util') + +describe('Util - Filter', function () { + + var debug = require('../../../src/util/debug') + beforeEach(function () { + spyOn(debug, 'warn') + }) + + it('resolveFilters', function () { + var filters = [ + { name: 'a', args: ['a'] }, + { name: 'b', args: ['b']}, + { name: 'c' } + ] + var vm = { + $options: { + filters: { + a: function (v, arg) { + return { id: 'a', value: v, arg: arg } + }, + b: { + read: function (v, arg) { + return { id: 'b', value: v, arg: arg } + }, + write: function (v, oldVal, arg) { + return { id: 'bw', value: v, arg: arg } + } + } + } + } + } + var target = { + value: 'v' + } + var res = _.resolveFilters(vm, filters, target) + expect(res.read.length).toBe(2) + expect(res.write.length).toBe(1) + + var readA = res.read[0](1) + expect(readA.id).toBe('a') + expect(readA.value).toBe(1) + expect(readA.arg).toBe('a') + + var readB = res.read[1](2) + expect(readB.id).toBe('b') + expect(readB.value).toBe(2) + expect(readB.arg).toBe('b') + + var writeB = res.write[0](3) + expect(writeB.id).toBe('bw') + expect(writeB.value).toBe(3) + expect(writeB.arg).toBe('b') + + expect(debug.warn).toHaveBeenCalled() + }) + + it('applyFilters', function () { + var filters = [ + function (v) { + return v + 2 + }, + function (v) { + return v + 3 + } + ] + var res = _.applyFilters(1, filters) + expect(res).toBe(6) + }) + +}) \ No newline at end of file diff --git a/test/unit/specs/util_lang_spec.js b/test/unit/specs/util_lang_spec.js new file mode 100644 index 00000000000..c37ef1dd059 --- /dev/null +++ b/test/unit/specs/util_lang_spec.js @@ -0,0 +1,131 @@ +var _ = require('../../../src/util') + +describe('Util - Language Enhancement', function () { + + it('bind', function () { + var original = function (a) { + return this.a + a + } + var ctx = { a: 'ctx a ' } + var bound = _.bind(original, ctx) + var res = bound('arg a') + expect(res).toBe('ctx a arg a') + }) + + it('toArray', function () { + // should make a copy of original array + var arr = [1,2,3] + var res = _.toArray(arr) + expect(Array.isArray(res)).toBe(true) + expect(res.toString()).toEqual('1,2,3') + expect(res).not.toBe(arr) + + // should work on arguments + ;(function () { + var res = _.toArray(arguments) + expect(Array.isArray(res)).toBe(true) + expect(res.toString()).toEqual('1,2,3') + })(1,2,3) + }) + + it('extend', function () { + var from = {a:1,b:2} + var to = {} + _.extend(to, from) + expect(to.a).toBe(from.a) + expect(to.b).toBe(from.b) + }) + + it('deepMixin', function () { + var from = Object.create({c:123}) + var to = {} + Object.defineProperty(from, 'a', { + enumerable: false, + configurable: true, + get: function () { + return 'AAA' + } + }) + Object.defineProperty(from, 'b', { + enumerable: true, + configurable: false, + value: 'BBB' + }) + _.deepMixin(to, from) + var descA = Object.getOwnPropertyDescriptor(to, 'a') + var descB = Object.getOwnPropertyDescriptor(to, 'b') + + expect(descA.enumerable).toBe(false) + expect(descA.configurable).toBe(true) + expect(to.a).toBe('AAA') + + expect(descB.enumerable).toBe(true) + expect(descB.configurable).toBe(false) + expect(to.b).toBe('BBB') + + expect(to.c).toBeUndefined() + }) + + it('proxy', function () { + var to = { test2: 'to' } + var from = { test2: 'from' } + var val = '123' + Object.defineProperty(from, 'test', { + get: function () { + return val + }, + set: function (v) { + val = v + } + }) + _.proxy(to, from, 'test') + expect(to.test).toBe(val) + to.test = '234' + expect(val).toBe('234') + expect(to.test).toBe(val) + // should not overwrite existing property + _.proxy(to, from, 'test2') + expect(to.test2).toBe('to') + + }) + + it('isObject', function () { + expect(_.isObject({})).toBe(true) + expect(_.isObject([])).toBe(false) + expect(_.isObject(null)).toBe(false) + if (_.inBrowser) { + expect(_.isObject(window)).toBe(false) + } + }) + + it('isArray', function () { + expect(_.isArray([])).toBe(true) + expect(_.isArray({})).toBe(false) + expect(_.isArray(arguments)).toBe(false) + }) + + it('define', function () { + var obj = {} + _.define(obj, 'test', 123) + expect(obj.test).toBe(123) + var desc = Object.getOwnPropertyDescriptor(obj, 'test') + expect(desc.enumerable).toBe(false) + + _.define(obj, 'test2', 123, true) + expect(obj.test2).toBe(123) + var desc = Object.getOwnPropertyDescriptor(obj, 'test2') + expect(desc.enumerable).toBe(true) + }) + + it('augment', function () { + if ('__proto__' in {}) { + var target = {} + var proto = {} + _.augment(target, proto) + expect(target.__proto__).toBe(proto) + } else { + expect(_.augment).toBe(_.deepMixin) + } + }) + +}) \ No newline at end of file diff --git a/test/unit/specs/util_option_spec.js b/test/unit/specs/util_option_spec.js new file mode 100644 index 00000000000..057432b0948 --- /dev/null +++ b/test/unit/specs/util_option_spec.js @@ -0,0 +1,127 @@ +var _ = require('../../../src/util') + +describe('Util - Option merging', function () { + + var merge = _.mergeOptions + + it('default strat', function () { + // child undefined + var res = merge({replace:true}, {}).replace + expect(res).toBe(true) + // child overwrite + var res = merge({replace:true}, {replace:false}).replace + expect(res).toBe(false) + }) + + it('hooks & paramAttributes', function () { + var fn1 = function () {} + var fn2 = function () {} + var res + // parent undefined + res = merge({}, {created: fn1}).created + expect(Array.isArray(res)).toBe(true) + expect(res.length).toBe(1) + expect(res[0]).toBe(fn1) + // child undefined + res = merge({created: [fn1]}, {}).created + expect(Array.isArray(res)).toBe(true) + expect(res.length).toBe(1) + expect(res[0]).toBe(fn1) + // both defined + res = merge({created: [fn1]}, {created: fn2}).created + expect(Array.isArray(res)).toBe(true) + expect(res.length).toBe(2) + expect(res[0]).toBe(fn1) + expect(res[1]).toBe(fn2) + }) + + it('events', function () { + + var fn1 = function () {} + var fn2 = function () {} + var fn3 = function () {} + var parent = { + events: { + 'fn1': [fn1, fn2], + 'fn2': [fn2] + } + } + var child = { + events: { + 'fn1': fn3, + 'fn3': fn3 + } + } + var res = merge(parent, child).events + assertRes(res.fn1, [fn1, fn2, fn3]) + assertRes(res.fn2, [fn2]) + assertRes(res.fn3, [fn3]) + + function assertRes (res, expected) { + expect(Array.isArray(res)).toBe(true) + expect(res.length).toBe(expected.length) + var i = expected.length + while (i--) { + expect(res[i]).toBe(expected[i]) + } + } + + }) + + it('normal object hashes', function () { + var fn1 = function () {} + var fn2 = function () {} + var res + // parent undefined + res = merge({}, {methods: {test: fn1}}).methods + expect(res.test).toBe(fn1) + // child undefined + res = merge({methods: {test: fn1}}, {}).methods + expect(res.test).toBe(fn1) + // both defined + var parent = {methods: {test: fn1}} + res = merge(parent, {methods: {test2: fn2}}).methods + expect(res.test).toBe(fn1) + expect(res.test2).toBe(fn2) + }) + + it('assets', function () { + var asset1 = {} + var asset2 = {} + var asset3 = {} + // mock vm + var vm = { + $parent: { + $options: { + directives: { + c: asset3 + } + } + } + } + var res = merge( + { directives: { a: asset1 }}, + { directives: { b: asset2 }}, + vm + ).directives + expect(res.a).toBe(asset1) + expect(res.b).toBe(asset2) + expect(res.c).toBe(asset3) + // test prototypal inheritance + var asset4 = vm.$parent.$options.directives.d = {} + expect(res.d).toBe(asset4) + }) + + it('ignore el, data & parent when inheriting', function () { + var res = merge({}, {el:1, data:2, parent:3}) + expect(res.el).toBeUndefined() + expect(res.data).toBeUndefined() + expect(res.parent).toBeUndefined() + + res = merge({}, {el:1, data:2, parent:3}, {}) + expect(res.el).toBe(1) + expect(res.data).toBe(2) + expect(res.parent).toBe(3) + }) + +}) \ No newline at end of file diff --git a/test/unit/specs/util_spec.js b/test/unit/specs/util_spec.js deleted file mode 100644 index 4bdcd441c92..00000000000 --- a/test/unit/specs/util_spec.js +++ /dev/null @@ -1,449 +0,0 @@ -var _ = require('../../../src/util') -var config = require('../../../src/config') -config.silent = true - -describe('Util', function () { - - describe('Language Enhancement', function () { - - it('bind', function () { - var original = function (a) { - return this.a + a - } - var ctx = { a: 'ctx a ' } - var bound = _.bind(original, ctx) - var res = bound('arg a') - expect(res).toBe('ctx a arg a') - }) - - it('toArray', function () { - // should make a copy of original array - var arr = [1,2,3] - var res = _.toArray(arr) - expect(Array.isArray(res)).toBe(true) - expect(res.toString()).toEqual('1,2,3') - expect(res).not.toBe(arr) - - // should work on arguments - ;(function () { - var res = _.toArray(arguments) - expect(Array.isArray(res)).toBe(true) - expect(res.toString()).toEqual('1,2,3') - })(1,2,3) - }) - - it('extend', function () { - var from = {a:1,b:2} - var to = {} - _.extend(to, from) - expect(to.a).toBe(from.a) - expect(to.b).toBe(from.b) - }) - - it('deepMixin', function () { - var from = Object.create({c:123}) - var to = {} - Object.defineProperty(from, 'a', { - enumerable: false, - configurable: true, - get: function () { - return 'AAA' - } - }) - Object.defineProperty(from, 'b', { - enumerable: true, - configurable: false, - value: 'BBB' - }) - _.deepMixin(to, from) - var descA = Object.getOwnPropertyDescriptor(to, 'a') - var descB = Object.getOwnPropertyDescriptor(to, 'b') - - expect(descA.enumerable).toBe(false) - expect(descA.configurable).toBe(true) - expect(to.a).toBe('AAA') - - expect(descB.enumerable).toBe(true) - expect(descB.configurable).toBe(false) - expect(to.b).toBe('BBB') - - expect(to.c).toBeUndefined() - }) - - it('proxy', function () { - var to = { test2: 'to' } - var from = { test2: 'from' } - var val = '123' - Object.defineProperty(from, 'test', { - get: function () { - return val - }, - set: function (v) { - val = v - } - }) - _.proxy(to, from, 'test') - expect(to.test).toBe(val) - to.test = '234' - expect(val).toBe('234') - expect(to.test).toBe(val) - // should not overwrite existing property - _.proxy(to, from, 'test2') - expect(to.test2).toBe('to') - - }) - - it('isObject', function () { - expect(_.isObject({})).toBe(true) - expect(_.isObject([])).toBe(false) - expect(_.isObject(null)).toBe(false) - if (_.inBrowser) { - expect(_.isObject(window)).toBe(false) - } - }) - - it('isArray', function () { - expect(_.isArray([])).toBe(true) - expect(_.isArray({})).toBe(false) - expect(_.isArray(arguments)).toBe(false) - }) - - it('define', function () { - var obj = {} - _.define(obj, 'test', 123) - expect(obj.test).toBe(123) - var desc = Object.getOwnPropertyDescriptor(obj, 'test') - expect(desc.enumerable).toBe(false) - - _.define(obj, 'test2', 123, true) - expect(obj.test2).toBe(123) - var desc = Object.getOwnPropertyDescriptor(obj, 'test2') - expect(desc.enumerable).toBe(true) - }) - - it('augment', function () { - if ('__proto__' in {}) { - var target = {} - var proto = {} - _.augment(target, proto) - expect(target.__proto__).toBe(proto) - } else { - expect(_.augment).toBe(_.deepMixin) - } - }) - - }) - - describe('Filter', function () { - - var debug = require('../../../src/util/debug') - beforeEach(function () { - spyOn(debug, 'warn') - }) - - it('resolveFilters', function () { - var filters = [ - { name: 'a', args: ['a'] }, - { name: 'b', args: ['b']}, - { name: 'c' } - ] - var vm = { - $options: { - filters: { - a: function (v, arg) { - return { id: 'a', value: v, arg: arg } - }, - b: { - read: function (v, arg) { - return { id: 'b', value: v, arg: arg } - }, - write: function (v, oldVal, arg) { - return { id: 'bw', value: v, arg: arg } - } - } - } - } - } - var target = { - value: 'v' - } - var res = _.resolveFilters(vm, filters, target) - expect(res.read.length).toBe(2) - expect(res.write.length).toBe(1) - - var readA = res.read[0](1) - expect(readA.id).toBe('a') - expect(readA.value).toBe(1) - expect(readA.arg).toBe('a') - - var readB = res.read[1](2) - expect(readB.id).toBe('b') - expect(readB.value).toBe(2) - expect(readB.arg).toBe('b') - - var writeB = res.write[0](3) - expect(writeB.id).toBe('bw') - expect(writeB.value).toBe(3) - expect(writeB.arg).toBe('b') - - expect(debug.warn).toHaveBeenCalled() - }) - - it('applyFilters', function () { - var filters = [ - function (v) { - return v + 2 - }, - function (v) { - return v + 3 - } - ] - var res = _.applyFilters(1, filters) - expect(res).toBe(6) - }) - - }) - - if (_.inBrowser) { - - describe('DOM', function () { - - var parent, child, target - - function div () { - return document.createElement('div') - } - - beforeEach(function () { - parent = div() - child = div() - target = div() - parent.appendChild(child) - }) - - it('before', function () { - _.before(target, child) - expect(target.parentNode).toBe(parent) - expect(target.nextSibling).toBe(child) - }) - - it('after', function () { - _.after(target, child) - expect(target.parentNode).toBe(parent) - expect(child.nextSibling).toBe(target) - }) - - it('after with sibling', function () { - var sibling = div() - parent.appendChild(sibling) - _.after(target, child) - expect(target.parentNode).toBe(parent) - expect(child.nextSibling).toBe(target) - }) - - it('remove', function () { - _.remove(child) - expect(child.parentNode).toBeNull() - expect(parent.childNodes.length).toBe(0) - }) - - it('prepend', function () { - _.prepend(target, parent) - expect(target.parentNode).toBe(parent) - expect(parent.firstChild).toBe(target) - }) - - it('prepend to empty node', function () { - parent.removeChild(child) - _.prepend(target, parent) - expect(target.parentNode).toBe(parent) - expect(parent.firstChild).toBe(target) - }) - - it('copyAttributes', function () { - parent.setAttribute('test1', 1) - parent.setAttribute('test2', 2) - _.copyAttributes(parent, target) - expect(target.attributes.length).toBe(2) - expect(target.getAttribute('test1')).toBe('1') - expect(target.getAttribute('test2')).toBe('2') - }) - }) - } - - if (typeof console !== 'undefined') { - - describe('Debug', function () { - - beforeEach(function () { - spyOn(console, 'log') - spyOn(console, 'warn') - if (console.trace) { - spyOn(console, 'trace') - } - }) - - it('log when debug is true', function () { - config.debug = true - _.log('hello', 'world') - expect(console.log).toHaveBeenCalledWith('hello', 'world') - }) - - it('not log when debug is false', function () { - config.debug = false - _.log('bye', 'world') - expect(console.log.calls.count()).toBe(0) - }) - - it('warn when silent is false', function () { - config.silent = false - _.warn('oops', 'ops') - expect(console.warn).toHaveBeenCalledWith('oops', 'ops') - }) - - it('not warn when silent is ture', function () { - config.silent = true - _.warn('oops', 'ops') - expect(console.warn.calls.count()).toBe(0) - }) - - if (console.trace) { - it('trace when not silent and debugging', function () { - config.debug = true - config.silent = false - _.warn('haha') - expect(console.trace).toHaveBeenCalled() - config.debug = false - config.silent = true - }) - } - }) - } - - describe('Option merging', function () { - - var merge = _.mergeOptions - - it('default strat', function () { - // child undefined - var res = merge({replace:true}, {}).replace - expect(res).toBe(true) - // child overwrite - var res = merge({replace:true}, {replace:false}).replace - expect(res).toBe(false) - }) - - it('hooks & paramAttributes', function () { - var fn1 = function () {} - var fn2 = function () {} - var res - // parent undefined - res = merge({}, {created: fn1}).created - expect(Array.isArray(res)).toBe(true) - expect(res.length).toBe(1) - expect(res[0]).toBe(fn1) - // child undefined - res = merge({created: [fn1]}, {}).created - expect(Array.isArray(res)).toBe(true) - expect(res.length).toBe(1) - expect(res[0]).toBe(fn1) - // both defined - res = merge({created: [fn1]}, {created: fn2}).created - expect(Array.isArray(res)).toBe(true) - expect(res.length).toBe(2) - expect(res[0]).toBe(fn1) - expect(res[1]).toBe(fn2) - }) - - it('events', function () { - - var fn1 = function () {} - var fn2 = function () {} - var fn3 = function () {} - var parent = { - events: { - 'fn1': [fn1, fn2], - 'fn2': [fn2] - } - } - var child = { - events: { - 'fn1': fn3, - 'fn3': fn3 - } - } - var res = merge(parent, child).events - assertRes(res.fn1, [fn1, fn2, fn3]) - assertRes(res.fn2, [fn2]) - assertRes(res.fn3, [fn3]) - - function assertRes (res, expected) { - expect(Array.isArray(res)).toBe(true) - expect(res.length).toBe(expected.length) - var i = expected.length - while (i--) { - expect(res[i]).toBe(expected[i]) - } - } - - }) - - it('normal object hashes', function () { - var fn1 = function () {} - var fn2 = function () {} - var res - // parent undefined - res = merge({}, {methods: {test: fn1}}).methods - expect(res.test).toBe(fn1) - // child undefined - res = merge({methods: {test: fn1}}, {}).methods - expect(res.test).toBe(fn1) - // both defined - var parent = {methods: {test: fn1}} - res = merge(parent, {methods: {test2: fn2}}).methods - expect(res.test).toBe(fn1) - expect(res.test2).toBe(fn2) - }) - - it('assets', function () { - var asset1 = {} - var asset2 = {} - var asset3 = {} - // mock vm - var vm = { - $parent: { - $options: { - directives: { - c: asset3 - } - } - } - } - var res = merge( - { directives: { a: asset1 }}, - { directives: { b: asset2 }}, - vm - ).directives - expect(res.a).toBe(asset1) - expect(res.b).toBe(asset2) - expect(res.c).toBe(asset3) - // test prototypal inheritance - var asset4 = vm.$parent.$options.directives.d = {} - expect(res.d).toBe(asset4) - }) - - it('ignore el, data & parent when inheriting', function () { - var res = merge({}, {el:1, data:2, parent:3}) - expect(res.el).toBeUndefined() - expect(res.data).toBeUndefined() - expect(res.parent).toBeUndefined() - - res = merge({}, {el:1, data:2, parent:3}, {}) - expect(res.el).toBe(1) - expect(res.data).toBe(2) - expect(res.parent).toBe(3) - }) - - }) - -}) \ No newline at end of file From 8dda39a346897fa87f91d9ddcf29dc011f44012a Mon Sep 17 00:00:00 2001 From: Evan You Date: Tue, 12 Aug 2014 08:59:49 -0400 Subject: [PATCH 0130/1534] compile wip --- src/instance/compile.js | 17 ++++++++++++++++- src/util/dom.js | 16 ++++++++++++++++ 2 files changed, 32 insertions(+), 1 deletion(-) diff --git a/src/instance/compile.js b/src/instance/compile.js index 1b92c33cdc0..68c4348f704 100644 --- a/src/instance/compile.js +++ b/src/instance/compile.js @@ -47,7 +47,22 @@ exports._compileNode = function (node) { */ exports._compileElement = function (node) { - + var tag = node.tagName + // textarea is pretty annoying + // because its value creates childNodes which + // we don't want to compile. + if (tag === 'TEXTAREA' && node.value) { + node.value = this.$interpolate(node.value) + } + if ( + // skip non-component with no attributes + (!node.hasAttributes() && tag.indexOf('-') < 0) || + // skip v-pre + _.attr(node, 'pre') !== null + ) { + return + } + // TODO } /** diff --git a/src/util/dom.js b/src/util/dom.js index ec38d338fcf..1bd519a95cc 100644 --- a/src/util/dom.js +++ b/src/util/dom.js @@ -1,3 +1,19 @@ +var config = require('../config') + +/** + * Extract an attribute from a node. + * + * @param {Node} node + * @param {String} attr + */ + +exports.attr = function (node, attr) { + attr = config.prefix + '-' + attr + var val = node.getAttribute(attr) + node.removeAttribute(attr) + return val +} + /** * Insert el before target * From f13558463b5890473486b378fb7f0e84084a41d5 Mon Sep 17 00:00:00 2001 From: Evan You Date: Tue, 12 Aug 2014 16:49:33 -0400 Subject: [PATCH 0131/1534] use charCodeAt instead of charAt when possible --- src/instance/scope.js | 6 +++--- src/observe/observer.js | 7 +++++-- src/parse/directive.js | 36 ++++++++++++++++++------------------ src/parse/text.js | 2 +- 4 files changed, 27 insertions(+), 24 deletions(-) diff --git a/src/instance/scope.js b/src/instance/scope.js index 51d92ce7955..f774b962902 100644 --- a/src/instance/scope.js +++ b/src/instance/scope.js @@ -93,7 +93,7 @@ exports._initData = function (data, init) { // delete keys not present in the new data for (key in scope) { if ( - key.charAt(0) !== '$' && + key.charCodeAt(0) !== 0x24 && // $ scope.hasOwnProperty(key) && !(key in data) ) { @@ -269,8 +269,8 @@ exports._syncData = function () { if (locked) { return } - var c = key.charAt(0) - if (c === '$' || c === '_') { + var c = key.charCodeAt(0) + if (c === 0x24 || c === 0x5F) { // $ and _ return } locked = true diff --git a/src/observe/observer.js b/src/observe/observer.js index 67b4bf1a80e..88ad273b8f1 100644 --- a/src/observe/observer.js +++ b/src/observe/observer.js @@ -110,8 +110,11 @@ Observer.create = function (value, options) { p.walk = function (obj) { var key, val, descriptor, prefix for (key in obj) { - prefix = key.charAt(0) - if (prefix === '$' || prefix === '_') { + prefix = key.charCodeAt(0) + if ( + prefix === 0x24 || // $ + prefix === 0x5F // _ + ) { continue } descriptor = Object.getOwnPropertyDescriptor(obj, key) diff --git a/src/parse/directive.js b/src/parse/directive.js index 0abfb6a3a13..51130600748 100644 --- a/src/parse/directive.js +++ b/src/parse/directive.js @@ -93,15 +93,15 @@ exports.parse = function (s) { arg = null for (i = 0, l = str.length; i < l; i++) { - c = str.charAt(i) + c = str.charCodeAt(i) if (inSingle) { // check single quote - if (c === "'") inSingle = !inSingle + if (c === 0x27) inSingle = !inSingle } else if (inDouble) { // check double quote - if (c === '"') inDouble = !inDouble + if (c === 0x22) inDouble = !inDouble } else if ( - c === ',' && + c === 0x2C && // comma !paren && !curly && !square ) { // reached the end of a directive @@ -110,7 +110,7 @@ exports.parse = function (s) { dir = {} begin = argIndex = lastFilterIndex = i + 1 } else if ( - c === ':' && + c === 0x3A && // colon !dir.expression && !dir.arg ) { @@ -121,16 +121,16 @@ exports.parse = function (s) { // an object literal or a ternary expression. if (argRE.test(arg)) { argIndex = i + 1 - argC = arg.charAt(0) + argC = arg.charCodeAt(0) // strip quotes - dir.arg = argC === '"' || argC === "'" + dir.arg = argC === 0x22 || argC === 0x27 ? arg.slice(1, -1) : arg } } else if ( - c === '|' && - str.charAt(i + 1) !== '|' && - str.charAt(i - 1) !== '|' + c === 0x7C && // pipe + str.charCodeAt(i + 1) !== 0x7C && + str.charCodeAt(i - 1) !== 0x7C ) { if (dir.expression === undefined) { // first filter, end of expression @@ -142,14 +142,14 @@ exports.parse = function (s) { } } else { switch (c) { - case '"': inDouble = true; break - case "'": inSingle = true; break - case '(': paren++; break - case ')': paren--; break - case '[': square++; break - case ']': square--; break - case '{': curly++; break - case '}': curly--; break + case 0x22: inDouble = true; break // " + case 0x27: inSingle = true; break // ' + case 0x28: paren++; break // ( + case 0x29: paren--; break // ) + case 0x5B: square++; break // [ + case 0x5D: square--; break // ] + case 0x7B: curly++; break // { + case 0x7D: curly--; break // } } } } diff --git a/src/parse/text.js b/src/parse/text.js index af252989201..e2745b77669 100644 --- a/src/parse/text.js +++ b/src/parse/text.js @@ -80,7 +80,7 @@ exports.parse = function (text) { }) } // tag token - oneTime = match[1].charAt(0) === '*' + oneTime = match[1].charCodeAt(0) === 0x2A // * value = oneTime ? match[1].slice(1) : match[1] From ea067d4275d4b709d6a70b824be9d1d421364c4e Mon Sep 17 00:00:00 2001 From: Evan You Date: Tue, 12 Aug 2014 18:12:09 -0400 Subject: [PATCH 0132/1534] change block instance strategy --- .jshintrc | 3 ++- src/instance/compile.js | 4 ++-- src/instance/element.js | 10 +++++----- src/instance/init.js | 2 +- src/parse/template.js | 2 -- 5 files changed, 10 insertions(+), 11 deletions(-) diff --git a/.jshintrc b/.jshintrc index ee94095c3cb..ec1d511640d 100644 --- a/.jshintrc +++ b/.jshintrc @@ -13,6 +13,7 @@ "eqnull": true, "proto": true, "globals": { - "console": true + "console": true, + "DocumentFragment": true } } \ No newline at end of file diff --git a/src/instance/compile.js b/src/instance/compile.js index 68c4348f704..f8dc8331c14 100644 --- a/src/instance/compile.js +++ b/src/instance/compile.js @@ -8,8 +8,8 @@ var config = require('../config') */ exports._compile = function () { - if (this._isBlock) { - this.$el.forEach(this._compileNode, this) + if (this._blockNodes) { + this._blockNodes.forEach(this._compileNode, this) } else { this._compileNode(this.$el) } diff --git a/src/instance/element.js b/src/instance/element.js index 13ddda847d7..75f4b8b7a6f 100644 --- a/src/instance/element.js +++ b/src/instance/element.js @@ -22,9 +22,9 @@ exports._initElement = function (el) { // instance is considered a "block instance" which manages // not a single element, but multiple elements. A block // instance's `$el` is an Array of the elements it manages. - if (el instanceof window.DocumentFragment) { - this._isBlock = true - this.$el = _.toArray(el.childNodes) + if (el instanceof DocumentFragment) { + this._blockNodes = _.toArray(el.childNodes) + this.$el = document.createComment('vue-block') } else { this.$el = el } @@ -55,8 +55,8 @@ exports._initTemplate = function () { // the template contains multiple nodes // in this case the original `el` is simply // a placeholder. - this._isBlock = true - this.$el = _.toArray(frag.childNodes) + this._blockNodes = _.toArray(frag.childNodes) + this.$el = document.createComment('vue-block') } else { // 1 to 1 replace, we need to copy all the // attributes from the original el to the replacer diff --git a/src/instance/init.js b/src/instance/init.js index 1c149a00624..827cd0f3e95 100644 --- a/src/instance/init.js +++ b/src/instance/init.js @@ -18,7 +18,7 @@ exports._init = function (options) { this.$el = null this._data = options.data || {} - this._isBlock = false + this._blockNodes = null this._isDestroyed = false this._rawContent = null this._emitter = new Emitter(this) diff --git a/src/parse/template.js b/src/parse/template.js index 119a9f09eb7..7c363954544 100644 --- a/src/parse/template.js +++ b/src/parse/template.js @@ -1,5 +1,3 @@ -/* global DocumentFragment */ - var Cache = require('../cache') var templateCache = new Cache(100) From 2c254d15e5245f6a9631df9e4eda2abdd9112601 Mon Sep 17 00:00:00 2001 From: Evan You Date: Tue, 12 Aug 2014 20:09:37 -0400 Subject: [PATCH 0133/1534] working on compile --- changes.md | 4 ++ src/config.js | 2 +- src/directive.js | 3 +- src/instance/compile.js | 121 +++++++++++++++++++++++++++++++++++++--- src/util/dom.js | 2 +- 5 files changed, 122 insertions(+), 10 deletions(-) diff --git a/changes.md b/changes.md index 651d9cf074e..43c6ef760c0 100644 --- a/changes.md +++ b/changes.md @@ -120,6 +120,10 @@ Instead of the old `Vue.config()` with a heavily overloaded API, the config obje Vue.config.debug = true ``` +## Prefix + +Config prefix now should include the hyphen: so the default is now `v-` and if you want to change it make sure to include the hyphen as well. e.g. `Vue.config.prefix = "data-"`. + ## Interpolation Delimiters In the old version the interpolation delimiters are limited to the same base character (i.e. `['{','}']` translates into `{{}}` for text and `{{{}}}` for HTML). Now you can set them to whatever you like (*almost), and to indicate HTML interpolation, simply wrap the tag with one extra outer most character on each end. Example: diff --git a/src/config.js b/src/config.js index 1a18f18db76..fef4a8edee4 100644 --- a/src/config.js +++ b/src/config.js @@ -6,7 +6,7 @@ module.exports = { * @type {String} */ - prefix: 'v', + prefix: 'v-', /** * Whether to print debug messages. diff --git a/src/directive.js b/src/directive.js index 5465344b094..5e9da96a9b8 100644 --- a/src/directive.js +++ b/src/directive.js @@ -31,6 +31,7 @@ function Directive (name, el, vm, descriptor) { this._bound = false // init definition this._initDef() + this._bind() } var p = Directive.prototype @@ -71,7 +72,7 @@ p._bind = function () { ) var value = this._watcher.value if (this.bind) { - this.bind(value) + this.bind() } if (this.update) { this.update(value) diff --git a/src/instance/compile.js b/src/instance/compile.js index f8dc8331c14..38829cd50af 100644 --- a/src/instance/compile.js +++ b/src/instance/compile.js @@ -1,4 +1,7 @@ +var _ = require('../util') var config = require('../config') +var Direcitve = require('../directive') +var dirParser = require('../parse/directive') /** * The main entrance to the compilation process. @@ -41,9 +44,9 @@ exports._compileNode = function (node) { } /** - * Compile an HTMLElement + * Compile an Element * - * @param {HTMLElement} node + * @param {Element} node */ exports._compileElement = function (node) { @@ -54,15 +57,71 @@ exports._compileElement = function (node) { if (tag === 'TEXTAREA' && node.value) { node.value = this.$interpolate(node.value) } + var hasAttributes = node.hasAttributes() + // check priority directives + if (hasAttributes) { + if (this._checkPriorityDirectives(node)) { + return + } + } + // check tag components if ( - // skip non-component with no attributes - (!node.hasAttributes() && tag.indexOf('-') < 0) || - // skip v-pre - _.attr(node, 'pre') !== null + tag.indexOf('-') > 0 && + this.$options.components[tag] ) { + this._bindDirective('component', tag, node) return } - // TODO + // compile normal directives + if (hasAttributes) { + this._compileAttrs(node) + } + // recursively compile childNodes + if (node.hasChildNodes()) { + _.toArray(node.childNodes) + .forEach(this._compileNode, this) + } +} + +/** + * Compile attribtues on an Element + * + * @param {Element} node + */ + +exports._compileAttrs = function (node) { + var attrs = _.toArray(node.attributes) + var i = attrs.length + var registry = this.$options.directives + var dirs = [] + var attr, attrName, dir, dirName + while (i--) { + attr = attrs[i] + attrName = attr.name + if (attrName.indexOf(config.prefix) === 0) { + dirName = attrName.slice(config.prefix.length) + if (registry[dirName]) { + node.removeAttribute(attrName) + dirs.push({ + name: dirName, + value: attr.value + }) + } else { + _.warn('Unknown directive: ' + dirName) + } + } + } + // sort the directives by priority, low to high + dirs.sort(function (a, b) { + a = registry[a.name].priority || 0 + b = registry[b.name].priority || 0 + return a > b ? 1 : -1 + }) + i = dirs.length + while (i--) { + dir = dirs[i] + this._bindDirective(dir.name, dir.value, node) + } } /** @@ -83,4 +142,52 @@ exports._compileTextNode = function (node) { exports._compileComment = function (node) { +} + +/** + * Check for priority directives that would potentially + * skip other directives: + * + * - v-pre + * - v-repeat + * - v-if + * - v-component + * + * @param {Element} node + * @return {Boolean} + */ + +exports._checkPriorityDirectives = function (node) { + var value + /* jshint boss: true */ + if (_.attr(node, 'pre') !== null) { + return true + } else if (value = _.attr(node, 'repeat')) { + this._bindDirective('repeat', value) + return true + } else if (value = _.attr(node, 'if')) { + this._bindDirective('if', value) + return true + } else if (value = _.attr(node, 'component')) { + this._bindDirective('component', value) + return true + } +} + +/** + * Bind a directive. + * + * @param {String} name + * @param {String} value + * @param {Element} node + */ + +exports._bindDirective = function (name, value, node) { + var descriptors = dirParser.parse(value) + var dirs = this._directives + for (var i = 0, l = descriptors.length; i < l; i++) { + dirs.push( + new Direcitve(name, node, this, descriptors[i]) + ) + } } \ No newline at end of file diff --git a/src/util/dom.js b/src/util/dom.js index 1bd519a95cc..c4c4b207028 100644 --- a/src/util/dom.js +++ b/src/util/dom.js @@ -8,7 +8,7 @@ var config = require('../config') */ exports.attr = function (node, attr) { - attr = config.prefix + '-' + attr + attr = config.prefix + attr var val = node.getAttribute(attr) node.removeAttribute(attr) return val From 0dbc76c40555b8e752f27e87c919373ade7c2544 Mon Sep 17 00:00:00 2001 From: Evan You Date: Tue, 12 Aug 2014 20:17:08 -0400 Subject: [PATCH 0134/1534] $el.__vue__ --- src/api/lifecycle.js | 2 ++ src/instance/element.js | 1 + 2 files changed, 3 insertions(+) diff --git a/src/api/lifecycle.js b/src/api/lifecycle.js index 9e9f7099232..549546f909f 100644 --- a/src/api/lifecycle.js +++ b/src/api/lifecycle.js @@ -37,6 +37,8 @@ exports.$destroy = function (remove) { this.$remove() } } + this.$el.__vue__ = null + this.$el = null var i // remove self from parent. only necessary // if this is called by the user. diff --git a/src/instance/element.js b/src/instance/element.js index 75f4b8b7a6f..ca6b86734c8 100644 --- a/src/instance/element.js +++ b/src/instance/element.js @@ -28,6 +28,7 @@ exports._initElement = function (el) { } else { this.$el = el } + this.$el.__vue__ = this this._initTemplate() this._initContent() } From 4e062e9270c90d6c4e4105bb08f5a1bc61270e2e Mon Sep 17 00:00:00 2001 From: Evan You Date: Wed, 13 Aug 2014 01:30:33 -0400 Subject: [PATCH 0135/1534] basic directives working --- changes.md | 4 ++ src/directives/attr.js | 20 ++++++++ src/directives/html.js | 33 +++++++++++++ src/directives/index.js | 19 +++++++- src/directives/text.js | 11 +++++ src/instance/compile.js | 67 +++++++++++++++++++++++++- src/instance/element.js | 3 +- src/parse/template.js | 7 ++- src/util/dom.js | 4 +- src/util/lang.js | 31 ++++++++++++ test/unit/specs/parse_template_spec.js | 6 +++ test/unit/specs/util_dom_spec.js | 7 +++ test/unit/specs/util_lang_spec.js | 15 ++++++ 13 files changed, 220 insertions(+), 7 deletions(-) create mode 100644 src/directives/attr.js create mode 100644 src/directives/html.js create mode 100644 src/directives/text.js diff --git a/changes.md b/changes.md index 43c6ef760c0..1422205cd8b 100644 --- a/changes.md +++ b/changes.md @@ -71,6 +71,10 @@ computed: { - `isFn` is no longer necessary for directives expecting function values. +## Interpolation + +Text bindings will no longer automatically stringify objects. Use the new `json` filter which gives more flexibility in formatting. Also, `null` will now be printed as is; only `undefined` will yield empty string. + ## Two Way filters If a filter is defined as a function, it is treated as a read filter by default - i.e. it is applied when data is read from the model and applied to the DOM. You can now specify write filters as well, which are applied when writing to the model, triggered by user input. Write filters are only triggered on two-way bindings like `v-model`. diff --git a/src/directives/attr.js b/src/directives/attr.js new file mode 100644 index 00000000000..0d9f73c01b6 --- /dev/null +++ b/src/directives/attr.js @@ -0,0 +1,20 @@ +var _ = require('../util') + +exports.bind = function () { + var params = this.vm.$options.paramAttributes + this.isParam = + this.el.__vue__ && // only check rootNode for params + params && + params.indexOf(this.arg) > -1 +} + +exports.update = function (value) { + if (value || value === 0) { + this.el.setAttribute(this.arg, value) + } else { + this.el.removeAttribute(this.arg) + } + if (this.isParam) { + this.vm[this.arg] = _.guardNumber(value) + } +} \ No newline at end of file diff --git a/src/directives/html.js b/src/directives/html.js new file mode 100644 index 00000000000..d9ef7b72c6c --- /dev/null +++ b/src/directives/html.js @@ -0,0 +1,33 @@ +var _ = require('../util') +var templateParser = require('../parse/template') + +exports.bind = function () { + // a comment node means this is a binding for + // {{{ inline unescaped html }}} + if (this.el.nodeType === 8) { + // hold nodes + this.nodes = [] + } +} + +exports.update = function (value) { + value = _.guard(value) + if (this.nodes) { + this.swap(value) + } else { + this.el.innerHTML = value + } +} + +exports.swap = function (value) { + // remove old nodes + var i = this.nodes.length + while (i--) { + _.remove(this.nodes[i]) + } + // convert new value to a fragment + var frag = templateParser.parse(value, true) + // save a reference to these nodes so we can remove later + this.nodes = _.toArray(frag.childNodes) + _.before(frag, this.el) +} \ No newline at end of file diff --git a/src/directives/index.js b/src/directives/index.js index 2efb8b8eeda..9692dda30c3 100644 --- a/src/directives/index.js +++ b/src/directives/index.js @@ -1 +1,18 @@ -module.exports = Object.create(null) \ No newline at end of file +var directives = module.exports = Object.create(null) + +directives.text = require('./text') +directives.html = require('./html') +directives.attr = require('./attr') +// directives.show = require('./show') +// directives['class'] = require('./class') +// directives.ref = require('./ref') +// directives.cloak = require('./cloak') +// directives.on = require('./on') +// directives.repeat = require('./repeat') +// directives.model = require('./model') +// directives['if'] = require('./if') +// directives['with'] = require('./with') +// directives.html = require('./html') +// directives.style = require('./style') +// directives.partial = require('./partial') +// directives.view = require('./view') \ No newline at end of file diff --git a/src/directives/text.js b/src/directives/text.js new file mode 100644 index 00000000000..a60bd5be7fb --- /dev/null +++ b/src/directives/text.js @@ -0,0 +1,11 @@ +var _ = require('../util') + +exports.bind = function () { + this.attr = this.el.nodeType === 3 + ? 'nodeValue' + : 'textContent' +} + +exports.update = function (value) { + this.el[this.attr] = _.guard(value) +} \ No newline at end of file diff --git a/src/instance/compile.js b/src/instance/compile.js index 38829cd50af..ca134c48336 100644 --- a/src/instance/compile.js +++ b/src/instance/compile.js @@ -1,7 +1,9 @@ var _ = require('../util') var config = require('../config') var Direcitve = require('../directive') +var textParser = require('../parse/text') var dirParser = require('../parse/directive') +var templateParser = require('../parse/template') /** * The main entrance to the compilation process. @@ -109,6 +111,8 @@ exports._compileAttrs = function (node) { } else { _.warn('Unknown directive: ' + dirName) } + } else if (config.interpolate) { + this._bindAttr(node, attr) } } // sort the directives by priority, low to high @@ -131,7 +135,38 @@ exports._compileAttrs = function (node) { */ exports._compileTextNode = function (node) { - + var tokens = textParser.parse(node.nodeValue) + if (!tokens) { + return + } + var el, token, value + for (var i = 0, l = tokens.length; i < l; i++) { + token = tokens[i] + if (token.tag) { + if (token.oneTime) { + value = this.$get(token.value) + el = token.html + ? templateParser.parse(value, true) + : document.createTextNode(value) + _.before(el, node) + } else { + value = token.value + if (token.html) { + el = document.createComment('vue-html') + _.before(el, node) + this._bindDirective('html', value, el) + } else { + el = document.createTextNode('') + _.before(el, node) + this._bindDirective('text', value, el) + } + } + } else { + el = document.createTextNode(token.value) + _.before(el, node) + } + } + _.remove(node) } /** @@ -144,6 +179,36 @@ exports._compileComment = function (node) { } +/** + * Check an attribute for potential bindings + */ + +exports._bindAttr = function (node, attr) { + var tokens = textParser.parse(attr.value) + if (!tokens) { + return + } + var expression = tokens.map(expifyToken).join('+') + this._bindDirective( + 'attr', + attr.name + ':' + expression, + node + ) +} + +/** + * Helper to translate token value into expression parts. + * + * @param {Object} token + * @return {String} + */ + +function expifyToken (token) { + return token.tag + ? token.value + : ("'" + token.value + "'") +} + /** * Check for priority directives that would potentially * skip other directives: diff --git a/src/instance/element.js b/src/instance/element.js index ca6b86734c8..58e73c33d35 100644 --- a/src/instance/element.js +++ b/src/instance/element.js @@ -43,13 +43,12 @@ exports._initTemplate = function () { var options = this.$options var template = options.template if (template) { - var frag = templateParser.parse(template) + var frag = templateParser.parse(template, true) if (!frag) { _.warn('Invalid template option: ' + template) } else { // collect raw content. this wipes out $el. this._collectRawContent() - frag = frag.cloneNode(true) if (options.replace) { // replace if (frag.childNodes.length > 1) { diff --git a/src/parse/template.js b/src/parse/template.js index 7c363954544..31f88891e28 100644 --- a/src/parse/template.js +++ b/src/parse/template.js @@ -115,10 +115,11 @@ function nodeToFragment (node) { * - Node object of type Template * - id selector: '#some-template-id' * - template string: '
{{msg}}
' + * @param {Boolean} clone * @return {DocumentFragment|undefined} */ -exports.parse = function (template) { +exports.parse = function (template, clone) { var node, frag // if the template is already a document fragment, @@ -149,5 +150,7 @@ exports.parse = function (template) { frag = nodeToFragment(template) } - return frag + return frag && clone + ? frag.cloneNode(true) + : frag } \ No newline at end of file diff --git a/src/util/dom.js b/src/util/dom.js index c4c4b207028..cb437ff313b 100644 --- a/src/util/dom.js +++ b/src/util/dom.js @@ -10,7 +10,9 @@ var config = require('../config') exports.attr = function (node, attr) { attr = config.prefix + attr var val = node.getAttribute(attr) - node.removeAttribute(attr) + if (val !== null) { + node.removeAttribute(attr) + } return val } diff --git a/src/util/lang.js b/src/util/lang.js index d6aa11f9c82..c211550011e 100644 --- a/src/util/lang.js +++ b/src/util/lang.js @@ -1,3 +1,34 @@ +/** + * Guard text output, make sure undefined outputs + * empty string + * + * @param {*} value + * @return {String} + */ + +exports.guard = function (value) { + return value === undefined + ? '' + : value +} + +/** + * Check and convert possible numeric numbers before + * setting back to data + * + * @param {*} value + * @return {*|Number} + */ + +exports.guardNumber = function (value) { + return ( + isNaN(value) || + value === null || + typeof value === 'boolean' + ) ? value + : Number(value) +} + /** * Simple bind, faster than native * diff --git a/test/unit/specs/parse_template_spec.js b/test/unit/specs/parse_template_spec.js index ebd161be995..d69c56c66ed 100644 --- a/test/unit/specs/parse_template_spec.js +++ b/test/unit/specs/parse_template_spec.js @@ -99,6 +99,12 @@ if (_.inBrowser) { expect(res1).toBe(res2) }) + it('should clone', function () { + var res1 = parse(testString, true) + var res2 = parse(testString, true) + expect(res1).not.toBe(res2) + }) + it('should cache id selectors', function () { var node = document.createElement('script') node.setAttribute('id', 'template-test') diff --git a/test/unit/specs/util_dom_spec.js b/test/unit/specs/util_dom_spec.js index 57fc1781b1a..670810b5088 100644 --- a/test/unit/specs/util_dom_spec.js +++ b/test/unit/specs/util_dom_spec.js @@ -16,6 +16,13 @@ if (_.inBrowser) { target = div() parent.appendChild(child) }) + + it('attr', function () { + target.setAttribute('v-test', 'ok') + var val = _.attr(target, 'test') + expect(val).toBe('ok') + expect(target.hasAttribute('v-test')).toBe(false) + }) it('before', function () { _.before(target, child) diff --git a/test/unit/specs/util_lang_spec.js b/test/unit/specs/util_lang_spec.js index c37ef1dd059..ac942698fea 100644 --- a/test/unit/specs/util_lang_spec.js +++ b/test/unit/specs/util_lang_spec.js @@ -2,6 +2,21 @@ var _ = require('../../../src/util') describe('Util - Language Enhancement', function () { + it('guard', function () { + expect(_.guard(1)).toBe(1) + expect(_.guard(null)).toBe(null) + expect(_.guard(undefined)).toBe('') + }) + + it('guardNumber', function () { + expect(_.guardNumber('12')).toBe(12) + expect(_.guardNumber('1e5')).toBe(1e5) + expect(_.guardNumber('0x2F')).toBe(0x2F) + expect(_.guardNumber(null)).toBe(null) + expect(_.guardNumber(true)).toBe(true) + expect(_.guardNumber('hello')).toBe('hello') + }) + it('bind', function () { var original = function (a) { return this.a + a From ec5798540b41c4098f8f1ed09198a71caba1ea6f Mon Sep 17 00:00:00 2001 From: Evan You Date: Thu, 14 Aug 2014 12:08:18 -0400 Subject: [PATCH 0136/1534] support namespaced attributes --- src/directives/attr.js | 35 +++++++++++++++++++++++++++++++---- src/instance/compile.js | 18 +++++++++++++++--- src/parse/template.js | 30 +++++++++++++++++++++++++----- 3 files changed, 71 insertions(+), 12 deletions(-) diff --git a/src/directives/attr.js b/src/directives/attr.js index 0d9f73c01b6..22bde603411 100644 --- a/src/directives/attr.js +++ b/src/directives/attr.js @@ -1,20 +1,47 @@ var _ = require('../util') +// SVG xml namespaces +var namespaces = { + xlink: 'http://www.w3.org/1999/xlink', + ev: 'http://www.w3.org/2001/xml-events' +} + exports.bind = function () { + var name = this.arg + // check param attributes var params = this.vm.$options.paramAttributes this.isParam = this.el.__vue__ && // only check rootNode for params params && - params.indexOf(this.arg) > -1 + params.indexOf(name) > -1 + // check namespaced attributes + if (this.el.namespaceURI.slice(-3) === 'svg') { + var colonIndex = name.indexOf(':') + if (colonIndex > 0) { + this.localName = name.slice(colonIndex + 1) + this.namespace = namespaces[name.slice(0, colonIndex)] + } + } } exports.update = function (value) { + var el = this.el + var ns = this.namespace + var name = this.arg if (value || value === 0) { - this.el.setAttribute(this.arg, value) + if (ns) { + el.setAttributeNS(ns, name, value) + } else { + el.setAttribute(name, value) + } } else { - this.el.removeAttribute(this.arg) + if (ns) { + el.removeAttributeNS(ns, this.localName) + } else { + el.removeAttribute(name) + } } if (this.isParam) { - this.vm[this.arg] = _.guardNumber(value) + this.vm[name] = _.guardNumber(value) } } \ No newline at end of file diff --git a/src/instance/compile.js b/src/instance/compile.js index ca134c48336..a0a61f1e7d5 100644 --- a/src/instance/compile.js +++ b/src/instance/compile.js @@ -109,7 +109,7 @@ exports._compileAttrs = function (node) { value: attr.value }) } else { - _.warn('Unknown directive: ' + dirName) + _.warn('Failed to resolve directive: ' + dirName) } } else if (config.interpolate) { this._bindAttr(node, attr) @@ -188,10 +188,22 @@ exports._bindAttr = function (node, attr) { if (!tokens) { return } - var expression = tokens.map(expifyToken).join('+') + if (tokens.length > 1) { + _.warn( + 'Invalid attribute binding: "' + attr.value + '"' + + '\nUse one single interpolation tag in ' + + 'attribute bindings.' + ) + return + } + // wrap namespaced attribute so it won't mess up + // the directive parser + var arg = attr.name.indexOf(':') > 0 + ? "'" + attr.name + "'" + : attr.name this._bindDirective( 'attr', - attr.name + ':' + expression, + arg + ':' + tokens[0].value, node ) } diff --git a/src/parse/template.js b/src/parse/template.js index 31f88891e28..7275e8066e9 100644 --- a/src/parse/template.js +++ b/src/parse/template.js @@ -2,17 +2,29 @@ var Cache = require('../cache') var templateCache = new Cache(100) var map = { + _default : [0, '', ''], legend : [1, '
', '
'], tr : [2, '', '
'], - col : [2, '', '
'], - _default : [0, '', ''] + col : [ + 2, + '', + '
' + ], } map.td = -map.th = [3, '', '
'] +map.th = [ + 3, + '', + '
' +] map.option = -map.optgroup = [1, ''] +map.optgroup = [ + 1, + '' +] map.thead = map.tbody = @@ -29,7 +41,15 @@ map.line = map.path = map.polygon = map.polyline = -map.rect = [1, '',''] +map.rect = [ + 1, + '', + '' +] var TAG_RE = /<([\w:]+)/ From 59202297867a28dc8ef37a52c39971203729fc5f Mon Sep 17 00:00:00 2001 From: Evan You Date: Fri, 15 Aug 2014 11:10:42 -0400 Subject: [PATCH 0137/1534] path.set/watcher.set should create if doesnt exist --- src/parse/path.js | 49 +++++++++++++++++++++++++----- test/unit/specs/parse_path_spec.js | 19 +++++------- test/unit/specs/watcher_spec.js | 12 ++++++++ 3 files changed, 62 insertions(+), 18 deletions(-) diff --git a/src/parse/path.js b/src/parse/path.js index 492870d3aa4..465b8bd3d85 100644 --- a/src/parse/path.js +++ b/src/parse/path.js @@ -317,18 +317,53 @@ exports.set = function (obj, path, val) { if (typeof path === 'string') { path = exports.parse(path) } - if (!path) { + if (!path || !isSettable(obj)) { return false } + var last, key for (var i = 0, l = path.length - 1; i < l; i++) { - if (!obj || typeof obj !== 'object') { - return false + last = obj + key = path[i] + obj = obj[key] + if (!isSettable(obj)) { + obj = {} + add(last, key, obj) } - obj = obj[path[i]] } - if (!obj || typeof obj !== 'object') { - return false + key = path[i] + if (obj.hasOwnProperty(key)) { + obj[key] = val + } else { + add(obj, key, val) } - obj[path[i]] = val return true +} + +/** + * Check if a value is an object that can have values + * set on it. Slightly faster than _.isObject. + * + * @param {*} val + * @return {Boolean} + */ + +function isSettable (val) { + return val && typeof val === 'object' +} + +/** + * Add a property to an object, using $add if target + * has been augmented by Vue's observer. + * + * @param {Object} obj + * @param {String} key + * @param {*} val + */ + +function add (obj, key, val) { + if (obj.$add) { + obj.$add(key, val) + } else { + obj[key] = val + } } \ No newline at end of file diff --git a/test/unit/specs/parse_path_spec.js b/test/unit/specs/parse_path_spec.js index 0a0e90a7066..e9cbd2c9656 100644 --- a/test/unit/specs/parse_path_spec.js +++ b/test/unit/specs/parse_path_spec.js @@ -107,7 +107,7 @@ describe('Path', function () { expect(Path.getFromObserver(obj, ['a','c'].join(delim))).toBeUndefined() }) - it('set success', function () { + it('set', function () { var path = 'a.b.c' var obj = { a: { @@ -121,19 +121,16 @@ describe('Path', function () { expect(obj.a.b.c).toBe(12345) }) - it('set fail', function () { - var obj = { - a: null - } - var res = Path.set(obj, 'a.b.c', 12345) - expect(res).toBe(false) - res = Path.set(obj, 'a.b', 12345) - expect(res).toBe(false) - }) - it('set invalid', function () { var res = Path.set({}, 'ab[c]d', 123) expect(res).toBe(false) }) + it('force set', function () { + var target = {} + var res = Path.set(target, 'a.b.c', 123, true) + expect(res).toBe(true) + expect(target.a.b.c).toBe(123) + }) + }) \ No newline at end of file diff --git a/test/unit/specs/watcher_spec.js b/test/unit/specs/watcher_spec.js index 571e46fa58c..67dd731d637 100644 --- a/test/unit/specs/watcher_spec.js +++ b/test/unit/specs/watcher_spec.js @@ -236,6 +236,18 @@ describe('Watcher', function () { }) }) + it('set non-existent values', function (done) { + var watcher = new Watcher(vm, 'd.e.f', spy) + expect(watcher.value).toBeUndefined() + watcher.set(123) + nextTick(function () { + expect(vm.d.e.f).toBe(123) + expect(watcher.value).toBe(123) + expect(spy).toHaveBeenCalledWith(123, undefined) + done() + }) + }) + it('teardown', function (done) { var watcher = new Watcher(vm, 'b.c', spy) watcher.teardown() From 762e1a7816aafa80646f3336217145ac85622645 Mon Sep 17 00:00:00 2001 From: Evan You Date: Fri, 15 Aug 2014 22:32:39 -0400 Subject: [PATCH 0138/1534] _observer -> $observer --- src/instance/bindings.js | 2 +- src/instance/scope.js | 16 ++++++++-------- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/src/instance/bindings.js b/src/instance/bindings.js index d8e12363877..148c0f1bee2 100644 --- a/src/instance/bindings.js +++ b/src/instance/bindings.js @@ -25,7 +25,7 @@ exports._initBindings = function () { root._addChild('$root', this.$root._rootBinding) } // setup observer events - this._observer + this.$observer // simple updates .on('set', this._updateBindingAt) .on('mutate', this._updateBindingAt) diff --git a/src/instance/scope.js b/src/instance/scope.js index f774b962902..af4da82d807 100644 --- a/src/instance/scope.js +++ b/src/instance/scope.js @@ -18,7 +18,7 @@ exports._initScope = function () { ? Object.create(parent.$scope) : {} // create scope observer - this._observer = Observer.create(scope, { + this.$observer = Observer.create(scope, { callbackContext: this, doNotAlterProto: true }) @@ -27,8 +27,8 @@ exports._initScope = function () { // relay change events that sent down from // the scope prototype chain. - var ob = this._observer - var pob = parent._observer + var ob = this.$observer + var pob = parent.$observer var listeners = this._scopeListeners = {} scopeEvents.forEach(function (event) { var cb = listeners[event] = function (key, a, b) { @@ -50,11 +50,11 @@ exports._initScope = function () { */ exports._teardownScope = function () { - this._observer.off() + this.$observer.off() this._unsyncData() this.$scope = null if (this.$parent) { - var pob = this.$parent._observer + var pob = this.$parent.$observer var listeners = this._scopeListeners scopeEvents.forEach(function (event) { pob.off(event, listeners[event]) @@ -146,7 +146,7 @@ exports._initProxy = function () { } } // keep proxying up-to-date with added/deleted keys. - this._observer + this.$observer .on('add:self', function (key) { _.proxy(this, scope, key) }) @@ -245,7 +245,7 @@ exports._syncData = function () { } // sync scope and original data. - this._observer + this.$observer .on('set:self', listeners.data.set) .on('add:self', listeners.data.add) .on('delete:self', listeners.data.delete) @@ -287,7 +287,7 @@ exports._syncData = function () { exports._unsyncData = function () { var listeners = this._syncListeners - this._observer + this.$observer .off('set:self', listeners.data.set) .off('add:self', listeners.data.add) .off('delete:self', listeners.data.delete) From fc80d8186e467f74bf767711fb361a8ed3f4c848 Mon Sep 17 00:00:00 2001 From: Evan You Date: Sat, 16 Aug 2014 00:24:25 -0400 Subject: [PATCH 0139/1534] inheritScope option --- changes.md | 6 ++++++ src/instance/scope.js | 5 +++-- src/vue.js | 14 +++++++++----- test/unit/specs/instance_scope_spec.js | 24 ++++++++++++------------ 4 files changed, 30 insertions(+), 19 deletions(-) diff --git a/changes.md b/changes.md index 1422205cd8b..927ec1ea405 100644 --- a/changes.md +++ b/changes.md @@ -34,6 +34,12 @@ It's probably easy to understand why `el` and `parent` are instance only. But wh When events are used extensively for cross-vm communication, the ready hook can get kinda messy. The new `events` option is similar to its Backbone equivalent, where you can declaratiely register a bunch of event listeners. +### new option: `inheritScope`. + +Default: `true`. + +Whether to inherit parent scope data. Set it to `false` if you want to create a component that have an isolated scope of its own. + ### removed options: `id`, `tagName`, `className`, `attributes`, `lazy`. Since now a vm must always be provided the `el` option or explicitly mounted to an existing element, the element creation releated options have been removed for simplicity. If you need to modify your element's attributes, simply do so in the new `beforeMount` hook. diff --git a/src/instance/scope.js b/src/instance/scope.js index af4da82d807..69d61c561be 100644 --- a/src/instance/scope.js +++ b/src/instance/scope.js @@ -14,7 +14,8 @@ var scopeEvents = ['set', 'mutate', 'add', 'delete'] exports._initScope = function () { var parent = this.$parent - var scope = this.$scope = parent + var inherit = parent && this.$options.inheritScope + var scope = this.$scope = inherit ? Object.create(parent.$scope) : {} // create scope observer @@ -23,7 +24,7 @@ exports._initScope = function () { doNotAlterProto: true }) - if (!parent) return + if (!inherit) return // relay change events that sent down from // the scope prototype chain. diff --git a/src/vue.js b/src/vue.js index cb3fac43084..41265bc7b6b 100644 --- a/src/vue.js +++ b/src/vue.js @@ -28,14 +28,18 @@ extend(Vue, require('./api/global')) * Vue and every constructor that extends Vue has an * associated options object, which can be accessed during * compilation steps as `this.constructor.options`. + * + * These can be seen as the default options of every + * Vue instance. */ Vue.options = { - directives : require('./directives'), - filters : require('./filters'), - partials : {}, - effects : {}, - components : {} + directives : require('./directives'), + filters : require('./filters'), + partials : {}, + effects : {}, + components : {}, + inheritScope : true } /** diff --git a/test/unit/specs/instance_scope_spec.js b/test/unit/specs/instance_scope_spec.js index 861cd5270f3..ff795d40e4e 100644 --- a/test/unit/specs/instance_scope_spec.js +++ b/test/unit/specs/instance_scope_spec.js @@ -33,7 +33,7 @@ describe('Scope', function () { it('should trigger set events', function () { var spy = jasmine.createSpy('basic') - vm._observer.on('set', spy) + vm.$observer.on('set', spy) // set on scope vm.$scope.a = 2 @@ -48,7 +48,7 @@ describe('Scope', function () { it('should trigger add/delete events', function () { var spy = jasmine.createSpy('instantiation') - vm._observer + vm.$observer .on('add', spy) .on('delete', spy) @@ -159,25 +159,25 @@ describe('Scope', function () { // when a shadowed property changed on parent scope, // the event should NOT be propagated down var spy = jasmine.createSpy('inheritance') - child._observer.on('set', spy) + child.$observer.on('set', spy) parent.c = 'c changed' expect(spy.calls.count()).toBe(1) expect(spy).toHaveBeenCalledWith('c', 'c changed', u) spy = jasmine.createSpy('inheritance') - child._observer.on('add', spy) + child.$observer.on('add', spy) parent.$scope.$add('e', 123) expect(spy.calls.count()).toBe(1) expect(spy).toHaveBeenCalledWith('e', 123, u) spy = jasmine.createSpy('inheritance') - child._observer.on('delete', spy) + child.$observer.on('delete', spy) parent.$scope.$delete('e') expect(spy.calls.count()).toBe(1) expect(spy).toHaveBeenCalledWith('e', u, u) spy = jasmine.createSpy('inheritance') - child._observer.on('mutate', spy) + child.$observer.on('mutate', spy) parent.arr.reverse() expect(spy.calls.mostRecent().args[0]).toBe('arr') expect(spy.calls.mostRecent().args[1]).toBe(parent.arr) @@ -189,7 +189,7 @@ describe('Scope', function () { // when a shadowed property changed on parent scope, // the event should NOT be propagated down var spy = jasmine.createSpy('inheritance') - child._observer.on('set', spy) + child.$observer.on('set', spy) parent.a = 'a changed' expect(spy.calls.count()).toBe(0) }) @@ -213,8 +213,8 @@ describe('Scope', function () { var parentSpy = jasmine.createSpy('parent') var childSpy = jasmine.createSpy('child') - parent._observer.on('set', parentSpy) - child._observer.on('set', childSpy) + parent.$observer.on('set', parentSpy) + child.$observer.on('set', childSpy) child.a = 3 // make sure data sync is working @@ -240,8 +240,8 @@ describe('Scope', function () { var vmSpy = jasmine.createSpy('vm') var vmAddSpy = jasmine.createSpy('vmAdd') var oldDataSpy = jasmine.createSpy('oldData') - vm._observer.on('set', vmSpy) - vm._observer.on('add', vmAddSpy) + vm.$observer.on('set', vmSpy) + vm.$observer.on('add', vmAddSpy) oldData.$observer.on('set', oldDataSpy) vm.$data = newData @@ -274,7 +274,7 @@ describe('Scope', function () { parent: parent }) var spy = jasmine.createSpy('teardown') - child._observer.on('set', spy) + child.$observer.on('set', spy) it('should stop relaying parent events', function () { child._teardownScope() From 4909889bd33da7428743f616b4f1c8c77fe517aa Mon Sep 17 00:00:00 2001 From: Evan You Date: Sat, 16 Aug 2014 01:02:27 -0400 Subject: [PATCH 0140/1534] paramAttributes should be bound as v-with --- src/directive.js | 5 ++-- src/directives/attr.js | 61 +++++++++++++++++++---------------------- src/directives/html.js | 56 +++++++++++++++++++------------------ src/directives/text.js | 18 +++++++----- src/instance/compile.js | 35 ++++++++++++++++++----- 5 files changed, 100 insertions(+), 75 deletions(-) diff --git a/src/directive.js b/src/directive.js index 5e9da96a9b8..5bd1f3ef117 100644 --- a/src/directive.js +++ b/src/directive.js @@ -90,11 +90,12 @@ p._bind = function () { * Check locked or not before calling definition update. * * @param {*} value + * @param {*} oldValue */ -p._update = function (value) { +p._update = function (value, oldValue) { if (!this._locked) { - this.update(value) + this.update(value, oldValue) } } diff --git a/src/directives/attr.js b/src/directives/attr.js index 22bde603411..5ead1ae7495 100644 --- a/src/directives/attr.js +++ b/src/directives/attr.js @@ -6,42 +6,37 @@ var namespaces = { ev: 'http://www.w3.org/2001/xml-events' } -exports.bind = function () { - var name = this.arg - // check param attributes - var params = this.vm.$options.paramAttributes - this.isParam = - this.el.__vue__ && // only check rootNode for params - params && - params.indexOf(name) > -1 - // check namespaced attributes - if (this.el.namespaceURI.slice(-3) === 'svg') { - var colonIndex = name.indexOf(':') - if (colonIndex > 0) { - this.localName = name.slice(colonIndex + 1) - this.namespace = namespaces[name.slice(0, colonIndex)] - } - } -} +module.exports = { -exports.update = function (value) { - var el = this.el - var ns = this.namespace - var name = this.arg - if (value || value === 0) { - if (ns) { - el.setAttributeNS(ns, name, value) - } else { - el.setAttribute(name, value) + bind: function () { + // check namespaced attributes + if (this.el.namespaceURI.slice(-3) === 'svg') { + var name = this.arg + var colonIndex = name.indexOf(':') + if (colonIndex > 0) { + this.localName = name.slice(colonIndex + 1) + this.namespace = namespaces[name.slice(0, colonIndex)] + } } - } else { - if (ns) { - el.removeAttributeNS(ns, this.localName) + }, + + update: function (value) { + var el = this.el + var ns = this.namespace + var name = this.arg + if (value || value === 0) { + if (ns) { + el.setAttributeNS(ns, name, value) + } else { + el.setAttribute(name, value) + } } else { - el.removeAttribute(name) + if (ns) { + el.removeAttributeNS(ns, this.localName) + } else { + el.removeAttribute(name) + } } } - if (this.isParam) { - this.vm[name] = _.guardNumber(value) - } + } \ No newline at end of file diff --git a/src/directives/html.js b/src/directives/html.js index d9ef7b72c6c..46065ca5362 100644 --- a/src/directives/html.js +++ b/src/directives/html.js @@ -1,33 +1,37 @@ var _ = require('../util') var templateParser = require('../parse/template') -exports.bind = function () { - // a comment node means this is a binding for - // {{{ inline unescaped html }}} - if (this.el.nodeType === 8) { - // hold nodes - this.nodes = [] - } -} +module.exports = { -exports.update = function (value) { - value = _.guard(value) - if (this.nodes) { - this.swap(value) - } else { - this.el.innerHTML = value - } -} + bind: function () { + // a comment node means this is a binding for + // {{{ inline unescaped html }}} + if (this.el.nodeType === 8) { + // hold nodes + this.nodes = [] + } + }, -exports.swap = function (value) { - // remove old nodes - var i = this.nodes.length - while (i--) { - _.remove(this.nodes[i]) + update: function (value) { + value = _.guard(value) + if (this.nodes) { + this.swap(value) + } else { + this.el.innerHTML = value + } + }, + + swap: function (value) { + // remove old nodes + var i = this.nodes.length + while (i--) { + _.remove(this.nodes[i]) + } + // convert new value to a fragment + var frag = templateParser.parse(value, true) + // save a reference to these nodes so we can remove later + this.nodes = _.toArray(frag.childNodes) + _.before(frag, this.el) } - // convert new value to a fragment - var frag = templateParser.parse(value, true) - // save a reference to these nodes so we can remove later - this.nodes = _.toArray(frag.childNodes) - _.before(frag, this.el) + } \ No newline at end of file diff --git a/src/directives/text.js b/src/directives/text.js index a60bd5be7fb..160e11a06fc 100644 --- a/src/directives/text.js +++ b/src/directives/text.js @@ -1,11 +1,15 @@ var _ = require('../util') -exports.bind = function () { - this.attr = this.el.nodeType === 3 - ? 'nodeValue' - : 'textContent' -} +module.exports = { -exports.update = function (value) { - this.el[this.attr] = _.guard(value) + bind: function () { + this.attr = this.el.nodeType === 3 + ? 'nodeValue' + : 'textContent' + }, + + update: function (value) { + this.el[this.attr] = _.guard(value) + } + } \ No newline at end of file diff --git a/src/instance/compile.js b/src/instance/compile.js index a0a61f1e7d5..e60e4a84d9c 100644 --- a/src/instance/compile.js +++ b/src/instance/compile.js @@ -180,29 +180,50 @@ exports._compileComment = function (node) { } /** - * Check an attribute for potential bindings + * Check an attribute for potential bindings. */ exports._bindAttr = function (node, attr) { - var tokens = textParser.parse(attr.value) + var name = attr.name + var value = attr.value + // check if this is a param attribute. + var params = this.$options.paramAttributes + var isParam = + node === this.$el && // only check on root node + params && + params.indexOf(name) > -1 + if (isParam) { + node.removeAttribute(name) + } + // parse attribute value + var tokens = textParser.parse(value) if (!tokens) { + if (isParam) { + this.$set(name, value) + } return } + // only 1 binding tag allowed if (tokens.length > 1) { _.warn( - 'Invalid attribute binding: "' + attr.value + '"' + + 'Invalid attribute binding: "' + value + '"' + '\nUse one single interpolation tag in ' + 'attribute bindings.' ) return } + // param attributes are bound as v-with + var dirName = isParam + ? 'with' + : 'attr' // wrap namespaced attribute so it won't mess up // the directive parser - var arg = attr.name.indexOf(':') > 0 - ? "'" + attr.name + "'" - : attr.name + var arg = name.indexOf(':') > 0 + ? "'" + name + "'" + : name + // bind this._bindDirective( - 'attr', + dirName, arg + ':' + tokens[0].value, node ) From d01c3578e8ab8e00a050296ac2d2d7a3088f7105 Mon Sep 17 00:00:00 2001 From: Evan You Date: Sat, 16 Aug 2014 01:03:31 -0400 Subject: [PATCH 0141/1534] remove unused --- src/directives/attr.js | 2 -- src/instance/compile.js | 13 ------------- 2 files changed, 15 deletions(-) diff --git a/src/directives/attr.js b/src/directives/attr.js index 5ead1ae7495..2082ffe3b69 100644 --- a/src/directives/attr.js +++ b/src/directives/attr.js @@ -1,5 +1,3 @@ -var _ = require('../util') - // SVG xml namespaces var namespaces = { xlink: 'http://www.w3.org/1999/xlink', diff --git a/src/instance/compile.js b/src/instance/compile.js index e60e4a84d9c..af921d94cee 100644 --- a/src/instance/compile.js +++ b/src/instance/compile.js @@ -229,19 +229,6 @@ exports._bindAttr = function (node, attr) { ) } -/** - * Helper to translate token value into expression parts. - * - * @param {Object} token - * @return {String} - */ - -function expifyToken (token) { - return token.tag - ? token.value - : ("'" + token.value + "'") -} - /** * Check for priority directives that would potentially * skip other directives: From e6ea813e06f980d349711d618704d055e9bbb532 Mon Sep 17 00:00:00 2001 From: Evan You Date: Sat, 16 Aug 2014 04:30:48 -0400 Subject: [PATCH 0142/1534] block instance + dom methods --- src/api/dom.js | 122 ++++++++++++++++++++++++++++++++--- src/api/global.js | 2 +- src/instance/compile.js | 5 +- src/instance/element.js | 33 +++++++--- src/instance/init.js | 5 +- src/transition/index.js | 23 +++++++ src/transition/transition.js | 0 7 files changed, 167 insertions(+), 23 deletions(-) create mode 100644 src/transition/index.js delete mode 100644 src/transition/transition.js diff --git a/src/api/dom.js b/src/api/dom.js index 74d243b2c0d..faadc12d34a 100644 --- a/src/api/dom.js +++ b/src/api/dom.js @@ -1,19 +1,121 @@ -exports.$appendTo = function () { - +var transition = require('../transition') + +/** + * Append instance to target + * + * @param {Node} target + * @param {Function} [cb] + */ + +exports.$appendTo = function (target, cb) { + target = query(target) + if (this._isBlock) { + blockOp(this, target, transition.append, cb) + } else { + transition.append(this.$el, target, cb) + } +} + +/** + * Prepend instance to target + * + * @param {Node} target + * @param {Function} [cb] + */ + +exports.$prependTo = function (target, cb) { + target = query(target) + if (target.hasChildNodes()) { + this.$before(target.firstChild, cb) + } else { + this.$appendTo(target, cb) + } +} + +/** + * Insert instance before target + * + * @param {Node} target + * @param {Function} [cb] + */ + +exports.$before = function (target, cb) { + target = query(target) + if (this._isBlock) { + blockOp(this, target, transition.before, cb) + } else { + transition.before(this.$el, target, cb) + } } -exports.$prependTo = function () { - +/** + * Insert instance after target + * + * @param {Node} target + * @param {Function} [cb] + */ + +exports.$after = function (target, cb) { + target = query(target) + if (target.nextSibling) { + this.$before(target.nextSibling) + } else { + this.$appendTo(target.parentNode) + } } -exports.$before = function () { - +/** + * Remove instance from DOM + * + * @param {Function} [cb] + */ + +exports.$remove = function (cb) { + if ( + this._isBlock && + !this._blockFragment.hasChildNodes() + ) { + blockOp( + this, + this._blockFragment, + transition.removeThenAppend, + cb + ) + } else if (this.$el.parentNode) { + transition.remove(this.$el, cb) + } } -exports.$after = function () { - +/** + * Execute a transition operation on a block instance, + * iterating through all its block nodes. + * + * @param {Vue} vm + * @param {Node} target + * @param {Function} op + * @param {Function} cb + */ + +function blockOp (vm, target, op, cb) { + var current = vm._blockStart + var end = vm._blockEnd + var next + while (next !== end) { + next = current.nextSibling + op(current, target) + current = next + } + op(end, target, cb) } -exports.$remove = function () { - +/** + * Check for selectors + * + * @param {String|Element} el + */ + +function query (el) { + return typeof el === 'string' + ? document.querySelector(el) + : el } \ No newline at end of file diff --git a/src/api/global.js b/src/api/global.js index 1d05e706802..55bea168893 100644 --- a/src/api/global.js +++ b/src/api/global.js @@ -14,7 +14,7 @@ var assetTypes = [ exports.util = _ exports.nextTick = _.nextTick exports.config = require('../config') -exports.transition = require('../transition/transition') +exports.transition = require('../transition') /** * Class inehritance diff --git a/src/instance/compile.js b/src/instance/compile.js index af921d94cee..b3df67ddd68 100644 --- a/src/instance/compile.js +++ b/src/instance/compile.js @@ -13,8 +13,9 @@ var templateParser = require('../parse/template') */ exports._compile = function () { - if (this._blockNodes) { - this._blockNodes.forEach(this._compileNode, this) + if (this._isBlock) { + _.toArray(this._blockFragment.childNodes) + .forEach(this._compileNode, this) } else { this._compileNode(this.$el) } diff --git a/src/instance/element.js b/src/instance/element.js index 58e73c33d35..870f2ddbcc5 100644 --- a/src/instance/element.js +++ b/src/instance/element.js @@ -18,19 +18,34 @@ exports._initElement = function (el) { _.warn('Cannot find element: ' + selector) } } - // If the passed in `el` is a DocumentFragment, the - // instance is considered a "block instance" which manages - // not a single element, but multiple elements. A block - // instance's `$el` is an Array of the elements it manages. if (el instanceof DocumentFragment) { - this._blockNodes = _.toArray(el.childNodes) - this.$el = document.createComment('vue-block') + this._initBlock(el) } else { this.$el = el + this._initTemplate() + this._initContent() } this.$el.__vue__ = this - this._initTemplate() - this._initContent() +} + +/** + * Initialize a block instance that manages a group of + * nodes instead of one element. The group is denoted by + * a starting node and an ending node. + * + * @param {DocumentFragment} frag + */ + +exports._initBlock = function (frag) { + this._isBlock = true + this.$el = + this._blockStart = + document.createComment('v-block-start') + this._blockEnd = + document.createComment('v-block-end') + _.prepend(this._blockStart, frag) + frag.appendChild(this._blockEnd) + this._blockFragment = frag } /** @@ -147,7 +162,7 @@ var concat = [].concat function getOutlets (el) { return _.isArray(el) ? concat.apply([], el.map(getOutlets)) - : _.toArray(el.getElementsByTagName('content')) + : _.toArray(el.querySelectorAll('content')) } /** diff --git a/src/instance/init.js b/src/instance/init.js index 827cd0f3e95..0386818adc2 100644 --- a/src/instance/init.js +++ b/src/instance/init.js @@ -18,7 +18,6 @@ exports._init = function (options) { this.$el = null this._data = options.data || {} - this._blockNodes = null this._isDestroyed = false this._rawContent = null this._emitter = new Emitter(this) @@ -26,6 +25,10 @@ exports._init = function (options) { this._activeWatcher = null this._directives = [] + this._isBlock = false + this._blockStart = null + this._blockEnd = null + // setup parent relationship this.$parent = options.parent this._children = [] diff --git a/src/transition/index.js b/src/transition/index.js new file mode 100644 index 00000000000..72b9eeede83 --- /dev/null +++ b/src/transition/index.js @@ -0,0 +1,23 @@ +var _ = require('../util') + +// TODO +// placeholder for testing + +exports.append = function (el, target, cb) { + target.appendChild(el) +} + +exports.before = function (el, target, cb) { + _.before(el, target) +} + +exports.remove = function (el, cb) { + _.remove(el) +} + +exports.removeThenAppend = function (el, target, cb) { + setTimeout(function () { + target.appendChild(el) + cb && cb() + }, 500) +} \ No newline at end of file diff --git a/src/transition/transition.js b/src/transition/transition.js deleted file mode 100644 index e69de29bb2d..00000000000 From b1654d5e024b523650c60df8f4c8e89f59d47b63 Mon Sep 17 00:00:00 2001 From: Evan You Date: Sat, 16 Aug 2014 05:41:18 -0400 Subject: [PATCH 0143/1534] a few more directives --- changes.md | 2 +- src/api/data.js | 12 ++++---- src/api/dom.js | 10 +++---- src/directive.js | 10 +++++-- src/directives/class.js | 57 +++++++++++++++++++++++++++++++++++++ src/directives/cloak.js | 14 +++++++++ src/directives/component.js | 31 ++++++++++++++++++++ src/directives/if.js | 0 src/directives/index.js | 37 +++++++++++++----------- src/directives/model.js | 0 src/directives/on.js | 0 src/directives/partial.js | 0 src/directives/ref.js | 19 +++++++++++++ src/directives/repeat.js | 0 src/directives/show.js | 8 ++++++ src/directives/style.js | 0 src/directives/view.js | 0 src/directives/with.js | 0 src/instance/compile.js | 1 + src/instance/events.js | 1 + src/instance/init.js | 5 +++- src/transition/index.js | 12 +++++--- 22 files changed, 184 insertions(+), 35 deletions(-) create mode 100644 src/directives/class.js create mode 100644 src/directives/cloak.js create mode 100644 src/directives/component.js create mode 100644 src/directives/if.js create mode 100644 src/directives/model.js create mode 100644 src/directives/on.js create mode 100644 src/directives/partial.js create mode 100644 src/directives/ref.js create mode 100644 src/directives/repeat.js create mode 100644 src/directives/show.js create mode 100644 src/directives/style.js create mode 100644 src/directives/view.js create mode 100644 src/directives/with.js diff --git a/changes.md b/changes.md index 927ec1ea405..9d2a4932e01 100644 --- a/changes.md +++ b/changes.md @@ -70,7 +70,7 @@ computed: { ### New options - `literal`: replaces old options `isLiteral` and `isEmpty`. -- `twoway`: indicates the directive is two-way and may write back to the model. Allows the use of `this.set(value)` inside directive functions. +- `twoWay`: indicates the directive is two-way and may write back to the model. Allows the use of `this.set(value)` inside directive functions. - `paramAttributes`: an Array of attribute names to extract as parameters for the directive. For example, given the option value `['my-param']` and markup ``, you can access `this.params['my-param']` with value `'123'` inside directive functions. ### Removed options: `isLiteral`, `isEmpty`, `isFn` diff --git a/src/api/data.js b/src/api/data.js index 91308241120..a13e4be2453 100644 --- a/src/api/data.js +++ b/src/api/data.js @@ -125,11 +125,13 @@ exports.$interpolate = function (text) { var tokens = textParser.parse(text) var vm = this if (tokens) { - return tokens.map(function (token) { - return token.tag - ? vm.$eval(token.value) - : token.value - }).join('') + return tokens.length === 1 + ? vm.$eval(tokens[0].value) + : tokens.map(function (token) { + return token.tag + ? vm.$eval(token.value) + : token.value + }).join('') } else { return text } diff --git a/src/api/dom.js b/src/api/dom.js index faadc12d34a..5d643078686 100644 --- a/src/api/dom.js +++ b/src/api/dom.js @@ -12,7 +12,7 @@ exports.$appendTo = function (target, cb) { if (this._isBlock) { blockOp(this, target, transition.append, cb) } else { - transition.append(this.$el, target, cb) + transition.append(this.$el, target, cb, this) } } @@ -44,7 +44,7 @@ exports.$before = function (target, cb) { if (this._isBlock) { blockOp(this, target, transition.before, cb) } else { - transition.before(this.$el, target, cb) + transition.before(this.$el, target, cb, this) } } @@ -82,7 +82,7 @@ exports.$remove = function (cb) { cb ) } else if (this.$el.parentNode) { - transition.remove(this.$el, cb) + transition.remove(this.$el, cb, this) } } @@ -102,10 +102,10 @@ function blockOp (vm, target, op, cb) { var next while (next !== end) { next = current.nextSibling - op(current, target) + op(current, target, null, vm) current = next } - op(end, target, cb) + op(end, target, cb, vm) } /** diff --git a/src/directive.js b/src/directive.js index 5bd1f3ef117..0772a66b483 100644 --- a/src/directive.js +++ b/src/directive.js @@ -61,14 +61,14 @@ p._initDef = function () { */ p._bind = function () { - if (this.expression && !this.isLiteral && this.update) { + if (this.expression && !this.literal && this.update) { this._watcher = new Watcher( this.vm, this.expression, this._update, // callback this, // callback context this.filters, - this.twoway // need setter + this.twoWay // need setter ) var value = this._watcher.value if (this.bind) { @@ -78,6 +78,10 @@ p._bind = function () { this.update(value) } } else { + if (this.literal) { + this.expression = + this.vm.$interpolate(this.expression) + } if (this.bind) { this.bind() } @@ -124,7 +128,7 @@ p._teardown = function () { */ p.set = function (value, lock) { - if (this.twoway) { + if (this.twoWay) { if (lock) { this._locked = true } diff --git a/src/directives/class.js b/src/directives/class.js new file mode 100644 index 00000000000..fc65d3276a6 --- /dev/null +++ b/src/directives/class.js @@ -0,0 +1,57 @@ +var _ = require('../util') +var hasClassList = + _.inBrowser && + 'classList' in document.documentElement + +/** + * add class for IE9 + * + * @param {Element} el + * @param {Strong} cls + */ + +function addClass (el, cls) { + if (hasClassList) { + el.classList.add(cls) + } else { + var cur = ' ' + el.className + ' ' + if (cur.indexOf(' ' + cls + ' ') < 0) { + el.className = (cur + cls).trim() + } + } +} + +/** + * remove class for IE9 + * + * @param {Element} el + * @param {Strong} cls + */ + +function removeClass (el, cls) { + if (hasClassList) { + el.classList.remove(cls) + } else { + var cur = ' ' + el.className + ' ' + var tar = ' ' + cls + ' ' + while (cur.indexOf(tar) >= 0) { + cur = cur.replace(tar, ' ') + } + el.className = cur.trim() + } +} + +module.exports = function (value) { + if (this.arg) { + var method = value ? addClass : removeClass + method(this.el, this.arg) + } else { + if (this.lastVal) { + removeClass(this.el, this.lastVal) + } + if (value) { + addClass(this.el, value) + this.lastVal = value + } + } +} \ No newline at end of file diff --git a/src/directives/cloak.js b/src/directives/cloak.js new file mode 100644 index 00000000000..6b7aa220e62 --- /dev/null +++ b/src/directives/cloak.js @@ -0,0 +1,14 @@ +var config = require('../config') + +module.exports = { + + literal: true, + + bind: function () { + var el = this.el + this.vm.$once('hook:ready', function () { + el.removeAttribute(config.prefix + 'cloak') + }) + } + +} \ No newline at end of file diff --git a/src/directives/component.js b/src/directives/component.js new file mode 100644 index 00000000000..13d2a8dec38 --- /dev/null +++ b/src/directives/component.js @@ -0,0 +1,31 @@ +var _ = require('../util') + +module.exports = { + + literal: true, + + bind: function () { + if (!this.el.__vue__) { + var registry = this.vm.$options.components + var Ctor = registry[this.expression] + if (Ctor) { + this.childVM = new Ctor({ + el: this.el, + parent: this.vm + }) + } else { + _.warn( + 'Failed to resolve component: ' + + this.expression + ) + } + } + }, + + unbind: function () { + if (this.childVM) { + this.childVM.$destroy() + } + } + +} \ No newline at end of file diff --git a/src/directives/if.js b/src/directives/if.js new file mode 100644 index 00000000000..e69de29bb2d diff --git a/src/directives/index.js b/src/directives/index.js index 9692dda30c3..58e1a1ef552 100644 --- a/src/directives/index.js +++ b/src/directives/index.js @@ -1,18 +1,23 @@ var directives = module.exports = Object.create(null) -directives.text = require('./text') -directives.html = require('./html') -directives.attr = require('./attr') -// directives.show = require('./show') -// directives['class'] = require('./class') -// directives.ref = require('./ref') -// directives.cloak = require('./cloak') -// directives.on = require('./on') -// directives.repeat = require('./repeat') -// directives.model = require('./model') -// directives['if'] = require('./if') -// directives['with'] = require('./with') -// directives.html = require('./html') -// directives.style = require('./style') -// directives.partial = require('./partial') -// directives.view = require('./view') \ No newline at end of file +// manipulation directives +directives.text = require('./text') +directives.html = require('./html') +directives.attr = require('./attr') +directives.show = require('./show') +directives['class'] = require('./class') +directives.ref = require('./ref') +directives.cloak = require('./cloak') +directives.style = require('./style') +directives.partial = require('./partial') + +// event listener directives +directives.on = require('./on') +directives.model = require('./model') + +// child vm directives +directives.view = require('./view') +directives.component = require('./component') +directives.repeat = require('./repeat') +directives['if'] = require('./if') +directives['with'] = require('./with') \ No newline at end of file diff --git a/src/directives/model.js b/src/directives/model.js new file mode 100644 index 00000000000..e69de29bb2d diff --git a/src/directives/on.js b/src/directives/on.js new file mode 100644 index 00000000000..e69de29bb2d diff --git a/src/directives/partial.js b/src/directives/partial.js new file mode 100644 index 00000000000..e69de29bb2d diff --git a/src/directives/ref.js b/src/directives/ref.js new file mode 100644 index 00000000000..8b557199593 --- /dev/null +++ b/src/directives/ref.js @@ -0,0 +1,19 @@ +module.exports = { + + literal: true, + + bind: function () { + var id = this.expression + if (id) { + this.vm.$parent.$[id] = this.vm + } + }, + + unbind: function () { + var id = this.expression + if (id) { + delete this.vm.$parent.$[id] + } + } + +} \ No newline at end of file diff --git a/src/directives/repeat.js b/src/directives/repeat.js new file mode 100644 index 00000000000..e69de29bb2d diff --git a/src/directives/show.js b/src/directives/show.js new file mode 100644 index 00000000000..aa522811ac2 --- /dev/null +++ b/src/directives/show.js @@ -0,0 +1,8 @@ +var transition = require('../transition') + +module.exports = function (value) { + var el = this.el + transition.apply(el, value ? 1 : -1, function () { + el.style.display = value ? '' : 'none' + }, this.vm) +} \ No newline at end of file diff --git a/src/directives/style.js b/src/directives/style.js new file mode 100644 index 00000000000..e69de29bb2d diff --git a/src/directives/view.js b/src/directives/view.js new file mode 100644 index 00000000000..e69de29bb2d diff --git a/src/directives/with.js b/src/directives/with.js new file mode 100644 index 00000000000..e69de29bb2d diff --git a/src/instance/compile.js b/src/instance/compile.js index b3df67ddd68..e631beb911a 100644 --- a/src/instance/compile.js +++ b/src/instance/compile.js @@ -19,6 +19,7 @@ exports._compile = function () { } else { this._compileNode(this.$el) } + this._isCompiled = true } /** diff --git a/src/instance/events.js b/src/instance/events.js index 8ddb7f92d6b..7094e07d203 100644 --- a/src/instance/events.js +++ b/src/instance/events.js @@ -35,4 +35,5 @@ exports._callHook = function (hook) { handlers[i].call(this) } } + this.$emit('hook:' + hook) } \ No newline at end of file diff --git a/src/instance/init.js b/src/instance/init.js index 0386818adc2..05919b02db9 100644 --- a/src/instance/init.js +++ b/src/instance/init.js @@ -17,8 +17,8 @@ exports._init = function (options) { options = options || {} this.$el = null + this.$ = {} this._data = options.data || {} - this._isDestroyed = false this._rawContent = null this._emitter = new Emitter(this) this._watchers = {} @@ -29,6 +29,9 @@ exports._init = function (options) { this._blockStart = null this._blockEnd = null + this._isCompiled = false + this._isDestroyed = false + // setup parent relationship this.$parent = options.parent this._children = [] diff --git a/src/transition/index.js b/src/transition/index.js index 72b9eeede83..bfdde19f4b2 100644 --- a/src/transition/index.js +++ b/src/transition/index.js @@ -3,21 +3,25 @@ var _ = require('../util') // TODO // placeholder for testing -exports.append = function (el, target, cb) { +exports.append = function (el, target, cb, vm) { target.appendChild(el) } -exports.before = function (el, target, cb) { +exports.before = function (el, target, cb, vm) { _.before(el, target) } -exports.remove = function (el, cb) { +exports.remove = function (el, cb, vm) { _.remove(el) } -exports.removeThenAppend = function (el, target, cb) { +exports.removeThenAppend = function (el, target, cb, vm) { setTimeout(function () { target.appendChild(el) cb && cb() }, 500) +} + +exports.apply = function (el, direction, cb, vm) { + } \ No newline at end of file From cb1d06d39b480ceb26fb13db68cf90633c7a4df4 Mon Sep 17 00:00:00 2001 From: Evan You Date: Sat, 16 Aug 2014 14:46:12 -0400 Subject: [PATCH 0144/1534] dynamic literals --- src/directive.js | 48 ++++++++++++++++++++++++++--------------- src/instance/compile.js | 7 +++--- 2 files changed, 35 insertions(+), 20 deletions(-) diff --git a/src/directive.js b/src/directive.js index 0772a66b483..ee9b3a2344c 100644 --- a/src/directive.js +++ b/src/directive.js @@ -1,5 +1,6 @@ var _ = require('./util') var Watcher = require('./watcher') +var textParser = require('./parse/text') /** * A directive links a DOM element with a piece of data, @@ -61,30 +62,43 @@ p._initDef = function () { */ p._bind = function () { - if (this.expression && !this.literal && this.update) { + // check if this is a dynamic literal binding + // e.g. v-component="{{currentView}}" + var expression = this.expression + var isDynamicLiteral = false + if (this.literal) { + var tokens = textParser.parse(expression) + if (tokens) { + if (tokens.length > 1) { + _.warn( + 'Invalid literal directive: ' + + this.name + '="' + expression + '"' + + '\nDon\'t mix binding tags with plain text ' + + 'in literal directives.' + ) + } else { + isDynamicLiteral = true + expression = tokens[0].value + this.expression = this.vm.$eval(expression) + } + } + } + if (this.bind) { + this.bind() + } + if ( + expression && this.update && + (!this.literal || isDynamicLiteral) + ) { this._watcher = new Watcher( this.vm, - this.expression, + expression, this._update, // callback this, // callback context this.filters, this.twoWay // need setter ) - var value = this._watcher.value - if (this.bind) { - this.bind() - } - if (this.update) { - this.update(value) - } - } else { - if (this.literal) { - this.expression = - this.vm.$interpolate(this.expression) - } - if (this.bind) { - this.bind() - } + this.update(this._watcher.value) } this._bound = true } diff --git a/src/instance/compile.js b/src/instance/compile.js index e631beb911a..8a7a12de213 100644 --- a/src/instance/compile.js +++ b/src/instance/compile.js @@ -208,9 +208,10 @@ exports._bindAttr = function (node, attr) { // only 1 binding tag allowed if (tokens.length > 1) { _.warn( - 'Invalid attribute binding: "' + value + '"' + - '\nUse one single interpolation tag in ' + - 'attribute bindings.' + 'Invalid attribute binding: "' + + name + '="' + value + '"' + + '\nDon\'t mix binding tags with plain text ' + + 'in attribute bindings.' ) return } From 86f4f6215d6fe9c18849f8e3cac3d19122e4cf9f Mon Sep 17 00:00:00 2001 From: Evan You Date: Sat, 16 Aug 2014 16:50:36 -0400 Subject: [PATCH 0145/1534] anonymous istance --- src/directives/ref.js | 11 ++++- src/instance/compile.js | 97 ++++++++++++++++++----------------------- src/instance/init.js | 6 +++ src/parse/template.js | 4 +- 4 files changed, 60 insertions(+), 58 deletions(-) diff --git a/src/directives/ref.js b/src/directives/ref.js index 8b557199593..86b8d23be83 100644 --- a/src/directives/ref.js +++ b/src/directives/ref.js @@ -5,14 +5,21 @@ module.exports = { bind: function () { var id = this.expression if (id) { - this.vm.$parent.$[id] = this.vm + var owner = this.vm.$parent + // find the first parent vm that is not an + // anonymous instance. + while (owner._isAnonymous) { + owner = owner.$parent + } + owner.$[id] = this.vm + this.owner = owner } }, unbind: function () { var id = this.expression if (id) { - delete this.vm.$parent.$[id] + delete this.owner.$[id] } } diff --git a/src/instance/compile.js b/src/instance/compile.js index 8a7a12de213..90fefaa6ca9 100644 --- a/src/instance/compile.js +++ b/src/instance/compile.js @@ -41,9 +41,6 @@ exports._compileNode = function (node) { this._compileTextNode(node) } break - case 8: // comment - this._compileComment(node) - break } } @@ -172,13 +169,51 @@ exports._compileTextNode = function (node) { } /** - * Compile a comment node (check for block flow-controls) + * Check for priority directives that would potentially + * skip other directives: + * + * - v-pre + * - v-repeat + * - v-if + * - v-component + * + * @param {Element} node + * @return {Boolean} + */ + +exports._checkPriorityDirectives = function (node) { + var value + /* jshint boss: true */ + if (_.attr(node, 'pre') !== null) { + return true + } else if (value = _.attr(node, 'repeat')) { + this._bindDirective('repeat', value) + return true + } else if (value = _.attr(node, 'if')) { + this._bindDirective('if', value) + return true + } else if (value = _.attr(node, 'component')) { + this._bindDirective('component', value) + return true + } +} + +/** + * Bind a directive. * - * @param {CommentNode} node + * @param {String} name + * @param {String} value + * @param {Element} node */ -exports._compileComment = function (node) { - +exports._bindDirective = function (name, value, node) { + var descriptors = dirParser.parse(value) + var dirs = this._directives + for (var i = 0, l = descriptors.length; i < l; i++) { + dirs.push( + new Direcitve(name, node, this, descriptors[i]) + ) + } } /** @@ -230,52 +265,4 @@ exports._bindAttr = function (node, attr) { arg + ':' + tokens[0].value, node ) -} - -/** - * Check for priority directives that would potentially - * skip other directives: - * - * - v-pre - * - v-repeat - * - v-if - * - v-component - * - * @param {Element} node - * @return {Boolean} - */ - -exports._checkPriorityDirectives = function (node) { - var value - /* jshint boss: true */ - if (_.attr(node, 'pre') !== null) { - return true - } else if (value = _.attr(node, 'repeat')) { - this._bindDirective('repeat', value) - return true - } else if (value = _.attr(node, 'if')) { - this._bindDirective('if', value) - return true - } else if (value = _.attr(node, 'component')) { - this._bindDirective('component', value) - return true - } -} - -/** - * Bind a directive. - * - * @param {String} name - * @param {String} value - * @param {Element} node - */ - -exports._bindDirective = function (name, value, node) { - var descriptors = dirParser.parse(value) - var dirs = this._directives - for (var i = 0, l = descriptors.length; i < l; i++) { - dirs.push( - new Direcitve(name, node, this, descriptors[i]) - ) - } } \ No newline at end of file diff --git a/src/instance/init.js b/src/instance/init.js index 05919b02db9..b6a60b9de5c 100644 --- a/src/instance/init.js +++ b/src/instance/init.js @@ -25,13 +25,19 @@ exports._init = function (options) { this._activeWatcher = null this._directives = [] + // block instance properties this._isBlock = false this._blockStart = null this._blockEnd = null + // lifecycle state this._isCompiled = false this._isDestroyed = false + // anonymous instances are created by flow-control + // directives such as v-if and v-repeat + this._isAnonymous = options.anonymous + // setup parent relationship this.$parent = options.parent this._children = [] diff --git a/src/parse/template.js b/src/parse/template.js index 7275e8066e9..912511442d3 100644 --- a/src/parse/template.js +++ b/src/parse/template.js @@ -145,7 +145,9 @@ exports.parse = function (template, clone) { // if the template is already a document fragment, // do nothing if (template instanceof DocumentFragment) { - return template + return clone + ? template.cloneNode(true) + : template } if (typeof template === 'string') { From 8b72c26a93c4ac8c5e5027eabc1dba232ca85657 Mon Sep 17 00:00:00 2001 From: Evan You Date: Sat, 16 Aug 2014 18:06:37 -0400 Subject: [PATCH 0146/1534] handle callable expressions for v-on --- changes.md | 5 +-- src/directive.js | 84 +++++++++++++++++++++++++++---------- src/directives/cloak.js | 2 - src/directives/component.js | 2 +- src/directives/if.js | 53 +++++++++++++++++++++++ src/directives/on.js | 62 +++++++++++++++++++++++++++ src/directives/ref.js | 2 +- src/instance/compile.js | 38 ++++++++--------- src/parse/expression.js | 5 ++- src/vue.js | 5 ++- 10 files changed, 207 insertions(+), 51 deletions(-) diff --git a/changes.md b/changes.md index 9d2a4932e01..8024f453ad6 100644 --- a/changes.md +++ b/changes.md @@ -69,13 +69,10 @@ computed: { ### New options -- `literal`: replaces old options `isLiteral` and `isEmpty`. - `twoWay`: indicates the directive is two-way and may write back to the model. Allows the use of `this.set(value)` inside directive functions. - `paramAttributes`: an Array of attribute names to extract as parameters for the directive. For example, given the option value `['my-param']` and markup ``, you can access `this.params['my-param']` with value `'123'` inside directive functions. -### Removed options: `isLiteral`, `isEmpty`, `isFn` - -- `isFn` is no longer necessary for directives expecting function values. +### Removed option: `isEmpty` ## Interpolation diff --git a/src/directive.js b/src/directive.js index ee9b3a2344c..59122da007b 100644 --- a/src/directive.js +++ b/src/directive.js @@ -1,6 +1,7 @@ var _ = require('./util') var Watcher = require('./watcher') var textParser = require('./parse/text') +var expParser = require('./parse/expression') /** * A directive links a DOM element with a piece of data, @@ -62,11 +63,44 @@ p._initDef = function () { */ p._bind = function () { - // check if this is a dynamic literal binding - // e.g. v-component="{{currentView}}" + this.watcherExp = this.expression + var isDynamicLiteral = this._checkDynamicLiteral() + if (this.bind) { + this.bind() + } + if ( + this.expression && this.update && + (!this.isLiteral || isDynamicLiteral) + ) { + if (!this._checkExpFn()) + // check if this is a function directive with an + // inline expression + { + this._watcher = new Watcher( + this.vm, + this.watcherExp, + this._update, // callback + this, // callback context + this.filters, + this.twoWay // need setter + ) + this.update(this._watcher.value) + } + } + this._bound = true +} + +/** + * check if this is a dynamic literal binding. + * + * e.g. v-component="{{currentView}}" + * + * @return {Boolean} + */ + +p._checkDynamicLiteral = function () { var expression = this.expression - var isDynamicLiteral = false - if (this.literal) { + if (expression && this.isLiteral) { var tokens = textParser.parse(expression) if (tokens) { if (tokens.length > 1) { @@ -77,30 +111,38 @@ p._bind = function () { 'in literal directives.' ) } else { - isDynamicLiteral = true - expression = tokens[0].value + this.watcherExp = tokens[0].value this.expression = this.vm.$eval(expression) + return true } } } - if (this.bind) { - this.bind() - } +} + +/** + * Check if the directive is a function caller + * and if the expression is a callable one. If both true, + * we wrap up the expression and use it as the event + * handler. + * + * e.g. v-on="click: a++" + * + * @return {Boolean} + */ + +p._checkExpFn = function () { + var expression = this.expression if ( - expression && this.update && - (!this.literal || isDynamicLiteral) + expression && this.isFn && + !expParser.pathTestRE.test(expression) ) { - this._watcher = new Watcher( - this.vm, - expression, - this._update, // callback - this, // callback context - this.filters, - this.twoWay // need setter - ) - this.update(this._watcher.value) + var fn = expParser.parse(expression).get + var scope = this.vm.$scope + this.update(function () { + fn(scope) + }) + return true } - this._bound = true } /** diff --git a/src/directives/cloak.js b/src/directives/cloak.js index 6b7aa220e62..bae6be177ce 100644 --- a/src/directives/cloak.js +++ b/src/directives/cloak.js @@ -2,8 +2,6 @@ var config = require('../config') module.exports = { - literal: true, - bind: function () { var el = this.el this.vm.$once('hook:ready', function () { diff --git a/src/directives/component.js b/src/directives/component.js index 13d2a8dec38..395ad3bb193 100644 --- a/src/directives/component.js +++ b/src/directives/component.js @@ -2,7 +2,7 @@ var _ = require('../util') module.exports = { - literal: true, + isLiteral: true, bind: function () { if (!this.el.__vue__) { diff --git a/src/directives/if.js b/src/directives/if.js index e69de29bb2d..15d033e3e85 100644 --- a/src/directives/if.js +++ b/src/directives/if.js @@ -0,0 +1,53 @@ +var _ = require('../util') + +module.exports = { + + bind: function () { + // resolve component + var registry = this.vm.$options.components + var el = this.el + this.Ctor = + registry[el.tagName.toLowerCase()] || + registry[_.attr(el, 'component')] || + _.Vue + this.isAnonymous = this.Ctor === _.Vue + // insert ref + this.ref = document.createComment('v-if') + _.before(this.ref, el) + _.remove(el) + // warn conflicts + if (_.attr(el, 'view')) { + _.warn( + 'Conflict: v-if cannot be used together with ' + + 'v-view. Just set v-view\'s binding value to ' + + 'empty string to empty it.' + ) + } + if (_.attr(el, 'repeat')) { + _.warn( + 'Conflict: v-if cannot be used together with ' + + 'v-repeat. Use `v-show` or the `filterBy` filter ' + + 'instead.' + ) + } + }, + + update: function (value) { + if (!value) { + this.unbind() + } else if (!this.childVM) { + this.childVM = new this.Ctor({ + el: this.el.cloneNode(true), + parent: this.vm, + anonymous: this.isAnonymous + }) + this.childVM.$before(this.ref) + } + }, + + unbind: function () { + if (this.childVM) { + this.childVM.$destroy() + } + } +} \ No newline at end of file diff --git a/src/directives/on.js b/src/directives/on.js index e69de29bb2d..49aa02f0579 100644 --- a/src/directives/on.js +++ b/src/directives/on.js @@ -0,0 +1,62 @@ +var _ = require('../util') + +module.exports = { + + isFn: true, + + bind: function () { + // deal with iframes + if ( + this.el.tagName === 'IFRAME' && + this.arg !== 'load' + ) { + var self = this + this.iframeBind = function () { + self.el.contentWindow.addEventListener( + self.arg, + self.handler + ) + } + this.el.addEventListener('load', this.iframeBind) + } + }, + + update: function (handler) { + if (typeof handler !== 'function') { + _.warn( + 'Directive "v-on:' + this.expression + '" ' + + 'expects a function value.' + ) + return + } + this.reset() + var vm = this.vm + var rootScope = vm.$root.$scope + this.handler = function (e) { + e.targetVM = vm + rootScope.$event = e + var res = handler(e) + rootScope.$event = null + return res + } + if (this.iframeBind) { + this.iframeBind() + } else { + this.el.addEventListener(this.arg, this.handler) + } + }, + + reset: function () { + var el = this.iframeBind + ? this.el.contentWindow + : this.el + if (this.handler) { + el.removeEventListener(this.arg, this.handler) + } + }, + + unbind: function () { + this.reset() + this.el.removeEventListener('load', this.iframeBind) + } +} \ No newline at end of file diff --git a/src/directives/ref.js b/src/directives/ref.js index 86b8d23be83..7d3f85e511f 100644 --- a/src/directives/ref.js +++ b/src/directives/ref.js @@ -1,6 +1,6 @@ module.exports = { - literal: true, + isLiteral: true, bind: function () { var id = this.expression diff --git a/src/instance/compile.js b/src/instance/compile.js index 90fefaa6ca9..a74181b0286 100644 --- a/src/instance/compile.js +++ b/src/instance/compile.js @@ -61,7 +61,7 @@ exports._compileElement = function (node) { var hasAttributes = node.hasAttributes() // check priority directives if (hasAttributes) { - if (this._checkPriorityDirectives(node)) { + if (this._checkPriorityDirs(node)) { return } } @@ -170,31 +170,31 @@ exports._compileTextNode = function (node) { /** * Check for priority directives that would potentially - * skip other directives: - * - * - v-pre - * - v-repeat - * - v-if - * - v-component + * skip other directives. * * @param {Element} node * @return {Boolean} */ -exports._checkPriorityDirectives = function (node) { - var value - /* jshint boss: true */ +var priorityDirs = [ + 'repeat', + 'if', + 'view', + 'component' +] + +exports._checkPriorityDirs = function (node) { if (_.attr(node, 'pre') !== null) { return true - } else if (value = _.attr(node, 'repeat')) { - this._bindDirective('repeat', value) - return true - } else if (value = _.attr(node, 'if')) { - this._bindDirective('if', value) - return true - } else if (value = _.attr(node, 'component')) { - this._bindDirective('component', value) - return true + } + var value, dir + /* jshint boss: true */ + for (var i = 0, l = priorityDirs.length; i < l; i++) { + dir = priorityDirs[i] + if (value = _.attr(node, dir)) { + this._bindDirective(dir, value) + return true + } } } diff --git a/src/parse/expression.js b/src/parse/expression.js index d48f489d3c7..89724daa50c 100644 --- a/src/parse/expression.js +++ b/src/parse/expression.js @@ -243,4 +243,7 @@ exports.parse = function (exp, needSet) { : compileExpFns(exp, needSet) expressionCache.put(exp, res) return res -} \ No newline at end of file +} + +// Export the pathRegex for external use +exports.pathTestRE = pathTestRE \ No newline at end of file diff --git a/src/vue.js b/src/vue.js index 41265bc7b6b..da2712afc95 100644 --- a/src/vue.js +++ b/src/vue.js @@ -1,4 +1,5 @@ -var extend = require('./util').extend +var _ = require('./util') +var extend = _.extend /** * The exposed Vue constructor. @@ -96,4 +97,4 @@ extend(p, require('./api/dom')) extend(p, require('./api/events')) extend(p, require('./api/lifecycle')) -module.exports = Vue \ No newline at end of file +module.exports = _.Vue = Vue \ No newline at end of file From 307208ead6b02320290959b5dbe4b5d354c20b50 Mon Sep 17 00:00:00 2001 From: Evan You Date: Sat, 16 Aug 2014 18:11:17 -0400 Subject: [PATCH 0147/1534] call context for exp fns --- src/directive.js | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/src/directive.js b/src/directive.js index 59122da007b..442e01c3161 100644 --- a/src/directive.js +++ b/src/directive.js @@ -72,10 +72,7 @@ p._bind = function () { this.expression && this.update && (!this.isLiteral || isDynamicLiteral) ) { - if (!this._checkExpFn()) - // check if this is a function directive with an - // inline expression - { + if (!this._checkExpFn()) { this._watcher = new Watcher( this.vm, this.watcherExp, @@ -137,9 +134,9 @@ p._checkExpFn = function () { !expParser.pathTestRE.test(expression) ) { var fn = expParser.parse(expression).get - var scope = this.vm.$scope + var vm = this.vm this.update(function () { - fn(scope) + fn.call(vm, vm.$scope) }) return true } From bcd2c2f91621253b1b1e5b8bbf37a94fb150aa3f Mon Sep 17 00:00:00 2001 From: Evan You Date: Sat, 16 Aug 2014 18:42:36 -0400 Subject: [PATCH 0148/1534] wip directives --- src/directives/partial.js | 36 ++++++++++++++++++++++++++++++++ src/directives/style.js | 43 +++++++++++++++++++++++++++++++++++++++ src/directives/view.js | 36 ++++++++++++++++++++++++++++++++ src/directives/with.js | 20 ++++++++++++++++++ 4 files changed, 135 insertions(+) diff --git a/src/directives/partial.js b/src/directives/partial.js index e69de29bb2d..bb767933534 100644 --- a/src/directives/partial.js +++ b/src/directives/partial.js @@ -0,0 +1,36 @@ +var _ = require('../util') +var templateParser = require('../parse/template') + +module.exports = { + + isLiteral: true, + + bind: function () { + var id = this.expression + var partial = this.vm.$options.partials[id] + if (!partial) { + return + } + partial = templateParser.parse(partial, true) + // comment ref node means inline partial + if (el.nodeType === 8) { + var el = this.el + var vm = this.vm + // keep a ref for the partial's content nodes + var nodes = _.toArray(partial.childNodes) + _.before(partial, el) + _.remove(el) + // compile partial after appending, because its + // children's parentNode will change from the fragment + // to the correct parentNode. This could affect + // directives that need access to its element's + // parentNode. + nodes.forEach(vm._compileNode, vm) + } else { + // just set innerHTML... + el.innerHTML = '' + el.appendChild(partial) + } + } + +} \ No newline at end of file diff --git a/src/directives/style.js b/src/directives/style.js index e69de29bb2d..60ae1139444 100644 --- a/src/directives/style.js +++ b/src/directives/style.js @@ -0,0 +1,43 @@ +var prefixes = ['-webkit-', '-moz-', '-ms-'] +var importantRE = /!important$/ + +module.exports = { + + bind: function () { + var prop = this.arg + if (!prop) return + if (prop.charAt(0) === '$') { + // properties that start with $ will be auto-prefixed + prop = prop.slice(1) + this.prefixed = true + } + this.prop = prop + }, + + update: function (value) { + var prop = this.prop + /* jshint eqeqeq: true */ + // cast possible numbers/booleans into strings + if (value != null) { + value += '' + } + if (prop) { + var isImportant = importantRE.test(value) + ? 'important' + : '' + if (isImportant) { + value = value.slice(0, -10).trim() + } + this.el.style.setProperty(prop, value, isImportant) + if (this.prefixed) { + var i = prefixes.length + while (i--) { + this.el.style.setProperty(prefixes[i] + prop, value, isImportant) + } + } + } else { + this.el.style.cssText = value + } + } + +} \ No newline at end of file diff --git a/src/directives/view.js b/src/directives/view.js index e69de29bb2d..7936c7385df 100644 --- a/src/directives/view.js +++ b/src/directives/view.js @@ -0,0 +1,36 @@ +var _ = require('../util') + +module.exports = { + + bind: function () { + // track position in DOM with a ref node + var el = this.el + var ref = this.ref = document.createComment('v-view') + _.before(ref, el) + _.remove(el) + }, + + update: function(value) { + this.unbind() + if (!value) { + return + } + var Ctor = this.vm.$options.components[value] + if (!Ctor) { + _.warn('Failed to resolve component: ' + value) + return + } + this.childVM = new Ctor({ + el: this.el.cloneNode(true), + parent: this.vm + }) + this.childVM.$before(this.ref) + }, + + unbind: function() { + if (this.childVM) { + this.childVM.$destroy() + } + } + +} \ No newline at end of file diff --git a/src/directives/with.js b/src/directives/with.js index e69de29bb2d..6c643017969 100644 --- a/src/directives/with.js +++ b/src/directives/with.js @@ -0,0 +1,20 @@ +module.exports = { + + bind: function () { + if (this.arg) { + var self = this + this.vm.$watch(this.arg, function (val) { + self.set(val) + }) + } + }, + + update: function (value) { + if (this.arg) { + this.vm.$set(this.arg, value) + } else if (this.vm.$data !== value) { + this.vm.$data = value + } + } + +} \ No newline at end of file From 20b26a619bc7c97a7d3cba51473fd14fa63d14a2 Mon Sep 17 00:00:00 2001 From: Evan You Date: Sat, 16 Aug 2014 18:51:03 -0400 Subject: [PATCH 0149/1534] move strip quotes to _.lang --- src/parse/directive.js | 8 ++------ src/util/lang.js | 15 +++++++++++++++ 2 files changed, 17 insertions(+), 6 deletions(-) diff --git a/src/parse/directive.js b/src/parse/directive.js index 51130600748..6e748f9b356 100644 --- a/src/parse/directive.js +++ b/src/parse/directive.js @@ -1,3 +1,4 @@ +var _ = require('../util') var Cache = require('../cache') var cache = new Cache(1000) var argRE = /^[\w\$-]+$|^'[^']*'$|^"[^"]*"$/ @@ -20,7 +21,6 @@ var dirs var dir var lastFilterIndex var arg -var argC /** * Push a directive object into the result Array @@ -121,11 +121,7 @@ exports.parse = function (s) { // an object literal or a ternary expression. if (argRE.test(arg)) { argIndex = i + 1 - argC = arg.charCodeAt(0) - // strip quotes - dir.arg = argC === 0x22 || argC === 0x27 - ? arg.slice(1, -1) - : arg + dir.arg = _.stripQuotes(arg) } } else if ( c === 0x7C && // pipe diff --git a/src/util/lang.js b/src/util/lang.js index c211550011e..8c199994907 100644 --- a/src/util/lang.js +++ b/src/util/lang.js @@ -12,6 +12,21 @@ exports.guard = function (value) { : value } +/** + * Strip quotes from a string + * + * @param {String} str + * @return {String} + */ + +exports.stripQuotes = function (str) { + var a = str.charCodeAt(0) + var b = str.charCodeAt(str.length - 1) + return a === b && (a === 0x22 || a === 0x27) + ? str.slice(1, -1) + : str +} + /** * Check and convert possible numeric numbers before * setting back to data From b768d0c1f30e7f0452291b30db1ea22cd8eae8a3 Mon Sep 17 00:00:00 2001 From: Evan You Date: Sat, 16 Aug 2014 22:40:59 -0400 Subject: [PATCH 0150/1534] array filters --- src/api/data.js | 3 +- src/directives/index.js | 33 +++++----- src/directives/model.js | 17 +++++ src/directives/on.js | 2 + src/directives/transition.js | 9 +++ src/directives/with.js | 2 + src/filters/array-filters.js | 121 +++++++++++++++++++++++++++++++++++ src/filters/index.js | 112 +++++++++++++++++++++++++++++++- src/util/filter.js | 5 +- src/util/lang.js | 2 + src/vue.js | 2 +- src/watcher.js | 4 +- 12 files changed, 289 insertions(+), 23 deletions(-) create mode 100644 src/directives/transition.js create mode 100644 src/filters/array-filters.js diff --git a/src/api/data.js b/src/api/data.js index a13e4be2453..b8f3b5b3be8 100644 --- a/src/api/data.js +++ b/src/api/data.js @@ -105,7 +105,8 @@ exports.$eval = function (text) { return dir.filters ? _.applyFilters( this.$get(dir.expression), - _.resolveFilters(this, dir.filters).read + _.resolveFilters(this, dir.filters).read, + this ) : this.$get(dir.expression) } else { diff --git a/src/directives/index.js b/src/directives/index.js index 58e1a1ef552..1a0c7c27fdf 100644 --- a/src/directives/index.js +++ b/src/directives/index.js @@ -1,23 +1,24 @@ var directives = module.exports = Object.create(null) // manipulation directives -directives.text = require('./text') -directives.html = require('./html') -directives.attr = require('./attr') -directives.show = require('./show') -directives['class'] = require('./class') -directives.ref = require('./ref') -directives.cloak = require('./cloak') -directives.style = require('./style') -directives.partial = require('./partial') +directives.text = require('./text') +directives.html = require('./html') +directives.attr = require('./attr') +directives.show = require('./show') +directives['class'] = require('./class') +directives.ref = require('./ref') +directives.cloak = require('./cloak') +directives.style = require('./style') +directives.partial = require('./partial') +directives.transition = require('./transition') // event listener directives -directives.on = require('./on') -directives.model = require('./model') +directives.on = require('./on') +directives.model = require('./model') // child vm directives -directives.view = require('./view') -directives.component = require('./component') -directives.repeat = require('./repeat') -directives['if'] = require('./if') -directives['with'] = require('./with') \ No newline at end of file +directives.view = require('./view') +directives.component = require('./component') +directives.repeat = require('./repeat') +directives['if'] = require('./if') +directives['with'] = require('./with') \ No newline at end of file diff --git a/src/directives/model.js b/src/directives/model.js index e69de29bb2d..0823a5683b9 100644 --- a/src/directives/model.js +++ b/src/directives/model.js @@ -0,0 +1,17 @@ +module.exports = { + + priority: 800, + + bind: function () { + + }, + + update: function () { + + }, + + unbind: function () { + + } + +} \ No newline at end of file diff --git a/src/directives/on.js b/src/directives/on.js index 49aa02f0579..5a30a12c24a 100644 --- a/src/directives/on.js +++ b/src/directives/on.js @@ -4,6 +4,8 @@ module.exports = { isFn: true, + priority: 700, + bind: function () { // deal with iframes if ( diff --git a/src/directives/transition.js b/src/directives/transition.js new file mode 100644 index 00000000000..167c606fc1d --- /dev/null +++ b/src/directives/transition.js @@ -0,0 +1,9 @@ +module.exports = { + + priority: 1000, + + bind: function () { + this.el.__vueTransition = this.expression + } + +} \ No newline at end of file diff --git a/src/directives/with.js b/src/directives/with.js index 6c643017969..6a3d1236632 100644 --- a/src/directives/with.js +++ b/src/directives/with.js @@ -1,5 +1,7 @@ module.exports = { + priority: 900, + bind: function () { if (this.arg) { var self = this diff --git a/src/filters/array-filters.js b/src/filters/array-filters.js new file mode 100644 index 00000000000..3d44bd76b12 --- /dev/null +++ b/src/filters/array-filters.js @@ -0,0 +1,121 @@ +var _ = require('../util') +var Path = require('../parse/path') + +/** + * Attempt to convert non-Array objects to array. + * This is the default filter installed to every v-repeat + * directive. + * + * @param {*} obj + * @return {Array} + * @private + */ + +exports._objToArray = function (obj) { + if (_.isArray(obj)) { + return obj + } + if (!_.isObject(obj)) { + _.warn( + 'Invalid value for v-repeat: ' + obj + + '\nOnly Arrays and Objects are allowed.' + ) + return + } + var res = [] + var val, data + for (var key in obj) { + res.push({ + key: key, + value: obj[key] + }) + } + res._converted = true + return res +} + +/** + * Filter filter for v-repeat + * + * @param {String} searchKey + * @param {String} [delimiter] + * @param {String} dataKey + */ + +exports.filterBy = function (arr, searchKey, delimiter, dataKey) { + // allow optional `in` delimiter + // because why not + if (delimiter && delimiter !== 'in') { + dataKey = delimiter + } + // get the search string + var search = + _.stripQuotes(searchKey) || + this.$get(searchKey) + if (!search) { + return arr + } + search = search.toLowerCase() + // get the optional dataKey + dataKey = + dataKey && + (stripQuotes(dataKey) || this.$get(dataKey)) + return arr.filter(function (item) { + return dataKey + ? contains(Path.get(item, dataKey), search) + : contains(item, search) + }) +} + +/** + * Filter filter for v-repeat + * + * @param {String} sortKey + * @param {String} reverseKey + */ + +exports.orderBy = function (arr, sortKey, reverseKey) { + var key = + _.stripQuotes(sortKey) || + this.$get(sortKey) + if (!key) { + return arr + } + var order = 1 + if (reverseKey) { + if (reverseKey === '-1') { + order = -1 + } else if (reverseKey.charCodeAt(0) === 0x21) { // ! + reverseKey = reverseKey.slice(1) + order = this.$get(reverseKey) ? 1 : -1 + } else { + order = this.$get(reverseKey) ? -1 : 1 + } + } + // sort on a copy to avoid mutating original array + return arr.slice().sort(function (a, b) { + a = Path.get(a, key) + b = Path.get(b, key) + return a === b ? 0 : a > b ? order : -order + }) +} + +/** + * String contain helper + * + * @param {*} val + * @param {String} search + */ + +function contains (val, search) { + /* jshint eqeqeq: false */ + if (_.isObject(val)) { + for (var key in val) { + if (contains(val[key], search)) { + return true + } + } + } else if (val != null) { + return val.toString().toLowerCase().indexOf(search) > -1 + } +} \ No newline at end of file diff --git a/src/filters/index.js b/src/filters/index.js index 2efb8b8eeda..01a564e8f7b 100644 --- a/src/filters/index.js +++ b/src/filters/index.js @@ -1 +1,111 @@ -module.exports = Object.create(null) \ No newline at end of file +var _ = require('../util') +var filters = module.exports = Object.create(null) + +/** + * 'abc' => 'Abc' + */ + +filters.capitalize = function (value) { + if (!value && value !== 0) return '' + value = value.toString() + return value.charAt(0).toUpperCase() + value.slice(1) +} + +/** + * 'abc' => 'ABC' + */ + +filters.uppercase = function (value) { + return (value || value === 0) + ? value.toString().toUpperCase() + : '' +} + +/** + * 'AbC' => 'abc' + */ + +filters.lowercase = function (value) { + return (value || value === 0) + ? value.toString().toLowerCase() + : '' +} + +/** + * 12345 => $12,345.00 + * + * @param {String} sign + */ + +var digitsRE = /(\d{3})(?=\d)/g + +filters.currency = function (value, sign) { + value = parseFloat(value) + if (!value && value !== 0) return '' + sign = sign || '$' + var s = Math.floor(value).toString(), + i = s.length % 3, + h = i > 0 + ? (s.slice(0, i) + (s.length > 3 ? ',' : '')) + : '', + f = '.' + value.toFixed(2).slice(-2) + return sign + h + s.slice(i).replace(digitsRE, '$1,') + f +} + +/** + * 'item' => 'items' + * + * @params + * an array of strings corresponding to + * the single, double, triple ... forms of the word to + * be pluralized. When the number to be pluralized + * exceeds the length of the args, it will use the last + * entry in the array. + * + * e.g. ['single', 'double', 'triple', 'multiple'] + */ + +filters.pluralize = function (value) { + var args = slice.call(arguments, 1) + return args.length > 1 + ? (args[value - 1] || args[args.length - 1]) + : (args[value - 1] || args[0] + 's') +} + +/** + * A special filter that takes a handler function, + * wraps it so it only gets triggered on specific + * keypresses. v-on only. + * + * @param {String} key + */ + +var keyCodes = { + enter : 13, + tab : 9, + 'delete' : 46, + up : 38, + left : 37, + right : 39, + down : 40, + esc : 27 +} + +filters.key = function (handler, key) { + if (!handler) return + var code = keyCodes[key] + if (!code) { + code = parseInt(key, 10) + } + return function (e) { + if (e.keyCode === code) { + return handler.call(this, e) + } + } +} + +/** + * Install special array filters + */ + +_.extend(filters, require('./array-filters')) \ No newline at end of file diff --git a/src/util/filter.js b/src/util/filter.js index 374925fae3b..7404cc2d0a7 100644 --- a/src/util/filter.js +++ b/src/util/filter.js @@ -61,15 +61,16 @@ exports.resolveFilters = function (vm, filters, target) { * * @param {*} value * @param {Array} filters + * @param {Vue} vm * @return {*} */ -exports.applyFilters = function (value, filters) { +exports.applyFilters = function (value, filters, vm) { if (!filters) { return value } for (var i = 0, l = filters.length; i < l; i++) { - value = filters[i](value) + value = filters[i].call(vm, value) } return value } \ No newline at end of file diff --git a/src/util/lang.js b/src/util/lang.js index 8c199994907..6ed91c7ed3c 100644 --- a/src/util/lang.js +++ b/src/util/lang.js @@ -49,6 +49,7 @@ exports.guardNumber = function (value) { * * @param {Function} fn * @param {Object} ctx + * @return {Function} */ exports.bind = function (fn, ctx) { @@ -62,6 +63,7 @@ exports.bind = function (fn, ctx) { * * @param {Array-like} list * @param {Number} [i] - start index + * @return {Array} */ var slice = [].slice diff --git a/src/vue.js b/src/vue.js index da2712afc95..321c02bc6cd 100644 --- a/src/vue.js +++ b/src/vue.js @@ -38,7 +38,7 @@ Vue.options = { directives : require('./directives'), filters : require('./filters'), partials : {}, - effects : {}, + transitions : {}, components : {}, inheritScope : true } diff --git a/src/watcher.js b/src/watcher.js index ecd9b855081..a7c7bd6a4b6 100644 --- a/src/watcher.js +++ b/src/watcher.js @@ -97,7 +97,7 @@ p.addDep = function (path) { p.get = function () { this.beforeGet() var value = this.getter.call(this.vm, this.vm.$scope) - value = _.applyFilters(value, this.readFilters) + value = _.applyFilters(value, this.readFilters, this.vm) this.afterGet() return value } @@ -109,7 +109,7 @@ p.get = function () { */ p.set = function (value) { - value = _.applyFilters(value, this.writeFilters) + value = _.applyFilters(value, this.writeFilters, this.vm) this.setter.call(this.vm, this.vm.$scope, value) } From b31550848ced33d03f8f4f50ca9cf6da26b8e42c Mon Sep 17 00:00:00 2001 From: Evan You Date: Sat, 16 Aug 2014 22:59:25 -0400 Subject: [PATCH 0151/1534] size task --- gruntfile.js | 7 ++++++- tasks/size.js | 12 ++++++++++++ 2 files changed, 18 insertions(+), 1 deletion(-) create mode 100644 tasks/size.js diff --git a/gruntfile.js b/gruntfile.js index f7a0a9d41e0..835a6965e73 100644 --- a/gruntfile.js +++ b/gruntfile.js @@ -132,6 +132,11 @@ module.exports = function (grunt) { grunt.registerTask('cover', ['karma:phantom']) grunt.registerTask('bench', ['browserify:bench']) grunt.registerTask('watch', ['browserify:watch']) - grunt.registerTask('build', ['browserify:test', 'browserify:build', 'uglify:build']) + grunt.registerTask('build', [ + 'browserify:test', + 'browserify:build', + 'uglify:build', + 'size' + ]) } \ No newline at end of file diff --git a/tasks/size.js b/tasks/size.js new file mode 100644 index 00000000000..d2cff6555de --- /dev/null +++ b/tasks/size.js @@ -0,0 +1,12 @@ +var zlib = require('zlib') + +module.exports = function (grunt) { + grunt.registerTask('size', function () { + var done = this.async() + zlib.gzip(grunt.file.read('dist/vue.min.js'), function (err, buf) { + var size = (buf.length / 1024).toFixed(2) + console.log('gzipped size: ' + size + 'kb') + done() + }) + }) +} \ No newline at end of file From c1f24e4e5c6e05fc4ba9e80f0d410b631f341b8e Mon Sep 17 00:00:00 2001 From: Evan You Date: Sat, 16 Aug 2014 23:06:03 -0400 Subject: [PATCH 0152/1534] dont remove v-cloak during compile --- src/instance/compile.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/instance/compile.js b/src/instance/compile.js index a74181b0286..7b8700c5c0d 100644 --- a/src/instance/compile.js +++ b/src/instance/compile.js @@ -102,7 +102,9 @@ exports._compileAttrs = function (node) { if (attrName.indexOf(config.prefix) === 0) { dirName = attrName.slice(config.prefix.length) if (registry[dirName]) { - node.removeAttribute(attrName) + if (dirName !== 'cloak') { + node.removeAttribute(attrName) + } dirs.push({ name: dirName, value: attr.value From b4aa4378ea2fcf7342c47d30686d596e4efc32b6 Mon Sep 17 00:00:00 2001 From: Evan You Date: Sun, 17 Aug 2014 14:13:52 -0400 Subject: [PATCH 0153/1534] new v-component --- changes.md | 59 +++++++++---------- src/api/data.js | 6 +- src/api/dom.js | 4 +- src/directive.js | 17 +++--- src/directives/component.js | 108 ++++++++++++++++++++++++++++++----- src/directives/if.js | 46 ++------------- src/directives/index.js | 1 - src/directives/partial.js | 4 +- src/directives/view.js | 36 ------------ src/filters/array-filters.js | 3 +- src/filters/index.js | 2 +- src/instance/compile.js | 7 +-- 12 files changed, 148 insertions(+), 145 deletions(-) delete mode 100644 src/directives/view.js diff --git a/changes.md b/changes.md index 8024f453ad6..792a738e790 100644 --- a/changes.md +++ b/changes.md @@ -67,6 +67,18 @@ computed: { ## Directive changes +### Dynamic literals + +Literal directives can now also be dynamic via bindings like this: + +``` html +
+``` + +When `test` changes, the component used will change! This essentially replaces the old `v-view` directive. + +When authoring literal directives, you can now provide an `update()` function if you wish to handle it dynamically. If no `update()` is provided the directive will be treated as a static literal and only evaluated once. + ### New options - `twoWay`: indicates the directive is two-way and may write back to the model. Allows the use of `this.set(value)` inside directive functions. @@ -95,25 +107,7 @@ Vue.filter('format', { ## Block logic control -``` html - -

{{title}}

-

{{content}}

- -``` - -``` html - - - - -``` - -``` html - -``` - -**Note** The old inline partial syntax `{{> partial}}` has been removed. This is to keep the semantics of interpolation tags purely for interpolation purposes; flow control and partials are now either used in the form of attribute directives or comment directives. +Still open to suggestions. See details [here]. ## Config API change @@ -143,24 +137,25 @@ Vue.config.delimiters = ['(%', '%)'] * Note you still cannot use `<` or `>` in delimiters because Vue uses DOM-based templating. -## (Experimental) Validators - -This is largely write filters that accept a Boolean return value. Probably should live as a plugin. +## One time interpolations ``` html - +{{* hello }} ``` +## `$watch` API change + +`vm.$watch` can now accept an expression: + ``` js - Vue.validator('email', function (val) { - return val.match(...) - }) - // this.$validation.abc // false - // this.$valid // false +vm.$watch('a + b', function (newVal, oldVal) { + // do something +}) ``` -## (Experimental) One time interpolations +By default the callback only fires when the value changes. If you want it to be called immediately with the initial value, use the third optional `immediate` argument: -``` html -{{* hello }} -``` \ No newline at end of file +``` js +vm.$watch('a', callback, true) +// callback is fired immediately with current value of `a` +``` \ No newline at end of file diff --git a/src/api/data.js b/src/api/data.js index b8f3b5b3be8..c00282dc41b 100644 --- a/src/api/data.js +++ b/src/api/data.js @@ -65,12 +65,16 @@ exports.$delete = function (key) { * * @param {String} exp * @param {Function} cb + * @param {Boolean} [immediate] * @return {Number} */ -exports.$watch = function (exp, cb) { +exports.$watch = function (exp, cb, immediate) { var watcher = new Watcher(this, exp, cb, this) this._watchers[watcher.id] = watcher + if (immediate) { + cb.call(this, watcher.value) + } return watcher.id } diff --git a/src/api/dom.js b/src/api/dom.js index 5d643078686..7e933ee4e4d 100644 --- a/src/api/dom.js +++ b/src/api/dom.js @@ -58,9 +58,9 @@ exports.$before = function (target, cb) { exports.$after = function (target, cb) { target = query(target) if (target.nextSibling) { - this.$before(target.nextSibling) + this.$before(target.nextSibling, cb) } else { - this.$appendTo(target.parentNode) + this.$appendTo(target.parentNode, cb) } } diff --git a/src/directive.js b/src/directive.js index 442e01c3161..236d352d4d0 100644 --- a/src/directive.js +++ b/src/directive.js @@ -63,19 +63,19 @@ p._initDef = function () { */ p._bind = function () { - this.watcherExp = this.expression - var isDynamicLiteral = this._checkDynamicLiteral() + this._watcherExp = this.expression + this._checkDynamicLiteral() if (this.bind) { this.bind() } if ( this.expression && this.update && - (!this.isLiteral || isDynamicLiteral) + (!this.isLiteral || this._isDynamicLiteral) ) { if (!this._checkExpFn()) { this._watcher = new Watcher( this.vm, - this.watcherExp, + this._watcherExp, this._update, // callback this, // callback context this.filters, @@ -91,8 +91,6 @@ p._bind = function () { * check if this is a dynamic literal binding. * * e.g. v-component="{{currentView}}" - * - * @return {Boolean} */ p._checkDynamicLiteral = function () { @@ -108,9 +106,10 @@ p._checkDynamicLiteral = function () { 'in literal directives.' ) } else { - this.watcherExp = tokens[0].value - this.expression = this.vm.$eval(expression) - return true + var exp = tokens[0].value + this.expression = this.vm.$get(exp) + this._watcherExp = exp + this._isDynamicLiteral = true } } } diff --git a/src/directives/component.js b/src/directives/component.js index 395ad3bb193..5e676705206 100644 --- a/src/directives/component.js +++ b/src/directives/component.js @@ -1,4 +1,22 @@ var _ = require('../util') +var Watcher = require('../watcher') + +/** + * Possible permutations: + * + * - literal: + * v-component="comp" + * + * - dynamic: + * v-component="{{currentView}}" + * + * - conditional: + * v-component="comp" v-if="abc" + * + * - dynamic + conditional: + * v-component="{{currentView}}" v-if="abc" + * + */ module.exports = { @@ -6,25 +24,87 @@ module.exports = { bind: function () { if (!this.el.__vue__) { - var registry = this.vm.$options.components - var Ctor = registry[this.expression] - if (Ctor) { - this.childVM = new Ctor({ - el: this.el, - parent: this.vm - }) - } else { - _.warn( - 'Failed to resolve component: ' + - this.expression - ) + // create a ref anchor + this.ref = document.createComment('v-component') + _.before(this.ref, this.el) + _.remove(this.el) + // check v-if conditionals + this.checkIf() + // if static, build right now. + if (!this._isDynamicLiteral) { + this.resolveCtor(this.expression) + this.build() } + } else { + _.warn( + 'v-component ' + this.expression + ' cannot be ' + + 'used on an already mounted instance.' + ) } }, - unbind: function () { + checkIf: function () { + var condition = _.attr(this.el, 'if') + if (condition !== null) { + this.ifWatcher = new Watcher( + this.vm, + condition, + this.ifCallback, + this + ) + this.active = this.ifWatcher.value + } else { + this.active = true + } + }, + + ifCallback: function (value) { + if (value) { + this.active = true + this.build() + } else { + this.active = false + this.unbuild(true) + } + }, + + resolveCtor: function (id) { + var registry = this.vm.$options.components + this.Ctor = registry[id] + if (!this.Ctor) { + _.warn('Failed to resolve component: ' + id) + } + }, + + build: function () { + if (this.active && this.Ctor && !this.childVM) { + this.childVM = new this.Ctor({ + el: this.el.cloneNode(true), + parent: this.vm + }) + this.childVM.$before(this.ref) + } + }, + + unbuild: function (remove) { if (this.childVM) { - this.childVM.$destroy() + this.childVM.$destroy(remove) + this.childVM = null + } + }, + + update: function (value) { + this.unbuild(true) + if (value) { + this.resolveCtor(value) + this.build() + } + }, + + unbind: function () { + this.unbuild() + if (this.ifWatcher) { + this.ifWatcher.teardown() } } diff --git a/src/directives/if.js b/src/directives/if.js index 15d033e3e85..2f30e81807e 100644 --- a/src/directives/if.js +++ b/src/directives/if.js @@ -3,51 +3,15 @@ var _ = require('../util') module.exports = { bind: function () { - // resolve component - var registry = this.vm.$options.components - var el = this.el - this.Ctor = - registry[el.tagName.toLowerCase()] || - registry[_.attr(el, 'component')] || - _.Vue - this.isAnonymous = this.Ctor === _.Vue - // insert ref - this.ref = document.createComment('v-if') - _.before(this.ref, el) - _.remove(el) - // warn conflicts - if (_.attr(el, 'view')) { - _.warn( - 'Conflict: v-if cannot be used together with ' + - 'v-view. Just set v-view\'s binding value to ' + - 'empty string to empty it.' - ) - } - if (_.attr(el, 'repeat')) { - _.warn( - 'Conflict: v-if cannot be used together with ' + - 'v-repeat. Use `v-show` or the `filterBy` filter ' + - 'instead.' - ) - } + }, - update: function (value) { - if (!value) { - this.unbind() - } else if (!this.childVM) { - this.childVM = new this.Ctor({ - el: this.el.cloneNode(true), - parent: this.vm, - anonymous: this.isAnonymous - }) - this.childVM.$before(this.ref) - } + update: function () { + }, unbind: function () { - if (this.childVM) { - this.childVM.$destroy() - } + } + } \ No newline at end of file diff --git a/src/directives/index.js b/src/directives/index.js index 1a0c7c27fdf..f1c910418a1 100644 --- a/src/directives/index.js +++ b/src/directives/index.js @@ -17,7 +17,6 @@ directives.on = require('./on') directives.model = require('./model') // child vm directives -directives.view = require('./view') directives.component = require('./component') directives.repeat = require('./repeat') directives['if'] = require('./if') diff --git a/src/directives/partial.js b/src/directives/partial.js index bb767933534..d93397abc7a 100644 --- a/src/directives/partial.js +++ b/src/directives/partial.js @@ -12,10 +12,10 @@ module.exports = { return } partial = templateParser.parse(partial, true) + var el = this.el + var vm = this.vm // comment ref node means inline partial if (el.nodeType === 8) { - var el = this.el - var vm = this.vm // keep a ref for the partial's content nodes var nodes = _.toArray(partial.childNodes) _.before(partial, el) diff --git a/src/directives/view.js b/src/directives/view.js deleted file mode 100644 index 7936c7385df..00000000000 --- a/src/directives/view.js +++ /dev/null @@ -1,36 +0,0 @@ -var _ = require('../util') - -module.exports = { - - bind: function () { - // track position in DOM with a ref node - var el = this.el - var ref = this.ref = document.createComment('v-view') - _.before(ref, el) - _.remove(el) - }, - - update: function(value) { - this.unbind() - if (!value) { - return - } - var Ctor = this.vm.$options.components[value] - if (!Ctor) { - _.warn('Failed to resolve component: ' + value) - return - } - this.childVM = new Ctor({ - el: this.el.cloneNode(true), - parent: this.vm - }) - this.childVM.$before(this.ref) - }, - - unbind: function() { - if (this.childVM) { - this.childVM.$destroy() - } - } - -} \ No newline at end of file diff --git a/src/filters/array-filters.js b/src/filters/array-filters.js index 3d44bd76b12..203d6e2fa2b 100644 --- a/src/filters/array-filters.js +++ b/src/filters/array-filters.js @@ -23,7 +23,6 @@ exports._objToArray = function (obj) { return } var res = [] - var val, data for (var key in obj) { res.push({ key: key, @@ -59,7 +58,7 @@ exports.filterBy = function (arr, searchKey, delimiter, dataKey) { // get the optional dataKey dataKey = dataKey && - (stripQuotes(dataKey) || this.$get(dataKey)) + (_.stripQuotes(dataKey) || this.$get(dataKey)) return arr.filter(function (item) { return dataKey ? contains(Path.get(item, dataKey), search) diff --git a/src/filters/index.js b/src/filters/index.js index 01a564e8f7b..26a913e74d7 100644 --- a/src/filters/index.js +++ b/src/filters/index.js @@ -66,7 +66,7 @@ filters.currency = function (value, sign) { */ filters.pluralize = function (value) { - var args = slice.call(arguments, 1) + var args = _.toArray(arguments, 1) return args.length > 1 ? (args[value - 1] || args[args.length - 1]) : (args[value - 1] || args[0] + 's') diff --git a/src/instance/compile.js b/src/instance/compile.js index 7b8700c5c0d..0cf594cf17d 100644 --- a/src/instance/compile.js +++ b/src/instance/compile.js @@ -180,9 +180,8 @@ exports._compileTextNode = function (node) { var priorityDirs = [ 'repeat', - 'if', - 'view', - 'component' + 'component', + 'if' ] exports._checkPriorityDirs = function (node) { @@ -194,7 +193,7 @@ exports._checkPriorityDirs = function (node) { for (var i = 0, l = priorityDirs.length; i < l; i++) { dir = priorityDirs[i] if (value = _.attr(node, dir)) { - this._bindDirective(dir, value) + this._bindDirective(dir, value, node) return true } } From 590d5b2207ed2961e95cb5e9db545cf316137abf Mon Sep 17 00:00:00 2001 From: Evan You Date: Sun, 17 Aug 2014 15:36:13 -0400 Subject: [PATCH 0154/1534] new v-if --- src/directives/if.js | 46 ++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 42 insertions(+), 4 deletions(-) diff --git a/src/directives/if.js b/src/directives/if.js index 2f30e81807e..3b9de3168d6 100644 --- a/src/directives/if.js +++ b/src/directives/if.js @@ -1,17 +1,55 @@ var _ = require('../util') +var templateParser = require('../parse/template') module.exports = { bind: function () { - + var el = this.el + if (!el.__vue__) { + this.ref = document.createComment('v-if') + _.after(this.ref, el) + _.remove(el) + this.inserted = false + if (el.tagName === 'TEMPLATE') { + this.el = templateParser.parse(el) + } + } else { + _.warn( + 'v-if ' + this.expression + ' cannot be ' + + 'used on an already mounted instance.' + ) + } }, - update: function () { - + update: function (value) { + if (value) { + if (!this.inserted) { + if (!this.childVM) { + this.build() + } + this.childVM.$before(this.ref) + this.inserted = true + } + } else { + if (this.inserted) { + this.childVM.$remove() + this.inserted = false + } + } + }, + + build: function () { + this.childVM = new _.Vue({ + el: this.el, + parent: this.vm, + anonymous: true + }) }, unbind: function () { - + if (this.childVM) { + this.childVM.$destroy() + } } } \ No newline at end of file From 7f00735a731a18946f49bf23d546b50de6473980 Mon Sep 17 00:00:00 2001 From: Evan You Date: Sun, 17 Aug 2014 16:33:16 -0400 Subject: [PATCH 0155/1534] check component options --- src/api/global.js | 56 ++++++++++++++++--------- src/instance/element.js | 17 ++++---- src/instance/init.js | 2 +- src/transition/index.js | 9 ++-- src/util/index.js | 1 - src/util/{option.js => merge-option.js} | 29 ++++++++++--- 6 files changed, 76 insertions(+), 38 deletions(-) rename src/util/{option.js => merge-option.js} (84%) diff --git a/src/api/global.js b/src/api/global.js index 55bea168893..dc9466d0d3b 100644 --- a/src/api/global.js +++ b/src/api/global.js @@ -1,11 +1,5 @@ var _ = require('../util') -var assetTypes = [ - 'directive', - 'filter', - 'partial', - 'effect', - 'component' -] +var mergeOptions = require('../util/merge-option') /** * Expose useful internals @@ -29,7 +23,7 @@ exports.extend = function (extendOptions) { } Sub.prototype = Object.create(Super.prototype) _.define(Sub.prototype, 'constructor', Sub) - Sub.options = _.mergeOptions(Super.options, extendOptions) + Sub.options = mergeOptions(Super.options, extendOptions) Sub.super = Super // allow further extension Sub.extend = Super.extend @@ -67,22 +61,44 @@ exports.use = function (plugin) { /** * Define asset registration methods on a constructor. * - * @param {Function} Ctor + * @param {Function} Constructor */ -createAssetRegisters(exports) -function createAssetRegisters (Ctor) { - assetTypes.forEach(function (type) { +var assetTypes = [ + 'directive', + 'filter', + 'partial', + 'transition' +] + +function createAssetRegisters (Constructor) { - /** - * Asset registration method. - * - * @param {String} id - * @param {*} definition - */ + /* Asset registration methods share the same signature: + * + * @param {String} id + * @param {*} definition + */ - Ctor[type] = function (id, definition) { + assetTypes.forEach(function (type) { + Constructor[type] = function (id, definition) { this.options[type + 's'][id] = definition } }) -} \ No newline at end of file + + /** + * Component registration needs to automatically invoke + * Vue.extend on object values. + * + * @param {String} id + * @param {Object|Function} definition + */ + + Constructor.component = function (id, definition) { + if (_.isObject(definition)) { + definition = _.Vue.extend(definition) + } + this.options.components[id] = definition + } +} + +createAssetRegisters(exports) \ No newline at end of file diff --git a/src/instance/element.js b/src/instance/element.js index 870f2ddbcc5..35eabe6d7fd 100644 --- a/src/instance/element.js +++ b/src/instance/element.js @@ -20,10 +20,10 @@ exports._initElement = function (el) { } if (el instanceof DocumentFragment) { this._initBlock(el) + this._initContent(el) } else { this.$el = el this._initTemplate() - this._initContent() } this.$el.__vue__ = this } @@ -70,16 +70,17 @@ exports._initTemplate = function () { // the template contains multiple nodes // in this case the original `el` is simply // a placeholder. - this._blockNodes = _.toArray(frag.childNodes) - this.$el = document.createComment('vue-block') + this._initBlock(frag) + this._initContent(_.toArray(frag.children)) } else { // 1 to 1 replace, we need to copy all the // attributes from the original el to the replacer this.$el = frag.firstChild _.copyAttributes(el, this.$el) + this._initContent(this.$el) } if (el.parentNode) { - _.before(this.$el, el) + this.$before(el) _.remove(el) } } else { @@ -111,11 +112,13 @@ exports._collectRawContent = function () { * Resolve insertion points mimicking the behavior * of the Shadow DOM spec: * - * http://w3c.github.io/webcomponents/spec/shadow/#insertion-points + * http://w3c.github.io/webcomponents/spec/shadow/#insertion-points + * + * @param {Element|DocumentFragment} el */ -exports._initContent = function () { - var outlets = getOutlets(this.$el) +exports._initContent = function (el) { + var outlets = getOutlets(el) var raw = this._rawContent var i = outlets.length var outlet, select, j, main diff --git a/src/instance/init.js b/src/instance/init.js index b6a60b9de5c..7e1f25795e9 100644 --- a/src/instance/init.js +++ b/src/instance/init.js @@ -1,5 +1,5 @@ var Emitter = require('../emitter') -var mergeOptions = require('../util').mergeOptions +var mergeOptions = require('../util/merge-option') /** * The main init sequence. This is called for every diff --git a/src/transition/index.js b/src/transition/index.js index bfdde19f4b2..17ab6da5e3d 100644 --- a/src/transition/index.js +++ b/src/transition/index.js @@ -5,21 +5,22 @@ var _ = require('../util') exports.append = function (el, target, cb, vm) { target.appendChild(el) + cb && cb() } exports.before = function (el, target, cb, vm) { _.before(el, target) + cb && cb() } exports.remove = function (el, cb, vm) { _.remove(el) + cb && cb() } exports.removeThenAppend = function (el, target, cb, vm) { - setTimeout(function () { - target.appendChild(el) - cb && cb() - }, 500) + target.appendChild(el) + cb && cb() } exports.apply = function (el, direction, cb, vm) { diff --git a/src/util/index.js b/src/util/index.js index bb65dcb040a..2b96b4dd3bf 100644 --- a/src/util/index.js +++ b/src/util/index.js @@ -4,6 +4,5 @@ var extend = lang.extend extend(exports, lang) extend(exports, require('./env')) extend(exports, require('./dom')) -extend(exports, require('./option')) extend(exports, require('./filter')) extend(exports, require('./debug')) \ No newline at end of file diff --git a/src/util/option.js b/src/util/merge-option.js similarity index 84% rename from src/util/option.js rename to src/util/merge-option.js index 1e728574a38..dac542f9ec0 100644 --- a/src/util/option.js +++ b/src/util/merge-option.js @@ -1,6 +1,5 @@ -// alias debug as _ so we can drop _.warn during uglify -var _ = require('./debug') -var extend = require('./lang').extend +var _ = require('./') +var extend = _.extend /** * Option overwriting strategies are functions that handle @@ -42,7 +41,7 @@ strats.paramAttributes = function (parentVal, childVal) { strats.directives = strats.filters = strats.partials = -strats.effects = +strats.transitions = strats.components = function (parentVal, childVal, key, vm) { var ret = Object.create( vm && vm.$parent @@ -103,6 +102,25 @@ var defaultStrat = function (parentVal, childVal) { : childVal } +/** + * Make sure component options get converted to actual + * constructors. + * + * @param {Object} components + */ + +function guardComponents (components) { + if (components) { + var def + for (var key in components) { + def = components[key] + if (_.isObject(def)) { + components[key] = _.Vue.extend(def) + } + } + } +} + /** * Merge two option objects into a new one. * Core utility used in both instantiation and inheritance. @@ -113,7 +131,8 @@ var defaultStrat = function (parentVal, childVal) { * an instantiation merge. */ -exports.mergeOptions = function (parent, child, vm) { +module.exports = function (parent, child, vm) { + guardComponents(child.components) var options = {} var key for (key in parent) { From 37466cb50663efb3f646be30cfd66179df1b1c4d Mon Sep 17 00:00:00 2001 From: Evan You Date: Sun, 17 Aug 2014 16:38:00 -0400 Subject: [PATCH 0156/1534] minor changes --- src/instance/compile.js | 16 +++++----------- test/unit/specs/util_option_spec.js | 3 +-- 2 files changed, 6 insertions(+), 13 deletions(-) diff --git a/src/instance/compile.js b/src/instance/compile.js index 0cf594cf17d..c6b62a64156 100644 --- a/src/instance/compile.js +++ b/src/instance/compile.js @@ -30,17 +30,11 @@ exports._compile = function () { */ exports._compileNode = function (node) { - switch (node.nodeType) { - case 1: // element - if (node.tagName !== 'SCRIPT') { - this._compileElement(node) - } - break - case 3: // text - if (config.interpolate) { - this._compileTextNode(node) - } - break + var type = node.nodeType + if (type === 1 && node.tagName !== 'SCRIPT') { + this._compileElement(node) + } else if (type === 3 && config.interpolate) { + this._compileTextNode(node) } } diff --git a/test/unit/specs/util_option_spec.js b/test/unit/specs/util_option_spec.js index 057432b0948..aa99651cc93 100644 --- a/test/unit/specs/util_option_spec.js +++ b/test/unit/specs/util_option_spec.js @@ -1,8 +1,7 @@ var _ = require('../../../src/util') +var merge = require('../../../src/util/merge-option') describe('Util - Option merging', function () { - - var merge = _.mergeOptions it('default strat', function () { // child undefined From 64d85d3c60856debd49d0f5c3edfc32f6084b9b2 Mon Sep 17 00:00:00 2001 From: Evan You Date: Sun, 17 Aug 2014 16:48:32 -0400 Subject: [PATCH 0157/1534] minor --- gruntfile.js | 6 +++--- src/directives/class.js | 3 +-- src/directives/with.js | 11 +++++------ 3 files changed, 9 insertions(+), 11 deletions(-) diff --git a/gruntfile.js b/gruntfile.js index 835a6965e73..884ff2c0e54 100644 --- a/gruntfile.js +++ b/gruntfile.js @@ -103,9 +103,9 @@ module.exports = function (grunt) { banner: banner, compress: { pure_funcs: [ - '_.log', - '_.warn', - 'enableDebug' + '_.log', + '_.warn', + 'enableDebug' ] } }, diff --git a/src/directives/class.js b/src/directives/class.js index fc65d3276a6..7ca5b7a2f21 100644 --- a/src/directives/class.js +++ b/src/directives/class.js @@ -1,6 +1,5 @@ -var _ = require('../util') var hasClassList = - _.inBrowser && + typeof document !== 'undefined' && 'classList' in document.documentElement /** diff --git a/src/directives/with.js b/src/directives/with.js index 6a3d1236632..7d430dfc746 100644 --- a/src/directives/with.js +++ b/src/directives/with.js @@ -3,12 +3,11 @@ module.exports = { priority: 900, bind: function () { - if (this.arg) { - var self = this - this.vm.$watch(this.arg, function (val) { - self.set(val) - }) - } + var self = this + var path = this.arg || '$data' + this.vm.$watch(path, function (val) { + self.set(val) + }) }, update: function (value) { From d5321eb123958c35ec451cfc140b9339c46c00cf Mon Sep 17 00:00:00 2001 From: Evan You Date: Sun, 17 Aug 2014 16:49:36 -0400 Subject: [PATCH 0158/1534] karma commonjs bridge cannot handle ./ --- src/util/merge-option.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/util/merge-option.js b/src/util/merge-option.js index dac542f9ec0..3151d22e675 100644 --- a/src/util/merge-option.js +++ b/src/util/merge-option.js @@ -1,4 +1,4 @@ -var _ = require('./') +var _ = require('./index') var extend = _.extend /** From 668b16eca162795d4c9c2eda2b0766653726e5b5 Mon Sep 17 00:00:00 2001 From: Evan You Date: Mon, 18 Aug 2014 11:34:06 -0400 Subject: [PATCH 0159/1534] tuning scope init perf --- src/instance/init.js | 3 --- src/instance/scope.js | 62 +++++++++++++++++++------------------------ src/vue.js | 2 +- 3 files changed, 29 insertions(+), 38 deletions(-) diff --git a/src/instance/init.js b/src/instance/init.js index 7e1f25795e9..09b967a4727 100644 --- a/src/instance/init.js +++ b/src/instance/init.js @@ -60,9 +60,6 @@ exports._init = function (options) { // @creates this.$scope this._initScope() - // setup initial data. - this._initData(this._data, true) - // setup property proxying this._initProxy() diff --git a/src/instance/scope.js b/src/instance/scope.js index 69d61c561be..f47070c6ca9 100644 --- a/src/instance/scope.js +++ b/src/instance/scope.js @@ -15,17 +15,26 @@ var scopeEvents = ['set', 'mutate', 'add', 'delete'] exports._initScope = function () { var parent = this.$parent var inherit = parent && this.$options.inheritScope + var data = this._data var scope = this.$scope = inherit ? Object.create(parent.$scope) : {} + // copy initial data into scope + for (var key in data) { + // use defineProperty so we can shadow parent accessors + _.define(scope, key, data[key], true) + } // create scope observer this.$observer = Observer.create(scope, { callbackContext: this, doNotAlterProto: true }) + // setup sync between data and the scope + this._syncData() - if (!inherit) return - + if (!inherit) { + return + } // relay change events that sent down from // the scope prototype chain. var ob = this.$observer @@ -64,45 +73,31 @@ exports._teardownScope = function () { } /** - * Setup the instances data object. - * - * Properties are copied into the scope object to take - * advantage of prototypal inheritance. + * Called when swapping the $data object. * - * If the `syncData` option is true, Vue will maintain - * property syncing between the scope and the original data - * object, so that any changes to the scope are synced back - * to the passed in object. This is useful internally when - * e.g. creating v-repeat instances with no alias. - * - * If swapping data object with the `$data` accessor, - * teardown previous sync listeners and delete keys not - * present in new data. + * Old properties that are not present in new data are + * deleted from the scope, and new data properties not + * already on the scope are added. Teardown old data sync + * listeners and setup new ones. * * @param {Object} data - * @param {Boolean} init - if not ture, indicates its a - * `$data` swap. */ -exports._initData = function (data, init) { +exports._setData = function (data) { var scope = this.$scope var key - - if (!init) { - // teardown old sync listeners - this._unsyncData() - // delete keys not present in the new data - for (key in scope) { - if ( - key.charCodeAt(0) !== 0x24 && // $ - scope.hasOwnProperty(key) && - !(key in data) - ) { - scope.$delete(key) - } + // teardown old sync listeners + this._unsyncData() + // delete keys not present in the new data + for (key in scope) { + if ( + key.charCodeAt(0) !== 0x24 && // $ + scope.hasOwnProperty(key) && + !(key in data) + ) { + scope.$delete(key) } } - // copy properties into scope for (key in data) { if (scope.hasOwnProperty(key)) { @@ -113,10 +108,8 @@ exports._initData = function (data, init) { scope.$add(key, data[key]) } } - // setup sync between scope and new data this._data = data - this._dataObserver = Observer.create(data) this._syncData() } @@ -251,6 +244,7 @@ exports._syncData = function () { .on('add:self', listeners.data.add) .on('delete:self', listeners.data.delete) + this._dataObserver = Observer.create(data) this._dataObserver .on('set:self', listeners.scope.set) .on('add:self', listeners.scope.add) diff --git a/src/vue.js b/src/vue.js index 321c02bc6cd..f9c0ada2372 100644 --- a/src/vue.js +++ b/src/vue.js @@ -73,7 +73,7 @@ Object.defineProperty(p, '$data', { return this._data }, set: function (newData) { - this._initData(newData) + this._setData(newData) } }) From 33da958ed113bfb795f3abc67eeba971e33edfdf Mon Sep 17 00:00:00 2001 From: Evan You Date: Mon, 18 Aug 2014 12:54:01 -0400 Subject: [PATCH 0160/1534] only collect deps for static path getters once --- src/parse/expression.js | 9 +++++---- src/util/merge-option.js | 2 +- src/watcher.js | 25 ++++++++++++++++++------- 3 files changed, 24 insertions(+), 12 deletions(-) diff --git a/src/parse/expression.js b/src/parse/expression.js index 89724daa50c..ae51efd6202 100644 --- a/src/parse/expression.js +++ b/src/parse/expression.js @@ -119,10 +119,11 @@ function compileExpFns (exp, needSet) { var getter = makeGetter(body) if (getter) { return { - get : getter, - body : body, - paths : paths, - set : needSet + computed : true, + get : getter, + body : body, + paths : paths, + set : needSet ? makeSetter(body) : null } diff --git a/src/util/merge-option.js b/src/util/merge-option.js index 3151d22e675..49d9a469000 100644 --- a/src/util/merge-option.js +++ b/src/util/merge-option.js @@ -131,7 +131,7 @@ function guardComponents (components) { * an instantiation merge. */ -module.exports = function (parent, child, vm) { +module.exports = function mergeOptions (parent, child, vm) { guardComponents(child.components) var options = {} var key diff --git a/src/watcher.js b/src/watcher.js index a7c7bd6a4b6..d33875910a5 100644 --- a/src/watcher.js +++ b/src/watcher.js @@ -41,7 +41,7 @@ function Watcher (vm, expression, cb, ctx, filters, needSet) { res = expParser.parse(expression, needSet) this.getter = res.get this.setter = res.set - this.initDeps(res.paths) + this.initDeps(res) } var p = Watcher.prototype @@ -58,15 +58,22 @@ var p = Watcher.prototype * the directive will end up with no dependency at all and * never gets updated. * - * @param {Array} paths + * @param {Object} res - expression parser result object */ -p.initDeps = function (paths) { - var i = paths.length +p.initDeps = function (res) { + var i = res.paths.length while (i--) { - this.addDep(paths[i]) + this.addDep(res.paths[i]) } + // temporarily set computed to true + // to force dep collection on first evaluation + this.isComputed = true this.value = this.get() + var computed = this.vm.$options.computed + this.isComputed = + res.computed || // inline expression + (computed && computed[expression]) // computed property } /** @@ -95,10 +102,14 @@ p.addDep = function (path) { */ p.get = function () { - this.beforeGet() + if (this.isComputed) { + this.beforeGet() + } var value = this.getter.call(this.vm, this.vm.$scope) value = _.applyFilters(value, this.readFilters, this.vm) - this.afterGet() + if (this.isComputed) { + this.afterGet() + } return value } From c6db85d7275a02f6aa25f42e94fa4814487a8e4d Mon Sep 17 00:00:00 2001 From: Evan You Date: Mon, 18 Aug 2014 13:01:26 -0400 Subject: [PATCH 0161/1534] restructure unit tests --- test/unit/specs/{api_data_spec.js => api/data_spec.js} | 0 .../specs/{instance_scope_spec.js => instance/scope_spec.js} | 4 ++-- .../{parse_directive_spec.js => parse/directive_spec.js} | 2 +- .../{parse_expression_spec.js => parse/expression_spec.js} | 4 ++-- test/unit/specs/{parse_path_spec.js => parse/path_spec.js} | 4 ++-- .../specs/{parse_template_spec.js => parse/template_spec.js} | 4 ++-- test/unit/specs/{parse_text_spec.js => parse/text_spec.js} | 4 ++-- test/unit/specs/{util_debug_spec.js => util/debug_spec.js} | 4 ++-- test/unit/specs/{util_dom_spec.js => util/dom_spec.js} | 2 +- test/unit/specs/{util_filter_spec.js => util/filter_spec.js} | 4 ++-- test/unit/specs/{util_lang_spec.js => util/lang_spec.js} | 2 +- test/unit/specs/{util_option_spec.js => util/option_spec.js} | 4 ++-- 12 files changed, 19 insertions(+), 19 deletions(-) rename test/unit/specs/{api_data_spec.js => api/data_spec.js} (100%) rename test/unit/specs/{instance_scope_spec.js => instance/scope_spec.js} (98%) rename test/unit/specs/{parse_directive_spec.js => parse/directive_spec.js} (98%) rename test/unit/specs/{parse_expression_spec.js => parse/expression_spec.js} (98%) rename test/unit/specs/{parse_path_spec.js => parse/path_spec.js} (96%) rename test/unit/specs/{parse_template_spec.js => parse/template_spec.js} (97%) rename test/unit/specs/{parse_text_spec.js => parse/text_spec.js} (92%) rename test/unit/specs/{util_debug_spec.js => util/debug_spec.js} (93%) rename test/unit/specs/{util_dom_spec.js => util/dom_spec.js} (97%) rename test/unit/specs/{util_filter_spec.js => util/filter_spec.js} (94%) rename test/unit/specs/{util_lang_spec.js => util/lang_spec.js} (98%) rename test/unit/specs/{util_option_spec.js => util/option_spec.js} (96%) diff --git a/test/unit/specs/api_data_spec.js b/test/unit/specs/api/data_spec.js similarity index 100% rename from test/unit/specs/api_data_spec.js rename to test/unit/specs/api/data_spec.js diff --git a/test/unit/specs/instance_scope_spec.js b/test/unit/specs/instance/scope_spec.js similarity index 98% rename from test/unit/specs/instance_scope_spec.js rename to test/unit/specs/instance/scope_spec.js index ff795d40e4e..fbd304d954a 100644 --- a/test/unit/specs/instance_scope_spec.js +++ b/test/unit/specs/instance/scope_spec.js @@ -3,8 +3,8 @@ * data event propagation and data sync */ -var Vue = require('../../../src/vue') -var Observer = require('../../../src/observe/observer') +var Vue = require('../../../../src/vue') +var Observer = require('../../../../src/observe/observer') var u = undefined Observer.pathDelimiter = '.' diff --git a/test/unit/specs/parse_directive_spec.js b/test/unit/specs/parse/directive_spec.js similarity index 98% rename from test/unit/specs/parse_directive_spec.js rename to test/unit/specs/parse/directive_spec.js index 3b6ddd8a73a..fae5ee68eb1 100644 --- a/test/unit/specs/parse_directive_spec.js +++ b/test/unit/specs/parse/directive_spec.js @@ -1,4 +1,4 @@ -var parse = require('../../../src/parse/directive').parse +var parse = require('../../../../src/parse/directive').parse describe('Directive Parser', function () { diff --git a/test/unit/specs/parse_expression_spec.js b/test/unit/specs/parse/expression_spec.js similarity index 98% rename from test/unit/specs/parse_expression_spec.js rename to test/unit/specs/parse/expression_spec.js index 2c558581bee..c128b2e93ac 100644 --- a/test/unit/specs/parse_expression_spec.js +++ b/test/unit/specs/parse/expression_spec.js @@ -1,5 +1,5 @@ -var expParser = require('../../../src/parse/expression') -var _ = require('../../../src/util') +var expParser = require('../../../../src/parse/expression') +var _ = require('../../../../src/util') function assertExp (testCase) { var res = expParser.parse(testCase.exp) diff --git a/test/unit/specs/parse_path_spec.js b/test/unit/specs/parse/path_spec.js similarity index 96% rename from test/unit/specs/parse_path_spec.js rename to test/unit/specs/parse/path_spec.js index e9cbd2c9656..11ad4b330dc 100644 --- a/test/unit/specs/parse_path_spec.js +++ b/test/unit/specs/parse/path_spec.js @@ -1,5 +1,5 @@ -var Path = require('../../../src/parse/path') -var Observer = require('../../../src/observe/observer') +var Path = require('../../../../src/parse/path') +var Observer = require('../../../../src/observe/observer') function assertPath (str, expected) { var path = Path.parse(str) diff --git a/test/unit/specs/parse_template_spec.js b/test/unit/specs/parse/template_spec.js similarity index 97% rename from test/unit/specs/parse_template_spec.js rename to test/unit/specs/parse/template_spec.js index d69c56c66ed..48a7873d7e6 100644 --- a/test/unit/specs/parse_template_spec.js +++ b/test/unit/specs/parse/template_spec.js @@ -1,5 +1,5 @@ -var _ = require('../../../src/util') -var templateParser = require('../../../src/parse/template') +var _ = require('../../../../src/util') +var templateParser = require('../../../../src/parse/template') var parse = templateParser.parse var testString = '
hello

world

' diff --git a/test/unit/specs/parse_text_spec.js b/test/unit/specs/parse/text_spec.js similarity index 92% rename from test/unit/specs/parse_text_spec.js rename to test/unit/specs/parse/text_spec.js index 9a8a338281d..b6ba5a58799 100644 --- a/test/unit/specs/parse_text_spec.js +++ b/test/unit/specs/parse/text_spec.js @@ -1,5 +1,5 @@ -var textParser = require('../../../src/parse/text') -var config = require('../../../src/config') +var textParser = require('../../../../src/parse/text') +var config = require('../../../../src/config') var testCases = [ { diff --git a/test/unit/specs/util_debug_spec.js b/test/unit/specs/util/debug_spec.js similarity index 93% rename from test/unit/specs/util_debug_spec.js rename to test/unit/specs/util/debug_spec.js index 6c7eb371eaf..037f418a94a 100644 --- a/test/unit/specs/util_debug_spec.js +++ b/test/unit/specs/util/debug_spec.js @@ -1,5 +1,5 @@ -var _ = require('../../../src/util') -var config = require('../../../src/config') +var _ = require('../../../../src/util') +var config = require('../../../../src/config') config.silent = true if (typeof console !== 'undefined') { diff --git a/test/unit/specs/util_dom_spec.js b/test/unit/specs/util/dom_spec.js similarity index 97% rename from test/unit/specs/util_dom_spec.js rename to test/unit/specs/util/dom_spec.js index 670810b5088..2c187bc3b9a 100644 --- a/test/unit/specs/util_dom_spec.js +++ b/test/unit/specs/util/dom_spec.js @@ -1,4 +1,4 @@ -var _ = require('../../../src/util') +var _ = require('../../../../src/util') if (_.inBrowser) { diff --git a/test/unit/specs/util_filter_spec.js b/test/unit/specs/util/filter_spec.js similarity index 94% rename from test/unit/specs/util_filter_spec.js rename to test/unit/specs/util/filter_spec.js index 642207e990e..918c87de687 100644 --- a/test/unit/specs/util_filter_spec.js +++ b/test/unit/specs/util/filter_spec.js @@ -1,8 +1,8 @@ -var _ = require('../../../src/util') +var _ = require('../../../../src/util') describe('Util - Filter', function () { - var debug = require('../../../src/util/debug') + var debug = require('../../../../src/util/debug') beforeEach(function () { spyOn(debug, 'warn') }) diff --git a/test/unit/specs/util_lang_spec.js b/test/unit/specs/util/lang_spec.js similarity index 98% rename from test/unit/specs/util_lang_spec.js rename to test/unit/specs/util/lang_spec.js index ac942698fea..c089373de20 100644 --- a/test/unit/specs/util_lang_spec.js +++ b/test/unit/specs/util/lang_spec.js @@ -1,4 +1,4 @@ -var _ = require('../../../src/util') +var _ = require('../../../../src/util') describe('Util - Language Enhancement', function () { diff --git a/test/unit/specs/util_option_spec.js b/test/unit/specs/util/option_spec.js similarity index 96% rename from test/unit/specs/util_option_spec.js rename to test/unit/specs/util/option_spec.js index aa99651cc93..e1285226ebb 100644 --- a/test/unit/specs/util_option_spec.js +++ b/test/unit/specs/util/option_spec.js @@ -1,5 +1,5 @@ -var _ = require('../../../src/util') -var merge = require('../../../src/util/merge-option') +var _ = require('../../../../src/util') +var merge = require('../../../../src/util/merge-option') describe('Util - Option merging', function () { From 5862da6475ad1122dff94e2b038005aaa361ba62 Mon Sep 17 00:00:00 2001 From: Evan You Date: Mon, 18 Aug 2014 13:03:02 -0400 Subject: [PATCH 0162/1534] unit test build fix --- gruntfile.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gruntfile.js b/gruntfile.js index 884ff2c0e54..29c9d6da87d 100644 --- a/gruntfile.js +++ b/gruntfile.js @@ -92,7 +92,7 @@ module.exports = function (grunt) { dest: 'benchmarks/browser.js' }, test: { - src: ['test/unit/specs/*.js'], + src: ['test/unit/specs/**/*.js'], dest: 'test/unit/specs.js' } }, From e2d5f4514c5e1a006f8c694d98fdb1e893c15c9a Mon Sep 17 00:00:00 2001 From: Evan You Date: Mon, 18 Aug 2014 15:09:09 -0400 Subject: [PATCH 0163/1534] binding test --- test/unit/specs/binding_spec.js | 40 +++++++++++++++++++++++++++++++++ 1 file changed, 40 insertions(+) create mode 100644 test/unit/specs/binding_spec.js diff --git a/test/unit/specs/binding_spec.js b/test/unit/specs/binding_spec.js new file mode 100644 index 00000000000..d165d2f1585 --- /dev/null +++ b/test/unit/specs/binding_spec.js @@ -0,0 +1,40 @@ +var Binding = require('../../../src/binding') + +describe('Binding', function () { + + var b + beforeEach(function () { + b = new Binding() + }) + + it('addChild', function () { + var child = new Binding() + b._addChild('test', child) + expect(b.test).toBe(child) + }) + + it('addSub', function () { + var sub = {} + b._addSub(sub) + expect(b._subs.length).toBe(1) + expect(b._subs.indexOf(sub)).toBe(0) + }) + + it('removeSub', function () { + var sub = {} + b._addSub(sub) + b._removeSub(sub) + expect(b._subs.length).toBe(0) + expect(b._subs.indexOf(sub)).toBe(-1) + }) + + it('notify', function () { + var sub = { + update: jasmine.createSpy('sub') + } + b._addSub(sub) + b._notify() + expect(sub.update).toHaveBeenCalled() + }) + +}) \ No newline at end of file From 73c9d99b2d3932b79c679061fd4de5b5ce3baabd Mon Sep 17 00:00:00 2001 From: Evan You Date: Mon, 18 Aug 2014 18:03:01 -0400 Subject: [PATCH 0164/1534] test for directive class --- changes.md | 7 +- gruntfile.js | 6 +- src/directive.js | 34 ++----- src/instance/scope.js | 2 +- src/observe/object-augmentations.js | 4 +- src/vue.js | 11 +- src/watcher.js | 1 + test/unit/specs/directive_spec.js | 149 ++++++++++++++++++++++++++++ test/unit/specs/filters_spec.js | 0 test/unit/specs/parse/text_spec.js | 12 +++ test/unit/specs/util/option_spec.js | 11 ++ 11 files changed, 196 insertions(+), 41 deletions(-) create mode 100644 test/unit/specs/directive_spec.js create mode 100644 test/unit/specs/filters_spec.js diff --git a/changes.md b/changes.md index 792a738e790..63fcd81db8f 100644 --- a/changes.md +++ b/changes.md @@ -34,11 +34,11 @@ It's probably easy to understand why `el` and `parent` are instance only. But wh When events are used extensively for cross-vm communication, the ready hook can get kinda messy. The new `events` option is similar to its Backbone equivalent, where you can declaratiely register a bunch of event listeners. -### new option: `inheritScope`. +### new option: `isolated`. -Default: `true`. +Default: `false`. -Whether to inherit parent scope data. Set it to `false` if you want to create a component that have an isolated scope of its own. +Whether to inherit parent scope data. Set it to `true` if you want to create a component that have an isolated scope of its own. An isolated scope means you won't be able to bind to data on parent scopes in the component's template. ### removed options: `id`, `tagName`, `className`, `attributes`, `lazy`. @@ -82,7 +82,6 @@ When authoring literal directives, you can now provide an `update()` function if ### New options - `twoWay`: indicates the directive is two-way and may write back to the model. Allows the use of `this.set(value)` inside directive functions. -- `paramAttributes`: an Array of attribute names to extract as parameters for the directive. For example, given the option value `['my-param']` and markup ``, you can access `this.params['my-param']` with value `'123'` inside directive functions. ### Removed option: `isEmpty` diff --git a/gruntfile.js b/gruntfile.js index 29c9d6da87d..2796f38be5f 100644 --- a/gruntfile.js +++ b/gruntfile.js @@ -31,11 +31,11 @@ module.exports = function (grunt) { frameworks: ['jasmine', 'commonjs'], files: [ 'src/**/*.js', - 'test/unit/specs/*.js' + 'test/unit/specs/**/*.js' ], preprocessors: { 'src/**/*.js': ['commonjs'], - 'test/unit/specs/*.js': ['commonjs'] + 'test/unit/specs/**/*.js': ['commonjs'] }, singleRun: true }, @@ -51,7 +51,7 @@ module.exports = function (grunt) { reporters: ['progress', 'coverage'], preprocessors: { 'src/**/*.js': ['commonjs', 'coverage'], - 'test/unit/specs/*.js': ['commonjs'] + 'test/unit/specs/**/*.js': ['commonjs'] }, coverageReporter: { reporters: [ diff --git a/src/directive.js b/src/directive.js index 236d352d4d0..1ac4ae36ea2 100644 --- a/src/directive.js +++ b/src/directive.js @@ -27,42 +27,23 @@ function Directive (name, el, vm, descriptor) { this.arg = descriptor.arg this.expression = descriptor.expression this.filters = descriptor.filters - // private this._locked = false this._bound = false - // init definition - this._initDef() + // init this._bind() } var p = Directive.prototype /** - * Initialize the directive instance's definition. - */ - -p._initDef = function () { - var def = this.vm.$options.directives[this.name] - _.extend(this, def) - // init params - var el = this.el - var attrs = this.paramAttributes - if (attrs) { - var params = this.params = {} - attrs.forEach(function (p) { - params[p] = el.getAttribute(p) - el.removeAttribute(p) - }) - } -} - -/** - * Initialize the directive, setup the watcher, - * call definition bind() and update() if present. + * Initialize the directive, mixin definition properties, + * setup the watcher, call definition bind() and update() + * if present. */ p._bind = function () { + _.extend(this, this.vm.$options.directives[this.name]) this._watcherExp = this.expression this._checkDynamicLiteral() if (this.bind) { @@ -186,7 +167,10 @@ p.set = function (value, lock) { } this._watcher.set(value) if (lock) { - _.nextTick(this._unlock, this) + var self = this + _.nextTick(function () { + self._locked = false + }) } } } diff --git a/src/instance/scope.js b/src/instance/scope.js index f47070c6ca9..21615d349a6 100644 --- a/src/instance/scope.js +++ b/src/instance/scope.js @@ -14,7 +14,7 @@ var scopeEvents = ['set', 'mutate', 'add', 'delete'] exports._initScope = function () { var parent = this.$parent - var inherit = parent && this.$options.inheritScope + var inherit = parent && !this.$options.isolated var data = this._data var scope = this.$scope = inherit ? Object.create(parent.$scope) diff --git a/src/observe/object-augmentations.js b/src/observe/object-augmentations.js index 9f80eb8bd6f..74d43f3810e 100644 --- a/src/observe/object-augmentations.js +++ b/src/observe/object-augmentations.js @@ -10,7 +10,7 @@ var objectAgumentations = Object.create(Object.prototype) * @public */ -_.define(objectAgumentations, '$add', function (key, val) { +_.define(objectAgumentations, '$add', function $add (key, val) { if (this.hasOwnProperty(key)) return // make sure it's defined on itself. _.define(this, key, val, true) @@ -29,7 +29,7 @@ _.define(objectAgumentations, '$add', function (key, val) { * @public */ -_.define(objectAgumentations, '$delete', function (key) { +_.define(objectAgumentations, '$delete', function $delete (key) { if (!this.hasOwnProperty(key)) return delete this[key] var ob = this.$observer diff --git a/src/vue.js b/src/vue.js index f9c0ada2372..dd93646623d 100644 --- a/src/vue.js +++ b/src/vue.js @@ -35,12 +35,11 @@ extend(Vue, require('./api/global')) */ Vue.options = { - directives : require('./directives'), - filters : require('./filters'), - partials : {}, - transitions : {}, - components : {}, - inheritScope : true + directives : require('./directives'), + filters : require('./filters'), + partials : {}, + transitions : {}, + components : {} } /** diff --git a/src/watcher.js b/src/watcher.js index d33875910a5..336c9d788ba 100644 --- a/src/watcher.js +++ b/src/watcher.js @@ -72,6 +72,7 @@ p.initDeps = function (res) { this.value = this.get() var computed = this.vm.$options.computed this.isComputed = + this.filters || // filters may access instance data res.computed || // inline expression (computed && computed[expression]) // computed property } diff --git a/test/unit/specs/directive_spec.js b/test/unit/specs/directive_spec.js new file mode 100644 index 00000000000..cfba71df21b --- /dev/null +++ b/test/unit/specs/directive_spec.js @@ -0,0 +1,149 @@ +var Vue = require('../../../src/vue') +var Directive = require('../../../src/directive') +var nextTick = Vue.nextTick + +describe('Directive', function () { + + var el = {} // simply a mock to be able to run in Node + var vm, def + + beforeEach(function () { + def = { + bind: jasmine.createSpy('bind'), + update: jasmine.createSpy('update'), + unbind: jasmine.createSpy('unbind') + } + vm = new Vue({ + data:{ + a:1 + }, + filters: { + test: function (v) { + return v * 2 + } + }, + directives: { + test: def + } + }) + }) + + it('normal', function (done) { + var d = new Directive('test', el, vm, { + expression: 'a', + arg: 'someArg', + filters: [{name:'test'}] + }) + // properties + expect(d.el).toBe(el) + expect(d.name).toBe('test') + expect(d.vm).toBe(vm) + expect(d.arg).toBe('someArg') + expect(d.expression).toBe('a') + // init calls + expect(def.bind).toHaveBeenCalled() + expect(def.update).toHaveBeenCalledWith(2) + expect(d._bound).toBe(true) + // update + vm.a = 2 + nextTick(function () { + expect(def.update).toHaveBeenCalledWith(4, 2) + // teardown + d._teardown() + expect(def.unbind).toHaveBeenCalled() + expect(d._bound).toBe(false) + expect(d._watcher.active).toBe(false) + done() + }) + }) + + it('static literal', function () { + def.isLiteral = true + var d = new Directive('test', el, vm, { + expression: 'a' + }) + expect(d._watcher).toBeUndefined() + expect(d.expression).toBe('a') + expect(d.bind).toHaveBeenCalled() + expect(d.update).not.toHaveBeenCalled() + }) + + it('static literal, interpolate with no update', function () { + def.isLiteral = true + delete def.update + var d = new Directive('test', el, vm, { + expression: '{{a}}' + }) + expect(d._watcher).toBeUndefined() + expect(d.expression).toBe(1) + expect(d.bind).toHaveBeenCalled() + }) + + it('dynamic literal', function (done) { + def.isLiteral = true + var d = new Directive('test', el, vm, { + expression: '{{a}}' + }) + expect(d._watcher).toBeDefined() + expect(d.expression).toBe(1) + expect(def.bind).toHaveBeenCalled() + expect(def.update).toHaveBeenCalledWith(1) + vm.a = 2 + nextTick(function () { + expect(def.update).toHaveBeenCalledWith(2, 1) + done() + }) + }) + + it('expression function', function () { + def.isFn = true + var d = new Directive('test', el, vm, { + expression: 'a++' + }) + expect(d._watcher).toBeUndefined() + expect(d.bind).toHaveBeenCalled() + var wrappedFn = d.update.calls.argsFor(0)[0] + expect(typeof wrappedFn).toBe('function') + // test invoke the wrapped fn + wrappedFn() + expect(vm.a).toBe(2) + }) + + it('two-way', function (done) { + def.twoWay = true + vm.$options.filters.test = { + write: function (v) { + return v * 3 + } + } + var d = new Directive('test', el, vm, { + expression: 'a', + filters: [{name:'test'}] + }) + d.set(2) + expect(vm.a).toBe(6) + nextTick(function () { + expect(def.update.calls.count()).toBe(2) + expect(def.update).toHaveBeenCalledWith(6, 1) + // locked set + d.set(3, true) + expect(vm.a).toBe(9) + nextTick(function () { + // should have no update calls + expect(def.update.calls.count()).toBe(2) + done() + }) + }) + }) + + it('invalid dynamic literal', function () { + var _ = Vue.util + spyOn(_, 'warn') + def.isLiteral = true + new Directive('test', el, vm, { + expression: 'abc {{a}}' + }) + expect(_.warn).toHaveBeenCalled() + }) + +}) \ No newline at end of file diff --git a/test/unit/specs/filters_spec.js b/test/unit/specs/filters_spec.js new file mode 100644 index 00000000000..e69de29bb2d diff --git a/test/unit/specs/parse/text_spec.js b/test/unit/specs/parse/text_spec.js index b6ba5a58799..2e3af7c09fa 100644 --- a/test/unit/specs/parse/text_spec.js +++ b/test/unit/specs/parse/text_spec.js @@ -64,4 +64,16 @@ describe('Text Parser', function () { expect(res1).toBe(res2) }) + it('custom delimiters', function () { + config.delimiters = ['[%', '%]'] + assertParse({ + text: '[%* text %] and [[% html %]]', + expected: [ + { tag: true, value: 'text', html: false, oneTime: true }, + { value: ' and ' }, + { tag: true, value: 'html', html: true, oneTime: false }, + ] + }) + }) + }) \ No newline at end of file diff --git a/test/unit/specs/util/option_spec.js b/test/unit/specs/util/option_spec.js index e1285226ebb..c837c16eb5b 100644 --- a/test/unit/specs/util/option_spec.js +++ b/test/unit/specs/util/option_spec.js @@ -1,4 +1,5 @@ var _ = require('../../../../src/util') +var Vue = require('../../../../src/vue') var merge = require('../../../../src/util/merge-option') describe('Util - Option merging', function () { @@ -111,6 +112,16 @@ describe('Util - Option merging', function () { expect(res.d).toBe(asset4) }) + it('guard components', function () { + var res = merge({}, { + components: { + a: { template: 'hi' } + } + }) + expect(typeof res.components.a).toBe('function') + expect(res.components.a.super).toBe(Vue) + }) + it('ignore el, data & parent when inheriting', function () { var res = merge({}, {el:1, data:2, parent:3}) expect(res.el).toBeUndefined() From 695a6d48e9cfee0e3f12c9ac55d5d9af695a99b5 Mon Sep 17 00:00:00 2001 From: Evan You Date: Mon, 18 Aug 2014 19:19:45 -0400 Subject: [PATCH 0165/1534] tests for filters --- src/filters/index.js | 14 ++- src/parse/directive.js | 2 +- src/util/lang.js | 30 ++--- test/unit/specs/filters_spec.js | 189 ++++++++++++++++++++++++++++++ test/unit/specs/util/lang_spec.js | 6 + 5 files changed, 223 insertions(+), 18 deletions(-) diff --git a/src/filters/index.js b/src/filters/index.js index 26a913e74d7..59ae88a5084 100644 --- a/src/filters/index.js +++ b/src/filters/index.js @@ -1,6 +1,16 @@ var _ = require('../util') var filters = module.exports = Object.create(null) +/** + * Stringify value. + * + * @param {Number} indent + */ + +filters.json = function (value, indent) { + return JSON.stringify(value, null, indent || 2) +} + /** * 'abc' => 'Abc' */ @@ -68,8 +78,8 @@ filters.currency = function (value, sign) { filters.pluralize = function (value) { var args = _.toArray(arguments, 1) return args.length > 1 - ? (args[value - 1] || args[args.length - 1]) - : (args[value - 1] || args[0] + 's') + ? (args[value % 10 - 1] || args[args.length - 1]) + : (args[0] + (value === 1 ? '' : 's')) } /** diff --git a/src/parse/directive.js b/src/parse/directive.js index 6e748f9b356..72ffa98e23d 100644 --- a/src/parse/directive.js +++ b/src/parse/directive.js @@ -121,7 +121,7 @@ exports.parse = function (s) { // an object literal or a ternary expression. if (argRE.test(arg)) { argIndex = i + 1 - dir.arg = _.stripQuotes(arg) + dir.arg = _.stripQuotes(arg) || arg } } else if ( c === 0x7C && // pipe diff --git a/src/util/lang.js b/src/util/lang.js index 6ed91c7ed3c..9821a33b48a 100644 --- a/src/util/lang.js +++ b/src/util/lang.js @@ -12,21 +12,6 @@ exports.guard = function (value) { : value } -/** - * Strip quotes from a string - * - * @param {String} str - * @return {String} - */ - -exports.stripQuotes = function (str) { - var a = str.charCodeAt(0) - var b = str.charCodeAt(str.length - 1) - return a === b && (a === 0x22 || a === 0x27) - ? str.slice(1, -1) - : str -} - /** * Check and convert possible numeric numbers before * setting back to data @@ -44,6 +29,21 @@ exports.guardNumber = function (value) { : Number(value) } +/** + * Strip quotes from a string + * + * @param {String} str + * @return {String | false} + */ + +exports.stripQuotes = function (str) { + var a = str.charCodeAt(0) + var b = str.charCodeAt(str.length - 1) + return a === b && (a === 0x22 || a === 0x27) + ? str.slice(1, -1) + : false +} + /** * Simple bind, faster than native * diff --git a/test/unit/specs/filters_spec.js b/test/unit/specs/filters_spec.js index e69de29bb2d..f8037d887e3 100644 --- a/test/unit/specs/filters_spec.js +++ b/test/unit/specs/filters_spec.js @@ -0,0 +1,189 @@ +var Vue = require('../../../src/vue') +var filters = require('../../../src/filters') + +describe('Filters', function () { + + it('json', function () { + var filter = filters.json + var obj = {a:{b:2}} + expect(filter(obj)).toBe(JSON.stringify(obj, null, 2)) + expect(filter(obj, 4)).toBe(JSON.stringify(obj, null, 4)) + }) + + it('capitalize', function () { + var filter = filters.capitalize + var res = filter('fsefsfsef') + expect(res.charAt(0)).toBe('F') + expect(res.slice(1)).toBe('sefsfsef') + assertNumberAndFalsy(filter) + }) + + it('uppercase', function () { + var filter = filters.uppercase + expect(filter('fsefef')).toBe('FSEFEF') + assertNumberAndFalsy(filter) + }) + + it('lowercase', function () { + var filter = filters.lowercase + expect(filter('AWEsoME')).toBe('awesome') + assertNumberAndFalsy(filter) + }) + + it('pluralize', function () { + var filter = filters.pluralize + // single arg + var arg = 'item' + expect(filter(0, arg)).toBe('items') + expect(filter(1, arg)).toBe('item') + expect(filter(2, arg)).toBe('items') + // multi args + expect(filter(0, 'st', 'nd', 'rd', 'th')).toBe('th') + expect(filter(1, 'st', 'nd', 'rd', 'th')).toBe('st') + expect(filter(2, 'st', 'nd', 'rd', 'th')).toBe('nd') + expect(filter(3, 'st', 'nd', 'rd', 'th')).toBe('rd') + expect(filter(4, 'st', 'nd', 'rd', 'th')).toBe('th') + }) + + it('currency', function () { + var filter = filters.currency + // default + expect(filter(1234)).toBe('$1,234.00') + expect(filter(1234.45)).toBe('$1,234.45') + expect(filter(123443434.4343434)).toBe('$123,443,434.43') + // sign arg + expect(filter(2134, '@')).toBe('@2,134.00') + // falsy and 0 + expect(filter(0)).toBe('$0.00') + expect(filter(false)).toBe('') + expect(filter(null)).toBe('') + expect(filter(undefined)).toBe('') + }) + + it('key', function () { + var filter = filters.key + expect(filter(null)).toBeUndefined() + var spy = jasmine.createSpy('filter:key') + var handler = filter(spy, 'enter') + handler({ keyCode: 0 }) + expect(spy).not.toHaveBeenCalled() + handler({ keyCode: 13 }) + expect(spy).toHaveBeenCalled() + // direct keycode + spy = jasmine.createSpy('filter:key') + handler = filter(spy, 13) + handler({ keyCode: 0 }) + expect(spy).not.toHaveBeenCalled() + handler({ keyCode: 13 }) + expect(spy).toHaveBeenCalled() + }) + + it('objToArray', function () { + var filter = filters._objToArray + var res = filter({a:1, b:2}) + expect(res._converted).toBe(true) + expect(res[0].key).toBe('a') + expect(res[0].value).toBe(1) + expect(res[1].key).toBe('b') + expect(res[1].value).toBe(2) + // array + var arr = [] + expect(filter(arr)).toBe(arr) + // non object/array + spyOn(Vue.util, 'warn') + expect(filter(123)).toBeUndefined() + expect(Vue.util.warn).toHaveBeenCalled() + }) + + it('filterBy', function () { + var filter = filters.filterBy + var arr = [ + { a: 1, b: { c: 'hello' }}, + { a: 1, b: 'hello'}, + { a: 1, b: 2 } + ] + var vm = new Vue({ + data: { + search: { + key: 'hello', + datakey: 'b.c' + } + } + }) + var res + // normal + res = filter.call(vm, arr, 'search.key') + expect(res.length).toBe(2) + expect(res[0]).toBe(arr[0]) + expect(res[1]).toBe(arr[1]) + // data key + res = filter.call(vm, arr, 'search.key', 'search.datakey') + expect(res.length).toBe(1) + expect(res[0]).toBe(arr[0]) + // quotes + res = filter.call(vm, arr, "'hello'", "'b.c'") + expect(res.length).toBe(1) + expect(res[0]).toBe(arr[0]) + // delimiter + res = filter.call(vm, arr, 'search.key', 'in', 'search.datakey') + expect(res.length).toBe(1) + expect(res[0]).toBe(arr[0]) + // no search key + res = filter.call(vm, arr, 'abc') + expect(res).toBe(arr) + }) + + it('orderBy', function () { + var filter = filters.orderBy + var arr = [ + { a: { b: 0 }, c: 'b'}, + { a: { b: 2 }, c: 'c'}, + { a: { b: 1 }, c: 'a'} + ] + var res + // sort key + res = filter.call(new Vue({ + data: { + sortby: 'a.b', + } + }), arr, 'sortby') + expect(res.length).toBe(3) + expect(res[0].a.b).toBe(0) + expect(res[1].a.b).toBe(1) + expect(res[2].a.b).toBe(2) + // reverse key + res = filter.call(new Vue({ + data: { sortby: 'a.b', reverse: true } + }), arr, 'sortby', 'reverse') + expect(res.length).toBe(3) + expect(res[0].a.b).toBe(2) + expect(res[1].a.b).toBe(1) + expect(res[2].a.b).toBe(0) + // literal args + res = filter.call(new Vue(), arr, "'c'", '-1') + expect(res.length).toBe(3) + expect(res[0].c).toBe('c') + expect(res[1].c).toBe('b') + expect(res[2].c).toBe('a') + // negate reverse + res = filter.call(new Vue({ + data: { reverse: true } + }), arr, "'c'", '!reverse') + expect(res.length).toBe(3) + expect(res[0].c).toBe('a') + expect(res[1].c).toBe('b') + expect(res[2].c).toBe('c') + // no sort key + res = filter.call(new Vue(), arr, 'abc') + expect(res).toBe(arr) + }) +}) + +function assertNumberAndFalsy (filter) { + // should stringify numbers + expect(filter(12345)).toBe('12345') + expect(filter(0)).toBe('0') + expect(filter(undefined)).toBe('') + expect(filter(null)).toBe('') + expect(filter(false)).toBe('') +} \ No newline at end of file diff --git a/test/unit/specs/util/lang_spec.js b/test/unit/specs/util/lang_spec.js index c089373de20..1d8065d85e9 100644 --- a/test/unit/specs/util/lang_spec.js +++ b/test/unit/specs/util/lang_spec.js @@ -17,6 +17,12 @@ describe('Util - Language Enhancement', function () { expect(_.guardNumber('hello')).toBe('hello') }) + it('strip quotes', function () { + expect(_.stripQuotes('"123"')).toBe('123') + expect(_.stripQuotes("'fff'")).toBe('fff') + expect(_.stripQuotes("'fff")).toBe(false) + }) + it('bind', function () { var original = function (a) { return this.a + a From e480368414bf32d547500c10887e554f48cddb34 Mon Sep 17 00:00:00 2001 From: Evan You Date: Tue, 19 Aug 2014 14:29:44 -0400 Subject: [PATCH 0166/1534] micro perf tuning --- src/emitter.js | 29 ++++++++++++++--------------- src/filters/array-filters.js | 4 +++- src/observe/array-augmentations.js | 9 +++++++-- src/util/lang.js | 13 +++++++++---- 4 files changed, 33 insertions(+), 22 deletions(-) diff --git a/src/emitter.js b/src/emitter.js index 04284e9a498..b76ac5c1fdb 100644 --- a/src/emitter.js +++ b/src/emitter.js @@ -1,3 +1,5 @@ +var _ = require('./util') + /** * Simple event emitter based on component/emitter. * @@ -38,12 +40,10 @@ p.on = function (event, fn) { p.once = function (event, fn) { var self = this this._cbs = this._cbs || {} - function on () { self.off(event, on) fn.apply(this, arguments) } - on.fn = fn this.on(event, on) return this @@ -60,23 +60,19 @@ p.once = function (event, fn) { p.off = function (event, fn) { this._cbs = this._cbs || {} - // all if (!arguments.length) { this._cbs = {} return this } - // specific event var callbacks = this._cbs[event] if (!callbacks) return this - // remove all handlers if (arguments.length === 1) { delete this._cbs[event] return this } - // remove specific handler var cb for (var i = 0; i < callbacks.length; i++) { @@ -100,14 +96,12 @@ p.off = function (event, fn) { p.emit = function (event, a, b, c) { this._cbs = this._cbs || {} var callbacks = this._cbs[event] - if (callbacks) { - callbacks = callbacks.slice(0) - for (var i = 0, len = callbacks.length; i < len; i++) { + callbacks = _.toArray(callbacks) + for (var i = 0, l = callbacks.length; i < l; i++) { callbacks[i].call(this._ctx, a, b, c) } } - return this } @@ -122,15 +116,20 @@ p.emit = function (event, a, b, c) { p.applyEmit = function (event) { this._cbs = this._cbs || {} var callbacks = this._cbs[event], args - if (callbacks) { - callbacks = callbacks.slice(0) - args = callbacks.slice.call(arguments, 1) - for (var i = 0, len = callbacks.length; i < len; i++) { + // avoid leaking arguments: + // http://jsperf.com/closure-with-arguments + var i + var l = arguments.length + var args = new Array(l - 1) + for (i = 1; i < l; i++) { + args[i - 1] = arguments[i] + } + callbacks = _.toArray(callbacks) + for (i = 0, l = callbacks.length; i < l; i++) { callbacks[i].apply(this._ctx, args) } } - return this } diff --git a/src/filters/array-filters.js b/src/filters/array-filters.js index 203d6e2fa2b..064eed8e533 100644 --- a/src/filters/array-filters.js +++ b/src/filters/array-filters.js @@ -23,7 +23,9 @@ exports._objToArray = function (obj) { return } var res = [] - for (var key in obj) { + var keys = Object.keys(obj) + for (var i = 0, l = keys.length, key; i < l; i++) { + key = keys[i] res.push({ key: key, value: obj[key] diff --git a/src/observe/array-augmentations.js b/src/observe/array-augmentations.js index e3b2d2c4cb3..482a376ba60 100644 --- a/src/observe/array-augmentations.js +++ b/src/observe/array-augmentations.js @@ -19,8 +19,13 @@ var arrayAugmentations = Object.create(Array.prototype) var original = Array.prototype[method] // define wrapped method _.define(arrayAugmentations, method, function () { - - var args = _.toArray(arguments) + // avoid leaking arguments: + // http://jsperf.com/closure-with-arguments + var l = arguments.length + var args = new Array(l) + for (var i = 0; i < l; i++) { + args[i] = arguments[i] + } var result = original.apply(this, args) var ob = this.$observer var inserted, removed, index diff --git a/src/util/lang.js b/src/util/lang.js index 9821a33b48a..4e35855ec19 100644 --- a/src/util/lang.js +++ b/src/util/lang.js @@ -62,13 +62,18 @@ exports.bind = function (fn, ctx) { * Convert an Array-like object to a real Array. * * @param {Array-like} list - * @param {Number} [i] - start index + * @param {Number} [start] - start index * @return {Array} */ -var slice = [].slice -exports.toArray = function (list, i) { - return slice.call(list, i || 0) +exports.toArray = function (list, start) { + start = start || 0 + var l = list.length + var ret = new Array(l - start) + for (var i = start; i < l; i++) { + ret[i - start] = list[i] + } + return ret } /** From 9255cbae59a5dbe2585f5f9b9a5da6823ad80826 Mon Sep 17 00:00:00 2001 From: Evan You Date: Tue, 19 Aug 2014 17:33:01 -0400 Subject: [PATCH 0167/1534] use better util method names --- src/directives/html.js | 2 +- src/directives/text.js | 2 +- src/directives/with.js | 4 +++- src/util/lang.js | 9 +++++---- test/unit/specs/batcher_spec.js | 9 --------- test/unit/specs/util/lang_spec.js | 23 ++++++++++++----------- 6 files changed, 22 insertions(+), 27 deletions(-) diff --git a/src/directives/html.js b/src/directives/html.js index 46065ca5362..11e1f550b17 100644 --- a/src/directives/html.js +++ b/src/directives/html.js @@ -13,7 +13,7 @@ module.exports = { }, update: function (value) { - value = _.guard(value) + value = _.toString(value) if (this.nodes) { this.swap(value) } else { diff --git a/src/directives/text.js b/src/directives/text.js index 160e11a06fc..16503220697 100644 --- a/src/directives/text.js +++ b/src/directives/text.js @@ -9,7 +9,7 @@ module.exports = { }, update: function (value) { - this.el[this.attr] = _.guard(value) + this.el[this.attr] = _.toString(value) } } \ No newline at end of file diff --git a/src/directives/with.js b/src/directives/with.js index 7d430dfc746..ac1bb6bc6cc 100644 --- a/src/directives/with.js +++ b/src/directives/with.js @@ -1,3 +1,5 @@ +var _ = require('../util') + module.exports = { priority: 900, @@ -6,7 +8,7 @@ module.exports = { var self = this var path = this.arg || '$data' this.vm.$watch(path, function (val) { - self.set(val) + self.set(_.toNumber(val)) }) }, diff --git a/src/util/lang.js b/src/util/lang.js index 4e35855ec19..90351d00f3b 100644 --- a/src/util/lang.js +++ b/src/util/lang.js @@ -6,10 +6,11 @@ * @return {String} */ -exports.guard = function (value) { - return value === undefined +exports.toString = function (value) { + /* jshint eqeqeq:false */ + return value == null ? '' - : value + : value.toString() } /** @@ -20,7 +21,7 @@ exports.guard = function (value) { * @return {*|Number} */ -exports.guardNumber = function (value) { +exports.toNumber = function (value) { return ( isNaN(value) || value === null || diff --git a/test/unit/specs/batcher_spec.js b/test/unit/specs/batcher_spec.js index 8739ee75f31..b567cb4356b 100644 --- a/test/unit/specs/batcher_spec.js +++ b/test/unit/specs/batcher_spec.js @@ -53,13 +53,4 @@ describe('Batcher', function () { }) }) - it('preFlush hook', function (done) { - batcher._preFlush = spy - batcher.push({ run: function () {}}) - nextTick(function () { - expect(spy.calls.count()).toBe(1) - done() - }) - }) - }) \ No newline at end of file diff --git a/test/unit/specs/util/lang_spec.js b/test/unit/specs/util/lang_spec.js index 1d8065d85e9..8cae4815117 100644 --- a/test/unit/specs/util/lang_spec.js +++ b/test/unit/specs/util/lang_spec.js @@ -2,19 +2,20 @@ var _ = require('../../../../src/util') describe('Util - Language Enhancement', function () { - it('guard', function () { - expect(_.guard(1)).toBe(1) - expect(_.guard(null)).toBe(null) - expect(_.guard(undefined)).toBe('') + it('toString', function () { + expect(_.toString('hi')).toBe('hi') + expect(_.toString(1.234)).toBe('1.234') + expect(_.toString(null)).toBe('') + expect(_.toString(undefined)).toBe('') }) - it('guardNumber', function () { - expect(_.guardNumber('12')).toBe(12) - expect(_.guardNumber('1e5')).toBe(1e5) - expect(_.guardNumber('0x2F')).toBe(0x2F) - expect(_.guardNumber(null)).toBe(null) - expect(_.guardNumber(true)).toBe(true) - expect(_.guardNumber('hello')).toBe('hello') + it('toNumber', function () { + expect(_.toNumber('12')).toBe(12) + expect(_.toNumber('1e5')).toBe(1e5) + expect(_.toNumber('0x2F')).toBe(0x2F) + expect(_.toNumber(null)).toBe(null) + expect(_.toNumber(true)).toBe(true) + expect(_.toNumber('hello')).toBe('hello') }) it('strip quotes', function () { From 9ea607180e6b123a0b24ca83845f0b7db30e764b Mon Sep 17 00:00:00 2001 From: Evan You Date: Tue, 19 Aug 2014 20:18:16 -0400 Subject: [PATCH 0168/1534] css transitions --- changes.md | 16 +++- gruntfile.js | 2 +- src/directives/transition.js | 5 +- src/emitter.js | 2 +- src/instance/compile.js | 28 +++++- src/transition/css.js | 128 +++++++++++++++++++++++++ src/transition/index.js | 111 ++++++++++++++++++--- src/transition/js.js | 3 + src/util/env.js | 49 +++++----- src/watcher.js | 3 +- test/.jshintrc | 10 +- test/unit/specs/batcher_spec.js | 9 ++ test/unit/specs/emitter_spec.js | 3 +- test/unit/specs/instance/scope_spec.js | 25 +++-- test/unit/specs/observer_spec.js | 55 +++++------ test/unit/specs/util/lang_spec.js | 2 +- test/unit/specs/util/option_spec.js | 2 +- 17 files changed, 355 insertions(+), 98 deletions(-) diff --git a/changes.md b/changes.md index 63fcd81db8f..8dad52f8203 100644 --- a/changes.md +++ b/changes.md @@ -157,4 +157,18 @@ By default the callback only fires when the value changes. If you want it to be ``` js vm.$watch('a', callback, true) // callback is fired immediately with current value of `a` -``` \ No newline at end of file +``` + +## Simplified Transition API + +- no more distinctions between `v-transition`, `v-animation` or `v-effect`; +- no more configuring enter/leave classes in `Vue.config`; +- `Vue.effect` has been replaced with `Vue.transition`, the `effects` option has also been replaced by `transitions`. + +With `v-transition="my-transition"`, Vue will: + +1. Try to find a transition definition object registered either through `Vue.transition(id, def)` or passed in with the `transitions` option, with the id `"my-transition"`. If it finds it, it will use that definition object to perform the custom JavaScript based transition. + +2. If no custom JavaScript transition is found, it will automatically sniff whether the target element has CSS transitions or CSS animations applied, and add/remove the classes as before. + +3. If no transitions/animations are detected, the DOM manipulation is executed immediately instead of hung up waiting for an event. \ No newline at end of file diff --git a/gruntfile.js b/gruntfile.js index 2796f38be5f..6f1841960ab 100644 --- a/gruntfile.js +++ b/gruntfile.js @@ -22,7 +22,7 @@ module.exports = function (grunt) { src: 'src/**/*.js' }, test: { - src: 'test/**/*.js' + src: ['test/unit/specs/**/*.js'] } }, diff --git a/src/directives/transition.js b/src/directives/transition.js index 167c606fc1d..3c2472fef3d 100644 --- a/src/directives/transition.js +++ b/src/directives/transition.js @@ -1,9 +1,12 @@ module.exports = { priority: 1000, + isLiteral: true, bind: function () { - this.el.__vueTransition = this.expression + this.el.__v_trans = { + id: this.expression + } } } \ No newline at end of file diff --git a/src/emitter.js b/src/emitter.js index b76ac5c1fdb..8752c18165d 100644 --- a/src/emitter.js +++ b/src/emitter.js @@ -115,7 +115,7 @@ p.emit = function (event, a, b, c) { p.applyEmit = function (event) { this._cbs = this._cbs || {} - var callbacks = this._cbs[event], args + var callbacks = this._cbs[event] if (callbacks) { // avoid leaking arguments: // http://jsperf.com/closure-with-arguments diff --git a/src/instance/compile.js b/src/instance/compile.js index c6b62a64156..989950a8495 100644 --- a/src/instance/compile.js +++ b/src/instance/compile.js @@ -39,7 +39,7 @@ exports._compileNode = function (node) { } /** - * Compile an Element + * Compile an Element. * * @param {Element} node */ @@ -79,7 +79,10 @@ exports._compileElement = function (node) { } /** - * Compile attribtues on an Element + * Compile attribtues on an Element. + * Collect directives, sort them by priority, + * then bind them. Also check normal directives for + * param attributes and interpolation bindings. * * @param {Element} node */ @@ -124,7 +127,13 @@ exports._compileAttrs = function (node) { } /** - * Compile a TextNode + * Compile a TextNode. + * Possible interpolations include: + * + * - normal text binding, e.g. {{text}} + * - unescaped html binding, e.g. {{{html}}} + * - one-time text binding, e.g. {{*text}} + * - one-time html binding, e.g. {{{*html}}} * * @param {TextNode} node */ @@ -166,7 +175,12 @@ exports._compileTextNode = function (node) { /** * Check for priority directives that would potentially - * skip other directives. + * skip other directives. Each priority directive, once + * detected, will cause the compilation to skip the rest + * and let that directive handle the rest. This allows, + * for example, "v-repeat" to handle how it should handle + * the situation when "v-component" is also present, and + * "v-component" won't have to worry about that. * * @param {Element} node * @return {Boolean} @@ -212,7 +226,11 @@ exports._bindDirective = function (name, value, node) { } /** - * Check an attribute for potential bindings. + * Check a normal attribute for bindings. + * A normal attribute could potentially be: + * + * - a param attribute (only on root nodes) + * - an interpolated attribute, e.g. attr="{{data}}" */ exports._bindAttr = function (node, attr) { diff --git a/src/transition/css.js b/src/transition/css.js index e69de29bb2d..21f32fa498c 100644 --- a/src/transition/css.js +++ b/src/transition/css.js @@ -0,0 +1,128 @@ +var _ = require('../util') +var Batcher = require('../batcher') +var batcher = new Batcher() +var transDurationProp = _.transitionProp + 'Duration' +var animDurationProp = _.animationProp + 'Duration' + +/** + * Force layout before triggering transitions/animations + */ + +batcher._preFlush = function () { + /* jshint unused: false */ + var f = document.body.offsetHeight +} + +/** + * Get an element's transition type based on the + * calculated styles + * + * @param {Element} el + * @return {Number} + * 1 - transition + * 2 - animation + */ + +function getTransitionType (el) { + var styles = window.getComputedStyle(el) + if (styles[transDurationProp] !== '0s') { + return 1 + } else if (styles[animDurationProp] !== '0s') { + return 2 + } +} + +/** + * Apply CSS transition to an element. + * + * @param {Element} el + * @param {Number} direction - 1: enter, -1: leave + * @param {Function} op - the actual DOM operation + * @param {Object} data - target element's transition data + */ + +module.exports = function (el, direction, op, data) { + var classList = el.classList + var callback = data.callback + var prefix = data.id || 'v' + var enterClass = prefix + '-enter' + var leaveClass = prefix + '-leave' + // clean up potential previously running transitions + if (data.callback) { + el.removeEventListener(data.event, callback) + classList.remove(enterClass) + classList.remove(leaveClass) + data.event = data.callback = null + } + var transitionType, onEnd, endEvent + + if (direction > 0) { // enter + + classList.add(enterClass) + op() + transitionType = getTransitionType(el) + if (transitionType === 1) { + // Enter Transition + // + // We need to force a reflow to have the enterClass + // applied before removing it to trigger the + // transition, so they are batched to make sure + // there's only one reflow for everything. + batcher.push({ + run: function () { + classList.remove(enterClass) + } + }) + } else if (transitionType === 2) { + // Enter Animation + // + // Animations are triggered automatically as the + // element is inserted into the DOM, so we just + // listen for the animationend event. + endEvent = data.event = _.animationEndEvent + onEnd = data.callback = function (e) { + if (e.target === el) { + el.removeEventListener(endEvent, onEnd) + data.event = data.callback = null + classList.remove(enterClass) + } + } + el.addEventListener(endEvent, onEnd) + } + + } else { // leave + + transitionType = getTransitionType(el) + if ( + transitionType && + (el.offsetWidth || el.offsetHeight) + ) { + // Leave Transition/Animation + // + // We push it to the batcher to ensure it triggers + // in the same frame with other enter transitions + // happening at the same time. + batcher.push({ + run: function () { + classList.add(leaveClass) + } + }) + endEvent = data.event = transitionType === 1 + ? _.transitionEndEvent + : _.animationEndEvent + onEnd = data.callback = function (e) { + if (e.target === el) { + el.removeEventListener(endEvent, onEnd) + data.event = data.callback = null + // actually remove node here + op() + classList.remove(leaveClass) + } + } + el.addEventListener(endEvent, onEnd) + } else { + op() + } + + } +} \ No newline at end of file diff --git a/src/transition/index.js b/src/transition/index.js index 17ab6da5e3d..1b4a3c5a7e9 100644 --- a/src/transition/index.js +++ b/src/transition/index.js @@ -1,28 +1,115 @@ var _ = require('../util') +var applyCSSTransition = require('./css') +var applyJSTransition = require('./js') -// TODO -// placeholder for testing +/** + * Append with transition. + * + * @oaram {Element} el + * @param {Element} target + * @param {Function} [cb] + * @param {Vue} vm + */ exports.append = function (el, target, cb, vm) { - target.appendChild(el) - cb && cb() + apply(el, 1, function () { + target.appendChild(el) + if (cb) cb() + }, vm) } +/** + * InsertBefore with transition. + * + * @oaram {Element} el + * @param {Element} target + * @param {Function} [cb] + * @param {Vue} vm + */ + exports.before = function (el, target, cb, vm) { - _.before(el, target) - cb && cb() + apply(el, 1, function () { + _.before(el, target) + if (cb) cb() + }, vm) } +/** + * Remove with transition. + * + * @oaram {Element} el + * @param {Function} [cb] + * @param {Vue} vm + */ + exports.remove = function (el, cb, vm) { - _.remove(el) - cb && cb() + apply(el, -1, function () { + _.remove(el) + if (cb) cb() + }, vm) } +/** + * Remove by appending to another parent with transition. + * + * @oaram {Element} el + * @param {Element} target + * @param {Function} [cb] + * @param {Vue} vm + */ + exports.removeThenAppend = function (el, target, cb, vm) { - target.appendChild(el) - cb && cb() + apply(el, -1, function () { + target.appendChild(el) + if (cb) cb() + }, vm) } -exports.apply = function (el, direction, cb, vm) { - +/** + * Apply transitions with an operation callback. + * + * @oaram {Element} el + * @param {Number} direction + * 1: enter + * -1: leave + * @param {Function} op - the actual DOM operation + * @param {Vue} vm + */ + +var apply = exports.apply = function (el, direction, op, vm) { + function applyOp () { + op() + vm._callHook(direction > 0 ? 'attached' : 'detached') + } + // if the vm is being manipulated by a parent directive + // during the parent's compilation phase, we skip the + // animation. + if (vm.$parent && !vm.$parent._isCompiled) { + applyOp() + return + } + // determine the transition type on the element + var transData = el.__v_trans + var registry = vm.$options.transitions + var jsTransition = transData && registry[transData.id] + if (jsTransition) { + // js + applyJSTransition( + el, + direction, + applyOp, + jsTransition + ) + } else if (transData && _.transitionEndEvent) { + // css + applyCSSTransition( + el, + direction, + applyOp, + transData + ) + } else { + // not applicable + applyOp() + } } \ No newline at end of file diff --git a/src/transition/js.js b/src/transition/js.js index e69de29bb2d..67f7bb58eb3 100644 --- a/src/transition/js.js +++ b/src/transition/js.js @@ -0,0 +1,3 @@ +module.exports = function () { + +} \ No newline at end of file diff --git a/src/util/env.js b/src/util/env.js index e00450618d2..3af079859e4 100644 --- a/src/util/env.js +++ b/src/util/env.js @@ -1,7 +1,5 @@ /** - * Are we in a browser or in Node? - * Calling toString on window has inconsistent results in - * browsers so we do it on the document instead. + * Indicates we have a window * * @type {Boolean} */ @@ -43,30 +41,29 @@ exports.isIE9 = navigator.userAgent.indexOf('MSIE 9.0') > 0 /** - * Detect transition and animation end events. + * Sniff transition/animation events */ -var testElement = inBrowser - ? document.createElement('div') - : undefined - -exports.transitionEndEvent = (function () { - if (inBrowser) { - var map = { - 'webkitTransition' : 'webkitTransitionEnd', - 'transition' : 'transitionend', - 'mozTransition' : 'transitionend' - } - for (var prop in map) { - if (testElement.style[prop] !== undefined) { - return map[prop] - } - } +if (inBrowser) { + if ( + window.ontransitionend === undefined && + window.onwebkittransitionend !== undefined + ) { + exports.transitionProp = 'WebkitTransition' + exports.transitionEndEvent = 'webkitTransitionEnd' + } else { + exports.transitionProp = 'transition' + exports.transitionEndEvent = 'transitionend' } -})() -exports.animationEndEvent = inBrowser - ? testElement.style.animation !== undefined - ? 'animationend' - : 'webkitAnimationEnd' - : undefined \ No newline at end of file + if ( + window.onanimationend === undefined && + window.onwebkitanimationend !== undefined + ) { + exports.animationProp = 'WebkitAnimation' + exports.animationEndEvent = 'webkitAnimationEnd' + } else { + exports.animationProp = 'animation' + exports.animationEndEvent = 'animationend' + } +} \ No newline at end of file diff --git a/src/watcher.js b/src/watcher.js index 336c9d788ba..60cf1ae9f41 100644 --- a/src/watcher.js +++ b/src/watcher.js @@ -71,10 +71,11 @@ p.initDeps = function (res) { this.isComputed = true this.value = this.get() var computed = this.vm.$options.computed + var exp = this.expression this.isComputed = this.filters || // filters may access instance data res.computed || // inline expression - (computed && computed[expression]) // computed property + (computed && computed[exp]) // computed property } /** diff --git a/test/.jshintrc b/test/.jshintrc index b438e2c635e..b1f09cbee7f 100644 --- a/test/.jshintrc +++ b/test/.jshintrc @@ -4,12 +4,13 @@ "asi": true, "multistr": true, "undef": true, - "unused": true, + "unused": false, "trailing": true, "sub": true, "node": true, "laxbreak": true, "evil": true, + "proto": true, "globals": { "console": true, "it": true, @@ -17,12 +18,13 @@ "beforeEach": true, "afterEach": true, "expect": true, - "mock": true, + "jasmine": true, + "spyOn": true, "Vue": true, - "$": true, "mockHTMLEvent": true, "mockMouseEvent": true, "mockKeyEvent": true, - "casper": true + "casper": true, + "DocumentFragment": true } } \ No newline at end of file diff --git a/test/unit/specs/batcher_spec.js b/test/unit/specs/batcher_spec.js index b567cb4356b..8739ee75f31 100644 --- a/test/unit/specs/batcher_spec.js +++ b/test/unit/specs/batcher_spec.js @@ -53,4 +53,13 @@ describe('Batcher', function () { }) }) + it('preFlush hook', function (done) { + batcher._preFlush = spy + batcher.push({ run: function () {}}) + nextTick(function () { + expect(spy.calls.count()).toBe(1) + done() + }) + }) + }) \ No newline at end of file diff --git a/test/unit/specs/emitter_spec.js b/test/unit/specs/emitter_spec.js index 73bc688469a..6b05b1074e2 100644 --- a/test/unit/specs/emitter_spec.js +++ b/test/unit/specs/emitter_spec.js @@ -1,5 +1,4 @@ var Emitter = require('../../../src/emitter') -var u = undefined describe('Emitter', function () { @@ -41,7 +40,7 @@ describe('Emitter', function () { e.emit('test1', 1) e.emit('test2', 2) expect(spy.calls.count()).toBe(1) - expect(spy).toHaveBeenCalledWith(2, u, u) + expect(spy).toHaveBeenCalledWith(2, undefined, undefined) }) it('off event + fn', function () { diff --git a/test/unit/specs/instance/scope_spec.js b/test/unit/specs/instance/scope_spec.js index fbd304d954a..ecf745eb1aa 100644 --- a/test/unit/specs/instance/scope_spec.js +++ b/test/unit/specs/instance/scope_spec.js @@ -5,7 +5,6 @@ var Vue = require('../../../../src/vue') var Observer = require('../../../../src/observe/observer') -var u = undefined Observer.pathDelimiter = '.' describe('Scope', function () { @@ -38,12 +37,12 @@ describe('Scope', function () { // set on scope vm.$scope.a = 2 expect(spy.calls.count()).toBe(1) - expect(spy).toHaveBeenCalledWith('a', 2, u) + expect(spy).toHaveBeenCalledWith('a', 2, undefined) // set on vm vm.b.c = 3 expect(spy.calls.count()).toBe(2) - expect(spy).toHaveBeenCalledWith('b.c', 3, u) + expect(spy).toHaveBeenCalledWith('b.c', 3, undefined) }) it('should trigger add/delete events', function () { @@ -55,12 +54,12 @@ describe('Scope', function () { // add on scope vm.$scope.$add('c', 123) expect(spy.calls.count()).toBe(1) - expect(spy).toHaveBeenCalledWith('c', 123, u) + expect(spy).toHaveBeenCalledWith('c', 123, undefined) // delete on scope vm.$scope.$delete('c') expect(spy.calls.count()).toBe(2) - expect(spy).toHaveBeenCalledWith('c', u, u) + expect(spy).toHaveBeenCalledWith('c', undefined, undefined) // vm $add/$delete are tested in the api suite }) @@ -162,19 +161,19 @@ describe('Scope', function () { child.$observer.on('set', spy) parent.c = 'c changed' expect(spy.calls.count()).toBe(1) - expect(spy).toHaveBeenCalledWith('c', 'c changed', u) + expect(spy).toHaveBeenCalledWith('c', 'c changed', undefined) spy = jasmine.createSpy('inheritance') child.$observer.on('add', spy) parent.$scope.$add('e', 123) expect(spy.calls.count()).toBe(1) - expect(spy).toHaveBeenCalledWith('e', 123, u) + expect(spy).toHaveBeenCalledWith('e', 123, undefined) spy = jasmine.createSpy('inheritance') child.$observer.on('delete', spy) parent.$scope.$delete('e') expect(spy.calls.count()).toBe(1) - expect(spy).toHaveBeenCalledWith('e', u, u) + expect(spy).toHaveBeenCalledWith('e', undefined, undefined) spy = jasmine.createSpy('inheritance') child.$observer.on('mutate', spy) @@ -221,11 +220,11 @@ describe('Scope', function () { expect(parent.arr[0].a).toBe(3) expect(parentSpy.calls.count()).toBe(1) - expect(parentSpy).toHaveBeenCalledWith('arr.0.a', 3, u) + expect(parentSpy).toHaveBeenCalledWith('arr.0.a', 3, undefined) expect(childSpy.calls.count()).toBe(2) - expect(childSpy).toHaveBeenCalledWith('a', 3, u) - expect(childSpy).toHaveBeenCalledWith('arr.0.a', 3, u) + expect(childSpy).toHaveBeenCalledWith('a', 3, undefined) + expect(childSpy).toHaveBeenCalledWith('arr.0.a', 3, undefined) }) }) @@ -250,8 +249,8 @@ describe('Scope', function () { expect(vm._data).toBe(newData) expect(vm.a).toBe(2) expect(vm.b).toBe(3) - expect(vmSpy).toHaveBeenCalledWith('a', 2, u) - expect(vmAddSpy).toHaveBeenCalledWith('b', 3, u) + expect(vmSpy).toHaveBeenCalledWith('a', 2, undefined) + expect(vmAddSpy).toHaveBeenCalledWith('b', 3, undefined) }) it('should unsync old data', function () { diff --git a/test/unit/specs/observer_spec.js b/test/unit/specs/observer_spec.js index 065f2c7fe05..9ed775f71ba 100644 --- a/test/unit/specs/observer_spec.js +++ b/test/unit/specs/observer_spec.js @@ -4,9 +4,6 @@ var Observer = require('../../../src/observe/observer') var _ = require('../../../src/util') -// internal emitter has fixed 3 arguments -// so we need to fill up the assetions with undefined -var u = undefined Observer.pathDelimiter = '.' describe('Observer', function () { @@ -30,12 +27,12 @@ describe('Observer', function () { ob.on('get', spy) var t = obj.a - expect(spy).toHaveBeenCalledWith('a', u, u) + expect(spy).toHaveBeenCalledWith('a', undefined, undefined) expect(spy.calls.count()).toBe(1) t = obj.b.c - expect(spy).toHaveBeenCalledWith('b', u, u) - expect(spy).toHaveBeenCalledWith('b.c', u, u) + expect(spy).toHaveBeenCalledWith('b', undefined, undefined) + expect(spy).toHaveBeenCalledWith('b.c', undefined, undefined) expect(spy.calls.count()).toBe(3) Observer.emitGet = false @@ -52,17 +49,17 @@ describe('Observer', function () { ob.on('set', spy) obj.a = 3 - expect(spy).toHaveBeenCalledWith('a', 3, u) + expect(spy).toHaveBeenCalledWith('a', 3, undefined) expect(spy.calls.count()).toBe(1) obj.b.c = 4 - expect(spy).toHaveBeenCalledWith('b.c', 4, u) + expect(spy).toHaveBeenCalledWith('b.c', 4, undefined) expect(spy.calls.count()).toBe(2) // swap set var newB = { c: 5 } obj.b = newB - expect(spy).toHaveBeenCalledWith('b', newB, u) + expect(spy).toHaveBeenCalledWith('b', newB, undefined) expect(spy.calls.count()).toBe(3) // same value set should not emit events @@ -116,8 +113,8 @@ describe('Observer', function () { ob.on('get', spy) var t = obj.arr[0].a - expect(spy).toHaveBeenCalledWith('arr', u, u) - expect(spy).toHaveBeenCalledWith('arr.0.a', u, u) + expect(spy).toHaveBeenCalledWith('arr', undefined, undefined) + expect(spy).toHaveBeenCalledWith('arr.0.a', undefined, undefined) expect(spy.calls.count()).toBe(2) Observer.emitGet = false @@ -131,12 +128,12 @@ describe('Observer', function () { ob.on('set', spy) obj.arr[0].a = 2 - expect(spy).toHaveBeenCalledWith('arr.0.a', 2, u) + expect(spy).toHaveBeenCalledWith('arr.0.a', 2, undefined) // set events after mutation obj.arr.reverse() obj.arr[0].a = 3 - expect(spy).toHaveBeenCalledWith('arr.0.a', 3, u) + expect(spy).toHaveBeenCalledWith('arr.0.a', 3, undefined) }) it('array push', function () { @@ -156,7 +153,7 @@ describe('Observer', function () { // test index update after mutation ob.on('set', spy) arr[2].a = 4 - expect(spy).toHaveBeenCalledWith('2.a', 4, u) + expect(spy).toHaveBeenCalledWith('2.a', 4, undefined) }) it('array pop', function () { @@ -194,7 +191,7 @@ describe('Observer', function () { // test index update after mutation ob.on('set', spy) arr[0].a = 4 - expect(spy).toHaveBeenCalledWith('0.a', 4, u) + expect(spy).toHaveBeenCalledWith('0.a', 4, undefined) }) it('array unshift', function () { @@ -215,7 +212,7 @@ describe('Observer', function () { // test index update after mutation ob.on('set', spy) arr[1].a = 4 - expect(spy).toHaveBeenCalledWith('1.a', 4, u) + expect(spy).toHaveBeenCalledWith('1.a', 4, undefined) }) it('array splice', function () { @@ -238,7 +235,7 @@ describe('Observer', function () { // test index update after mutation ob.on('set', spy) arr[1].a = 4 - expect(spy).toHaveBeenCalledWith('1.a', 4, u) + expect(spy).toHaveBeenCalledWith('1.a', 4, undefined) }) it('array sort', function () { @@ -259,7 +256,7 @@ describe('Observer', function () { // test index update after mutation ob.on('set', spy) arr[1].a = 4 - expect(spy).toHaveBeenCalledWith('1.a', 4, u) + expect(spy).toHaveBeenCalledWith('1.a', 4, undefined) }) it('array reverse', function () { @@ -278,7 +275,7 @@ describe('Observer', function () { // test index update after mutation ob.on('set', spy) arr[1].a = 4 - expect(spy).toHaveBeenCalledWith('1.a', 4, u) + expect(spy).toHaveBeenCalledWith('1.a', 4, undefined) }) it('object.$add', function () { @@ -293,12 +290,12 @@ describe('Observer', function () { // add event var add = {d:2} obj.a.$add('c', add) - expect(spy).toHaveBeenCalledWith('a.c', add, u) + expect(spy).toHaveBeenCalledWith('a.c', add, undefined) // check if add object is properly observed ob.on('set', spy) obj.a.c.d = 3 - expect(spy).toHaveBeenCalledWith('a.c.d', 3, u) + expect(spy).toHaveBeenCalledWith('a.c.d', 3, undefined) }) it('object.$delete', function () { @@ -311,7 +308,7 @@ describe('Observer', function () { expect(spy.calls.count()).toBe(0) obj.a.$delete('b') - expect(spy).toHaveBeenCalledWith('a.b', u, u) + expect(spy).toHaveBeenCalledWith('a.b', undefined, undefined) }) it('array.$set', function () { @@ -335,7 +332,7 @@ describe('Observer', function () { ob.on('set', spy) arr[1].a = 4 - expect(spy).toHaveBeenCalledWith('1.a', 4, u) + expect(spy).toHaveBeenCalledWith('1.a', 4, undefined) }) it('array.$set with out of bound length', function () { @@ -366,7 +363,7 @@ describe('Observer', function () { ob.on('set', spy) arr[0].a = 3 - expect(spy).toHaveBeenCalledWith('0.a', 3, u) + expect(spy).toHaveBeenCalledWith('0.a', 3, undefined) }) it('array.$remove object', function () { @@ -387,7 +384,7 @@ describe('Observer', function () { ob.on('set', spy) arr[0].a = 3 - expect(spy).toHaveBeenCalledWith('0.a', 3, u) + expect(spy).toHaveBeenCalledWith('0.a', 3, undefined) }) it('shared observe', function () { @@ -400,14 +397,14 @@ describe('Observer', function () { obB.on('set', spy) obj.a = 2 expect(spy.calls.count()).toBe(2) - expect(spy).toHaveBeenCalledWith('child1.a', 2, u) - expect(spy).toHaveBeenCalledWith('child2.a', 2, u) + expect(spy).toHaveBeenCalledWith('child1.a', 2, undefined) + expect(spy).toHaveBeenCalledWith('child2.a', 2, undefined) // test unobserve parentA.child1 = null obj.a = 3 expect(spy.calls.count()).toBe(4) - expect(spy).toHaveBeenCalledWith('child1', null, u) - expect(spy).toHaveBeenCalledWith('child2.a', 3, u) + expect(spy).toHaveBeenCalledWith('child1', null, undefined) + expect(spy).toHaveBeenCalledWith('child2.a', 3, undefined) }) }) \ No newline at end of file diff --git a/test/unit/specs/util/lang_spec.js b/test/unit/specs/util/lang_spec.js index 8cae4815117..b50db9d9f6f 100644 --- a/test/unit/specs/util/lang_spec.js +++ b/test/unit/specs/util/lang_spec.js @@ -135,7 +135,7 @@ describe('Util - Language Enhancement', function () { _.define(obj, 'test2', 123, true) expect(obj.test2).toBe(123) - var desc = Object.getOwnPropertyDescriptor(obj, 'test2') + desc = Object.getOwnPropertyDescriptor(obj, 'test2') expect(desc.enumerable).toBe(true) }) diff --git a/test/unit/specs/util/option_spec.js b/test/unit/specs/util/option_spec.js index c837c16eb5b..56abc9f9ad1 100644 --- a/test/unit/specs/util/option_spec.js +++ b/test/unit/specs/util/option_spec.js @@ -9,7 +9,7 @@ describe('Util - Option merging', function () { var res = merge({replace:true}, {}).replace expect(res).toBe(true) // child overwrite - var res = merge({replace:true}, {replace:false}).replace + res = merge({replace:true}, {replace:false}).replace expect(res).toBe(false) }) From fae4d6d77a61170f8822f25d6e07ccaf31bb5e9e Mon Sep 17 00:00:00 2001 From: Evan You Date: Tue, 19 Aug 2014 20:42:49 -0400 Subject: [PATCH 0169/1534] js transitions --- changes.md | 31 ++++++++++++++++++++++++++++++- src/transition/index.js | 1 + src/transition/js.js | 36 ++++++++++++++++++++++++++++++++++-- 3 files changed, 65 insertions(+), 3 deletions(-) diff --git a/changes.md b/changes.md index 8dad52f8203..af938046cb7 100644 --- a/changes.md +++ b/changes.md @@ -171,4 +171,33 @@ With `v-transition="my-transition"`, Vue will: 2. If no custom JavaScript transition is found, it will automatically sniff whether the target element has CSS transitions or CSS animations applied, and add/remove the classes as before. -3. If no transitions/animations are detected, the DOM manipulation is executed immediately instead of hung up waiting for an event. \ No newline at end of file +3. If no transitions/animations are detected, the DOM manipulation is executed immediately instead of hung up waiting for an event. + +### JavaScript transitions API change + +Now more similar to Angular: + +``` js +Vue.transition('fade', { + enter: function (el, done) { + // element is already inserted into the DOM + // call done when animation finishes. + $(el) + .css('opacity', 0) + .animate({ opacity: 1 }, 1000, done) + // optionally return a "cancel" function + // to clean up if the animation is cancelled + return function () { + $(el).stop() + } + }, + leave: function (el, done) { + // same as enter + $(el) + .animate({ opacity: 0 }, 1000, done) + return function () { + $(el).stop() + } + } +}) +``` \ No newline at end of file diff --git a/src/transition/index.js b/src/transition/index.js index 1b4a3c5a7e9..983e2077d26 100644 --- a/src/transition/index.js +++ b/src/transition/index.js @@ -98,6 +98,7 @@ var apply = exports.apply = function (el, direction, op, vm) { el, direction, applyOp, + transData, jsTransition ) } else if (transData && _.transitionEndEvent) { diff --git a/src/transition/js.js b/src/transition/js.js index 67f7bb58eb3..29fe0dae2d4 100644 --- a/src/transition/js.js +++ b/src/transition/js.js @@ -1,3 +1,35 @@ -module.exports = function () { - +/** + * Apply JavaScript enter/leave functions. + * + * @param {Element} el + * @param {Number} direction - 1: enter, -1: leave + * @param {Function} op - the actual DOM operation + * @param {Object} data - target element's transition data + * @param {Object} def - transition definition object + */ + +module.exports = function (el, direction, op, data, def) { + if (data.cancel) { + data.cancel() + data.cancel = null + } + var enter = def.enter + var leave = def.leave + if (direction > 0) { // enter + op() + if (enter) { + data.cancel = enter(el, function () { + data.cancel = null + }) + } + } else { // leave + if (leave) { + data.cancel = leave(el, function () { + op() + data.cancel = null + }) + } else { + op() + } + } } \ No newline at end of file From 374dcb616c8371555216bdbed85901942708f2f9 Mon Sep 17 00:00:00 2001 From: Evan You Date: Tue, 19 Aug 2014 20:51:39 -0400 Subject: [PATCH 0170/1534] transition tweaks --- src/transition/index.js | 21 +++++++++++---------- src/util/env.js | 2 +- 2 files changed, 12 insertions(+), 11 deletions(-) diff --git a/src/transition/index.js b/src/transition/index.js index 983e2077d26..2c8bc83c75e 100644 --- a/src/transition/index.js +++ b/src/transition/index.js @@ -81,17 +81,18 @@ var apply = exports.apply = function (el, direction, op, vm) { op() vm._callHook(direction > 0 ? 'attached' : 'detached') } - // if the vm is being manipulated by a parent directive - // during the parent's compilation phase, we skip the - // animation. - if (vm.$parent && !vm.$parent._isCompiled) { - applyOp() - return + var transData = el.__v_trans + if ( + !transData || + // if the vm is being manipulated by a parent directive + // during the parent's compilation phase, skip the + // animation. + (vm.$parent && !vm.$parent._isCompiled) + ) { + return applyOp() } // determine the transition type on the element - var transData = el.__v_trans - var registry = vm.$options.transitions - var jsTransition = transData && registry[transData.id] + var jsTransition = vm.$options.transitions[transData.id] if (jsTransition) { // js applyJSTransition( @@ -101,7 +102,7 @@ var apply = exports.apply = function (el, direction, op, vm) { transData, jsTransition ) - } else if (transData && _.transitionEndEvent) { + } else if (_.transitionEndEvent) { // css applyCSSTransition( el, diff --git a/src/util/env.js b/src/util/env.js index 3af079859e4..0d12a44d677 100644 --- a/src/util/env.js +++ b/src/util/env.js @@ -44,7 +44,7 @@ exports.isIE9 = * Sniff transition/animation events */ -if (inBrowser) { +if (inBrowser && !exports.isIE9) { if ( window.ontransitionend === undefined && window.onwebkittransitionend !== undefined From 04dd3af0a13d244ee9e14ee685a567995e45d438 Mon Sep 17 00:00:00 2001 From: Evan You Date: Tue, 19 Aug 2014 23:59:29 -0400 Subject: [PATCH 0171/1534] allow return false in event handlers to cancel propagation/broadcasting --- src/api/events.js | 32 +++++++++++++------------------- src/emitter.js | 6 +++++- 2 files changed, 18 insertions(+), 20 deletions(-) diff --git a/src/api/events.js b/src/api/events.js index 541d3c61741..99222e76d2a 100644 --- a/src/api/events.js +++ b/src/api/events.js @@ -22,17 +22,14 @@ */ exports.$broadcast = function () { - var children = this.children + var children = this._children for (var i = 0, l = children.length; i < l; i++) { var child = children[i] - child._emitter.applyEmit.apply( - child._emitter, - arguments - ) - child.$broadcast.apply( - child, - arguments - ) + var emitter = child._emitter + emitter.applyEmit.apply(emitter, arguments) + if (!emitter._cancelled) { + child.$broadcast.apply(child, arguments) + } } } @@ -44,15 +41,12 @@ exports.$broadcast = function () { */ exports.$dispatch = function () { - this._emitter.applyEmit.apply( - this._emitter, - arguments - ) - var parent = this.$parent - if (parent) { - parent.$dispatch.apply( - parent, - arguments - ) + var emitter = this._emitter + emitter.applyEmit.apply(emitter, arguments) + if (!emitter._cancelled) { + var parent = this.$parent + if (parent) { + parent.$dispatch.apply(parent, arguments) + } } } \ No newline at end of file diff --git a/src/emitter.js b/src/emitter.js index 8752c18165d..e6c1c26f681 100644 --- a/src/emitter.js +++ b/src/emitter.js @@ -8,6 +8,7 @@ var _ = require('./util') */ function Emitter (ctx) { + this._cancelled = false this._ctx = ctx || this } @@ -114,6 +115,7 @@ p.emit = function (event, a, b, c) { */ p.applyEmit = function (event) { + this._cancelled = false this._cbs = this._cbs || {} var callbacks = this._cbs[event] if (callbacks) { @@ -127,7 +129,9 @@ p.applyEmit = function (event) { } callbacks = _.toArray(callbacks) for (i = 0, l = callbacks.length; i < l; i++) { - callbacks[i].apply(this._ctx, args) + if (callbacks[i].apply(this._ctx, args) === false) { + this._cancelled = true + } } } return this From d229fe0c1a27c125e66218120f6944215d40ae52 Mon Sep 17 00:00:00 2001 From: Evan You Date: Wed, 20 Aug 2014 00:01:46 -0400 Subject: [PATCH 0172/1534] record event api change --- changes.md | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/changes.md b/changes.md index af938046cb7..af4f215f04b 100644 --- a/changes.md +++ b/changes.md @@ -200,4 +200,32 @@ Vue.transition('fade', { } } }) +``` + +## Events API + +Now if an event handler return `false`, it will stop event propagation for `$dispatch` and stop broadcasting to children for `$broadcast`. + +``` js +var a = new Vue() +var b = new Vue({ + parent: a +}) +var c = new Vue({ + parent: b +}) + +a.$on('test', function () { + console.log('a') +}) +b.$on('test', function () { + console.log('b') + return false +}) +c.$on('test', function () { + console.log('c') +}) +c.$dispatch('test') +// -> 'c' +// -> 'b' ``` \ No newline at end of file From 668548ba4c59d1173208a3e18bddc786dc4555b7 Mon Sep 17 00:00:00 2001 From: Evan You Date: Wed, 20 Aug 2014 00:03:23 -0400 Subject: [PATCH 0173/1534] update changes for interpolation --- changes.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/changes.md b/changes.md index af4f215f04b..ac6ba411a8d 100644 --- a/changes.md +++ b/changes.md @@ -87,7 +87,7 @@ When authoring literal directives, you can now provide an `update()` function if ## Interpolation -Text bindings will no longer automatically stringify objects. Use the new `json` filter which gives more flexibility in formatting. Also, `null` will now be printed as is; only `undefined` will yield empty string. +Text bindings will no longer automatically stringify objects. Use the new `json` filter which gives more flexibility in formatting. ## Two Way filters From c917988253bee8de7316e8470d39ff1a7710bcd3 Mon Sep 17 00:00:00 2001 From: Evan You Date: Wed, 20 Aug 2014 00:11:54 -0400 Subject: [PATCH 0174/1534] block logic control changes --- changes.md | 40 +++++++++++++++++++++++++++++++++++++++- 1 file changed, 39 insertions(+), 1 deletion(-) diff --git a/changes.md b/changes.md index ac6ba411a8d..f02d7e71283 100644 --- a/changes.md +++ b/changes.md @@ -106,7 +106,45 @@ Vue.filter('format', { ## Block logic control -Still open to suggestions. See details [here]. +One limitation of flow control direcitves like `v-repeat` and `v-if` is that they can only be used on a single element. Now you can use them to manage a block of content by using them on a `