From 49cfe4645e34bf0e47cda219f22d0adf3f8087a8 Mon Sep 17 00:00:00 2001 From: Gokmen Goksel Date: Tue, 8 Aug 2017 02:16:33 -0700 Subject: [PATCH 1/6] KDData: create proxy only on set --- lib/core/data.coffee | 35 ++++++++++------------------------- 1 file changed, 10 insertions(+), 25 deletions(-) diff --git a/lib/core/data.coffee b/lib/core/data.coffee index 8f05b867..e757c1fe 100644 --- a/lib/core/data.coffee +++ b/lib/core/data.coffee @@ -47,50 +47,35 @@ module.exports = class KDData proxyHandler = (base) -> - get: (target, key) -> - - value = target[key] - return value if typeof key isnt 'string' - - key = getFullPath target, key - - if value and dataValue = KD.utils.JsPath.getAt base.__data__, key - if dataValue instanceof Object and value not instanceof Date - proxy = createProxy value, proxyHandler base - Object.defineProperty proxy, KDData.NAME, { - value: key, configurable: yes - } - return proxy - - return value - - set: (target, key, value, receiver) -> if base.isArray currentLength = target.length + if value instanceof Object and value not instanceof Date + if root = receiver[KDData.NAME] + key = "#{root}.#{key}" + value = createProxy value, proxyHandler base + Object.defineProperty value, KDData.NAME, { + value: key, configurable: yes + } + target[key] = value if base.isArray lengthChanged = target.length isnt currentLength return true if key is 'length' and not lengthChanged + return true if typeof key is 'symbol' or /^__|__$/.test key + if root = receiver[KDData.NAME] key = "#{root}.#{key}" else root = '' - return true if typeof key is 'symbol' or /^__|__$/.test key - if lengthChanged prefix = if key.indexOf('.') >= 0 then "#{root}." else '' base.emit 'update', [ "#{prefix}length" ] base.emit 'update', [ key ] return true - - - getPrototypeOf: (target) -> - - return KDData.prototype From d6b8b0ea86e92ce20b4be2cb88e3f447884d2b92 Mon Sep 17 00:00:00 2001 From: Gokmen Goksel Date: Tue, 8 Aug 2017 02:16:50 -0700 Subject: [PATCH 2/6] KDData: updated tests for more coverage --- Makefile | 1 + test/core/data.coffee | 33 +++++++++++++++++++++++---------- 2 files changed, 24 insertions(+), 10 deletions(-) diff --git a/Makefile b/Makefile index df4f0bdb..9026f62e 100644 --- a/Makefile +++ b/Makefile @@ -73,6 +73,7 @@ css: clean: clean_dist @rm -fr build + @rm -fr coverage clean_dist: @echo ' - Cleanup...' diff --git a/test/core/data.coffee b/test/core/data.coffee index 918de492..186468a8 100644 --- a/test/core/data.coffee +++ b/test/core/data.coffee @@ -37,6 +37,12 @@ describe 'KDData', -> @instance.foo = 'bar' @instance.foo.should.equal 'bar' + it 'should support compare', -> + + @instance.foo = {bar: 'baz'} + (@instance.foo is @instance.foo).should.equal true + (@instance.foo.bar is @instance.foo.bar).should.equal true + it 'should emit update changed fields on data change', -> emitter = KDData.getEmitter @instance @@ -65,11 +71,11 @@ describe 'KDData', -> spy.should.be.calledOnce() spy.should.be.calledWith ['foo'] - @instance.foo.bar.baz += 10 + @instance.foo.bar = 15 spy.should.be.calledTwice() - spy.should.be.calledWith ['foo.bar.baz'] + spy.should.be.calledWith ['foo.bar'] - @instance.foo.bar.baz.should.equal 15 + @instance.foo.bar.should.equal 15 describe 'Arrays', -> @@ -85,6 +91,13 @@ describe 'KDData', -> @instance[0].should.equal 'bar' + it 'should support compare', -> + + @instance.push [1, 2] + (@instance[0] is @instance[0]).should.equal true + (@instance[0][1] is @instance[0][1]).should.equal true + + it 'should emit update changed fields on data change', -> emitter = KDData.getEmitter @instance @@ -112,20 +125,20 @@ describe 'KDData', -> spy = sinon.spy -> yes emitter.on 'update', spy - @instance[0] = ['foo', [1, 2]] + @instance[0] = ['foo', 1, 2] spy.should.be.calledTwice() spy.should.be.calledWith ['0'] spy.should.be.calledWith ['length'] - @instance[0][1][0] += 4 + @instance[0][1] += 4 spy.should.be.calledThrice() - spy.should.be.calledWith ['0.1.0'] - @instance[0][1][0].should.be.equal 5 + spy.should.be.calledWith ['0.1'] + @instance[0][1].should.be.equal 5 - @instance[0][1].push 10 - spy.should.be.calledWith ['0.1.length'] + @instance[0].push 10 + spy.should.be.calledWith ['0.length'] - @instance[0][1][2].should.be.equal 10 + @instance[0][3].should.be.equal 10 describe 'triggers render on pistachio', -> From 681b107e58d729dd208abc4ca79e4ba2516acb7c Mon Sep 17 00:00:00 2001 From: Gokmen Goksel Date: Tue, 8 Aug 2017 22:06:05 -0700 Subject: [PATCH 3/6] Test-watch: use Chrome only, DiaScene destroy existing canvases --- gulpfile.coffee | 2 ++ lib/components/dia/diascene.coffee | 5 +++-- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/gulpfile.coffee b/gulpfile.coffee index ff5d942e..c0c4c345 100644 --- a/gulpfile.coffee +++ b/gulpfile.coffee @@ -26,6 +26,8 @@ gulp.task 'test', (done) -> gulp.task 'test-watch', (done) -> options.singleRun = false + options.browsers = ['Chrome'] + server(options, -> done() if not doneBefore doneBefore = yes diff --git a/lib/components/dia/diascene.coffee b/lib/components/dia/diascene.coffee index 1f40d107..7c7d4ff7 100644 --- a/lib/components/dia/diascene.coffee +++ b/lib/components/dia/diascene.coffee @@ -23,7 +23,7 @@ module.exports = class KDDiaScene extends KDView options.updateEvery ?= 10 options.prependCanvas ?= no - super + super options, data @containers = [] @connections = [] @@ -523,7 +523,8 @@ module.exports = class KDDiaScene extends KDView createCanvas: -> - return if @realCanvas + @realCanvas?.destroy() + @fakeCanvas?.destroy() @addSubView @realCanvas = new KDCustomHTMLView tagName : 'canvas' From 39e66c57048b0f01d0bce694ed25574bfd3cfaf4 Mon Sep 17 00:00:00 2001 From: Gokmen Goksel Date: Tue, 8 Aug 2017 22:06:54 -0700 Subject: [PATCH 4/6] KDData: handle all cases on set trap, create proxies recursively on init --- lib/core/data.coffee | 110 ++++++++++++++++++++++++++++++------------- 1 file changed, 77 insertions(+), 33 deletions(-) diff --git a/lib/core/data.coffee b/lib/core/data.coffee index e757c1fe..2345a9e4 100644 --- a/lib/core/data.coffee +++ b/lib/core/data.coffee @@ -1,3 +1,4 @@ +debug = require('debug') 'kd:data' KD = require './kd' KDEventEmitter = require './eventemitter' @@ -11,24 +12,46 @@ createProxy = (data, handler) -> console.warn 'Proxies are not supported on this platform!' return new Object data +isValid = (value) -> + value and typeof value is 'object' and value not instanceof Date + + module.exports = class KDData @EMITTER = createSymbol 'kddata' @NAME = createSymbol 'name' - constructor: (data = {}) -> + constructor: (data = {}, options = {}) -> + + @emitter = new KDEventEmitter + @emitter.__data__ = data + @emitter.__event__ = options.updateEvent ? 'update' + @emitter.maxdepth = options.max_depth ? 2 + + @proxy = createProxy data, KDData::proxyHandler.call this + Object.defineProperty @proxy, KDData.EMITTER, { + value: @emitter, configurable: yes + } + + initialize = (data, key = null, depth = 0) => + return if not isValid data - emitter = new KDEventEmitter - emitter.__data__ = data - emitter.isArray = Array.isArray data + depth += 1 + for _key, child of data + path = if key then "#{key}.#{_key}" else _key + if depth < @emitter.maxdepth and isValid child + initialize child, path, depth + KD.utils.JsPath.setAt @proxy, path, child - proxy = createProxy data, proxyHandler emitter - Object.defineProperty proxy, KDData.EMITTER, value: emitter + initialize data + @initialized = yes + + return @proxy - return proxy @isSupported = -> !!window.Proxy? + @getEmitter = (data) -> return unless data? @@ -38,44 +61,65 @@ module.exports = class KDData return data - getFullPath = (parent, child) -> + emit: (updates) -> + + return if not @initialized + @emitter.emit @emitter.__event__, [ updates ] + + + createObjectProxy: (obj, key) -> - if root = parent[KDData.NAME] - return "#{root}.#{child}" - return child + value = createProxy obj, @proxyHandler.call this + return value unless key + Object.defineProperty value, KDData.NAME, { + value: key, configurable: yes + } + return value - proxyHandler = (base) -> - set: (target, key, value, receiver) -> + proxify: (value, key, depth = 0) -> - if base.isArray + return value if not isValid value + + depth += 1 + + for _key, child of value + path = if key then "#{key}.#{_key}" else _key + debug 'path on', _key, path + if depth < @emitter.maxdepth and isValid child + value[_key] = @createObjectProxy child, path + @proxify child, path, depth + + debug 'creating proxy', value, key + + if isValid value + return @createObjectProxy value, key + else + return value + + + proxyHandler: -> + + set: (target, key, value, receiver) => + + debug 'setting', key, value + + if isArray = Array.isArray target currentLength = target.length - if value instanceof Object and value not instanceof Date - if root = receiver[KDData.NAME] - key = "#{root}.#{key}" - value = createProxy value, proxyHandler base - Object.defineProperty value, KDData.NAME, { - value: key, configurable: yes - } + if parent = receiver[KDData.NAME] ? '' + path = "#{parent}.#{key}" - target[key] = value + target[key] = @proxify value, path ? key - if base.isArray + if isArray lengthChanged = target.length isnt currentLength return true if key is 'length' and not lengthChanged - return true if typeof key is 'symbol' or /^__|__$/.test key - - if root = receiver[KDData.NAME] - key = "#{root}.#{key}" - else - root = '' + @emit path ? key - if lengthChanged - prefix = if key.indexOf('.') >= 0 then "#{root}." else '' - base.emit 'update', [ "#{prefix}length" ] + if lengthChanged and key isnt 'length' + @emit if parent then "#{parent}.length" else 'length' - base.emit 'update', [ key ] return true From 0fedf7386303b044509902154ae478da17d3bb8e Mon Sep 17 00:00:00 2001 From: Gokmen Goksel Date: Tue, 8 Aug 2017 22:07:43 -0700 Subject: [PATCH 5/6] Tests: KDData: add new test case scenarios for all functionality --- test/core/data.coffee | 212 +++++++++++++++++++++++++++++++++++++++++- 1 file changed, 207 insertions(+), 5 deletions(-) diff --git a/test/core/data.coffee b/test/core/data.coffee index 186468a8..f8c820ba 100644 --- a/test/core/data.coffee +++ b/test/core/data.coffee @@ -39,9 +39,10 @@ describe 'KDData', -> it 'should support compare', -> - @instance.foo = {bar: 'baz'} - (@instance.foo is @instance.foo).should.equal true - (@instance.foo.bar is @instance.foo.bar).should.equal true + @instance.foo = { bar: 'baz' } + + @instance.foo.should.deepEqual @instance.foo + @instance.foo.bar.should.deepEqual @instance.foo.bar it 'should emit update changed fields on data change', -> @@ -78,6 +79,90 @@ describe 'KDData', -> @instance.foo.bar.should.equal 15 + it 'should support objects in objects', -> + + emitter = KDData.getEmitter @instance + emitter.should.exist + + spy = sinon.spy -> yes + emitter.on 'update', spy + + @instance.foo = { bar: { baz: 5 }, x: { y: [1, 2] } } + + spy.should.be.calledOnce() + spy.should.be.calledWith ['foo'] + + @instance.foo.bar.baz += 15 + + spy.should.be.calledTwice() + spy.should.be.calledWith ['foo.bar.baz'] + @instance.foo.bar.baz.should.equal 20 + + @instance.foo.x.y[0] += 3 + @instance.foo.x.y[0].should.equal 4 + + + it 'should support custom depth level', -> + + @instance = new KDData {}, { max_depth: 3 } + + emitter = KDData.getEmitter @instance + emitter.should.exist + + spy = sinon.spy -> yes + emitter.on 'update', spy + + @instance.a = {b:{c:{d:''}}} + + spy.should.be.calledOnce() + spy.should.be.calledWith ['a'] + + @instance.a.b.c.d = 'foo' + + spy.should.be.calledTwice() + spy.should.be.calledWith ['a.b.c.d'] + + + it 'should work with existing data', -> + + @instance = new KDData { + a: { + b: [1, 2] + }, + c: { + d: { + e: { + f: 10 + } + } + } + }, { + max_depth: 3 + } + + emitter = KDData.getEmitter @instance + emitter.should.exist + + spy = sinon.spy -> yes + emitter.on 'update', spy + + @instance.a.b.push 5 + + spy.should.be.calledTwice() + spy.should.be.calledWith ['a.b.2'] + spy.should.be.calledWith ['a.b.length'] + + @instance.a.b[0] += 2 + @instance.a.b[0].should.equal 3 + spy.should.be.calledWith ['a.b.0'] + + spy.should.be.calledThrice() + + @instance.c.d.e.f += 20 + @instance.c.d.e.f.should.equal 30 + spy.should.be.calledWith ['c.d.e.f'] + + describe 'Arrays', -> beforeEach -> @@ -94,8 +179,9 @@ describe 'KDData', -> it 'should support compare', -> @instance.push [1, 2] - (@instance[0] is @instance[0]).should.equal true - (@instance[0][1] is @instance[0][1]).should.equal true + + @instance[0].should.deepEqual @instance[0] + @instance[0][1].should.deepEqual @instance[0][1] it 'should emit update changed fields on data change', -> @@ -141,6 +227,122 @@ describe 'KDData', -> @instance[0][3].should.be.equal 10 + it 'should support inner arrays in arrays', -> + + emitter = KDData.getEmitter @instance + emitter.should.exist + + spy = sinon.spy (updates) -> yes + emitter.on 'update', spy + + @instance.push ['foo', [1, 2]] + + spy.should.be.calledTwice() + spy.should.be.calledWith ['0'] + spy.should.be.calledWith ['length'] + + @instance[0][1][0] += 4 + + spy.should.be.calledThrice() + spy.should.be.calledWith ['0.1.0'] + @instance[0][1][0].should.be.equal 5 + + @instance[0][1].push 10 + + spy.should.be.calledWith ['0.1.length'] + @instance[0][1][2].should.be.equal 10 + + @instance[0][1][3] = [] + + spy.should.be.calledWith ['0.1.3'] + spy.should.be.calledWith ['0.1.length'] + + @instance[0][1][3].push 5 + + spy.should.be.calledWith ['0.1.3.length'] + @instance[0][1][3][0].should.be.equal 5 + + + it 'should support custom depth level', -> + + @instance = new KDData [], { max_depth: 3 } + + emitter = KDData.getEmitter @instance + emitter.should.exist + + spy = sinon.spy -> yes + emitter.on 'update', spy + + @instance.push [[[5, [1, 2]]]] + + spy.should.be.calledTwice() + spy.should.be.calledWith ['0'] + spy.should.be.calledWith ['length'] + + @instance[0][0][0].push 10 + @instance[0][0][0][2].should.equal 10 + + spy.should.be.calledWith ['0.0.0.2'] + spy.should.be.calledWith ['0.0.0.length'] + + + it 'should work with existing data', -> + + @instance = new KDData [[[1, 2]]] + + emitter = KDData.getEmitter @instance + emitter.should.exist + + spy = sinon.spy -> yes + emitter.on 'update', spy + + child = { x: 10 } + @instance[0][0].push child + @instance[0][0][2].should.deepEqual child + + spy.should.be.calledTwice() + spy.should.be.calledWith ['0.0.2'] + spy.should.be.calledWith ['0.0.length'] + + @instance[0][0][2].x += 5 + @instance[0][0][2].x.should.equal 15 + + spy.should.be.calledThrice() + spy.should.be.calledWith ['0.0.2.x'] + + + it 'should support native Array operations', -> + + emitter = KDData.getEmitter @instance + emitter.should.exist + + spy = sinon.spy -> yes + emitter.on 'update', spy + + @instance.push 5 + + @instance.length.should.equal 1 + spy.should.be.calledTwice() + spy.should.be.calledWith ['0'] + spy.should.be.calledWith ['length'] + + @instance.pop() + + @instance.length.should.equal 0 + spy.should.be.calledThrice() + spy.should.be.calledWith ['length'] + + @instance.push 5 + @instance.push 10 + + @instance.length.should.equal 2 + + @instance.shift() + + @instance.length.should.equal 1 + @instance[0].should.equal 10 + + describe 'triggers render on pistachio', -> describe 'with Objects', -> From 27bd7c33321b58836eedafb3ef01b9ebeeeb6d51 Mon Sep 17 00:00:00 2001 From: Gokmen Goksel Date: Tue, 8 Aug 2017 22:15:01 -0700 Subject: [PATCH 6/6] Readme: add npm badge --- Readme.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Readme.md b/Readme.md index 3bd06788..2ab813a0 100644 --- a/Readme.md +++ b/Readme.md @@ -1,4 +1,4 @@ -[![Build Status](https://img.shields.io/travis/koding/kd.svg?style=flat)](https://travis-ci.org/koding/kd) [![Coverage Status](https://img.shields.io/coveralls/koding/kd.svg?style=flat)](https://coveralls.io/github/koding/kd?branch=master) +[![Build Status](https://img.shields.io/travis/koding/kd.svg?style=flat)](https://travis-ci.org/koding/kd) [![npm](https://img.shields.io/npm/v/kd.js.svg)](https://www.npmjs.com/package/kd.js) [![Coverage Status](https://img.shields.io/coveralls/koding/kd.svg?style=flat)](https://coveralls.io/github/koding/kd?branch=master) # kd.js @@ -44,7 +44,7 @@ main.addSubView(tabs); # example -Type `make example` and go to http://localhost:3000/example. This also starts a `watchify` process, so any changes you make in `example/index.js` will be recompiled on the spot. +Type `make example` to checkout some examples. # development