From c74ea8b1056e36996105e9d5421b361d61ad180b Mon Sep 17 00:00:00 2001 From: ampatspell Date: Thu, 9 Aug 2018 18:11:30 +0300 Subject: [PATCH 01/63] models property definition, noop internal & model --- .../less-experimental/models/index.js | 28 +++++++++++++++ .../less-experimental/models/internal.js | 18 ++++++++++ .../less-experimental/models/models.js | 5 +++ .../less-experimental/models/property.js | 21 +++++++++++ addon/less-experimental/index.js | 5 +++ addon/less-experimental/models.js | 3 ++ app/initializers/zuglet-internal.js | 9 +++++ tests/unit/less-experimental-models-test.js | 36 +++++++++++++++++++ 8 files changed, 125 insertions(+) create mode 100644 addon/-private/less-experimental/models/index.js create mode 100644 addon/-private/less-experimental/models/internal.js create mode 100644 addon/-private/less-experimental/models/models.js create mode 100644 addon/-private/less-experimental/models/property.js create mode 100644 addon/less-experimental/index.js create mode 100644 addon/less-experimental/models.js create mode 100644 tests/unit/less-experimental-models-test.js diff --git a/addon/-private/less-experimental/models/index.js b/addon/-private/less-experimental/models/index.js new file mode 100644 index 00000000..1ac6f29d --- /dev/null +++ b/addon/-private/less-experimental/models/index.js @@ -0,0 +1,28 @@ +import { A } from '@ember/array'; +import { assign } from '@ember/polyfills'; +import property from './property'; + +const compact = array => A(array).compact(); + +const build = (opts, nested={}) => { + opts = assign({}, opts, nested); + let prop = property(opts); + prop.owner = (...args) => build(opts, { owner: compact(args) }); + prop.object = (...args) => build(opts, { object: compact(args) }); + prop.inline = arg => build(opts, { inline: arg }); + prop.named = arg => build(opts, { named: arg }); + prop.mapping = arg => build(opts, { mapping: arg }); + return prop; +} + +export default source => { + let opts = { + source, + owner: [], + object: [], + inline: undefined, + named: undefined, + mapping: undefined + }; + return build(opts); +} diff --git a/addon/-private/less-experimental/models/internal.js b/addon/-private/less-experimental/models/internal.js new file mode 100644 index 00000000..38a72d9a --- /dev/null +++ b/addon/-private/less-experimental/models/internal.js @@ -0,0 +1,18 @@ +import Internal from '../../internal/internal'; +import { getOwner } from '@ember/application'; + +export default Internal.extend({ + + parent: null, + key: null, + opts: null, // { source, owner, object, inline, named, mapping } + + createModel() { + return getOwner(this.parent).factoryFor('zuglet:less-experimental/models').create({ _internal: this }); + }, + + willDestroy() { + this._super(...arguments); + } + +}); diff --git a/addon/-private/less-experimental/models/models.js b/addon/-private/less-experimental/models/models.js new file mode 100644 index 00000000..7f548c02 --- /dev/null +++ b/addon/-private/less-experimental/models/models.js @@ -0,0 +1,5 @@ +import EmberObject from '@ember/object'; +import ModelMixin from '../../internal/model-mixin'; + +export default EmberObject.extend(ModelMixin, { +}); diff --git a/addon/-private/less-experimental/models/property.js b/addon/-private/less-experimental/models/property.js new file mode 100644 index 00000000..22763af3 --- /dev/null +++ b/addon/-private/less-experimental/models/property.js @@ -0,0 +1,21 @@ +import destroyable from '../../util/destroyable'; +import { A } from '@ember/array'; +import { getOwner } from '@ember/application'; + +const get = internal => internal.model(true); +const reusable = () => false; + +const create = opts => function(key) { + let parent = this; + let owner = getOwner(this); + return owner.factoryFor('zuglet:less-experimental/models/internal').create({ parent, key, opts }); +} + +export default opts => { + let dependencies = A().compact(); + return destroyable(...dependencies, { + create: create(opts), + reusable, + get + }); +} diff --git a/addon/less-experimental/index.js b/addon/less-experimental/index.js new file mode 100644 index 00000000..cb447884 --- /dev/null +++ b/addon/less-experimental/index.js @@ -0,0 +1,5 @@ +import models from './models'; + +export { + models +} diff --git a/addon/less-experimental/models.js b/addon/less-experimental/models.js new file mode 100644 index 00000000..6f8cf016 --- /dev/null +++ b/addon/less-experimental/models.js @@ -0,0 +1,3 @@ +import models from '../-private/less-experimental/models'; + +export default models; diff --git a/app/initializers/zuglet-internal.js b/app/initializers/zuglet-internal.js index e756625e..0fb06266 100644 --- a/app/initializers/zuglet-internal.js +++ b/app/initializers/zuglet-internal.js @@ -100,6 +100,9 @@ import ComputedModelInternal from 'ember-cli-zuglet/-private/experimental/model/ import ComputedModelsInternal from 'ember-cli-zuglet/-private/experimental/models/internal'; import ComputedModels from 'ember-cli-zuglet/-private/experimental/models/models'; +import LessExperimentalModelsInternal from 'ember-cli-zuglet/-private/less-experimental/models/internal'; +import LessExperimentalModels from 'ember-cli-zuglet/-private/less-experimental/models/models'; + export default { name: 'zuglet:internal', initialize(container) { @@ -239,9 +242,15 @@ export default { // + // experimental + container.register('zuglet:computed/observed/internal', ComputedObservedInternal); container.register('zuglet:computed/model/internal', ComputedModelInternal); container.register('zuglet:computed/models/internal', ComputedModelsInternal); container.register('zuglet:computed/models', ComputedModels); + + // less-experimental + container.register('zuglet:less-experimental/models/internal', LessExperimentalModelsInternal); + container.register('zuglet:less-experimental/models', LessExperimentalModels); } } diff --git a/tests/unit/less-experimental-models-test.js b/tests/unit/less-experimental-models-test.js new file mode 100644 index 00000000..c96e6cbe --- /dev/null +++ b/tests/unit/less-experimental-models-test.js @@ -0,0 +1,36 @@ +import EmberObject from '@ember/object'; +import { module, test, setupStoreTest } from '../helpers/setup'; +import { getOwner } from '@ember/application'; +import { models } from 'ember-cli-zuglet/less-experimental'; +import { A } from '@ember/array'; + +const Owner = EmberObject.extend({ +}); + +module('less-experimental-models', function(hooks) { + setupStoreTest(hooks); + + hooks.beforeEach(async function() { + this.getOwner = () => getOwner(this.store); + this.registerModel = (name, factory) => this.getOwner().register(`model:${name}`, factory); + this.subject = props => { + let name = 'subject'; + let factory = Owner.extend(props); + let owner = this.getOwner(); + let fullName = `component:${name}`; + owner.register(fullName, factory); + return owner.factoryFor(fullName).create(); + }; + }); + + test('model is created', async function(assert) { + let subject = this.subject({ + docs: A(), + models: models('docs').inline({ + }) + }); + + assert.ok(subject.models); + }); + +}); From d4e600aa6dfbca6c7d950126f5e69bf24d1c5eeb Mon Sep 17 00:00:00 2001 From: ampatspell Date: Thu, 9 Aug 2018 18:15:15 +0300 Subject: [PATCH 02/63] cleanup --- tests/unit/less-experimental-models-test.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/tests/unit/less-experimental-models-test.js b/tests/unit/less-experimental-models-test.js index c96e6cbe..ce5437b2 100644 --- a/tests/unit/less-experimental-models-test.js +++ b/tests/unit/less-experimental-models-test.js @@ -13,8 +13,7 @@ module('less-experimental-models', function(hooks) { hooks.beforeEach(async function() { this.getOwner = () => getOwner(this.store); this.registerModel = (name, factory) => this.getOwner().register(`model:${name}`, factory); - this.subject = props => { - let name = 'subject'; + this.subject = (props, name='subject') => { let factory = Owner.extend(props); let owner = this.getOwner(); let fullName = `component:${name}`; From 11d9f1153863478953fa80a669e01d1704dae68d Mon Sep 17 00:00:00 2001 From: ampatspell Date: Thu, 9 Aug 2018 18:30:29 +0300 Subject: [PATCH 03/63] so, models public will be ArrayProxy --- addon/-private/less-experimental/models/internal.js | 9 ++++++++- addon/-private/less-experimental/models/models.js | 4 ++-- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/addon/-private/less-experimental/models/internal.js b/addon/-private/less-experimental/models/internal.js index 38a72d9a..ea3a4c34 100644 --- a/addon/-private/less-experimental/models/internal.js +++ b/addon/-private/less-experimental/models/internal.js @@ -1,5 +1,6 @@ import Internal from '../../internal/internal'; import { getOwner } from '@ember/application'; +import { A } from '@ember/array'; export default Internal.extend({ @@ -7,8 +8,14 @@ export default Internal.extend({ key: null, opts: null, // { source, owner, object, inline, named, mapping } + init() { + this._super(...arguments); + this.content = A(); + }, + createModel() { - return getOwner(this.parent).factoryFor('zuglet:less-experimental/models').create({ _internal: this }); + let content = this.content; + return getOwner(this.parent).factoryFor('zuglet:less-experimental/models').create({ _internal: this, content }); }, willDestroy() { diff --git a/addon/-private/less-experimental/models/models.js b/addon/-private/less-experimental/models/models.js index 7f548c02..99200961 100644 --- a/addon/-private/less-experimental/models/models.js +++ b/addon/-private/less-experimental/models/models.js @@ -1,5 +1,5 @@ -import EmberObject from '@ember/object'; +import ArrayProxy from '@ember/array/proxy'; import ModelMixin from '../../internal/model-mixin'; -export default EmberObject.extend(ModelMixin, { +export default ArrayProxy.extend(ModelMixin, { }); From 9e424ac51a1a898c47fb1c78a0b8937e358eae6b Mon Sep 17 00:00:00 2001 From: ampatspell Date: Thu, 9 Aug 2018 18:52:35 +0300 Subject: [PATCH 04/63] add object-observer which observes single object's array of properties --- .../less-experimental/util/object-observer.js | 68 +++++++++++++++++++ .../less-experimental-object-observer-test.js | 66 ++++++++++++++++++ 2 files changed, 134 insertions(+) create mode 100644 addon/-private/less-experimental/util/object-observer.js create mode 100644 tests/unit/less-experimental-object-observer-test.js diff --git a/addon/-private/less-experimental/util/object-observer.js b/addon/-private/less-experimental/util/object-observer.js new file mode 100644 index 00000000..8e6c1a25 --- /dev/null +++ b/addon/-private/less-experimental/util/object-observer.js @@ -0,0 +1,68 @@ +import { addObserver, removeObserver } from '@ember/object/observers'; +import { assert } from '@ember/debug'; +import { typeOf } from '@ember/utils'; +import { get } from '@ember/object'; + +const withKeys = (keys, cb) => { + if(!keys || keys.length === 0) { + return; + } + keys.map(key => cb(key)); +} + +export const startObservingObject = (object, keys, target, method) => { + withKeys(keys, key => addObserver(object, key, target, method)); +} + +export const stopObservingObject = (object, keys, target, method) => { + withKeys(keys, key => removeObserver(object, key, target, method)); +} + +export const startObservingObjects = (objects, keys, target, method) => { + objects.map(object => startObservingObject(object, keys, target, method)); +} + +export const stopObservingObjects = (objects, keys, target, method) => { + objects.map(object => stopObservingObject(object, keys, target, method)); +} + +export default class ObjectObserver { + + // object: EmberObject + // observe: [ 'id', 'type' ] + // delegate: { updated(object, key) } + constructor({ object, observe, delegate }) { + assert(`object is required`, !!object); + assert(`observe must be array`, typeOf(observe) === 'array'); + assert(`delegate is required`, !!delegate); + assert(`delegate.updated must be function`, typeOf(delegate.updated) === 'function'); + this._object = object; + this._observe = observe; + this._delegate = delegate; + this._enabled = get(observe, 'length') > 0; + this._startObserving(); + } + + _startObserving() { + if(!this._enabled) { + return; + } + startObservingObject(this._object, this._observe, this, this._objectValueForKeyDidChange); + } + + _stopObserving() { + if(!this._enabled) { + return; + } + stopObservingObject(this._object, this._observe, this, this._objectValueForKeyDidChange); + } + + _objectValueForKeyDidChange(object, key) { + this._delegate.updated(object, key); + } + + destroy() { + this._stopObserving(); + } + +} diff --git a/tests/unit/less-experimental-object-observer-test.js b/tests/unit/less-experimental-object-observer-test.js new file mode 100644 index 00000000..205230e2 --- /dev/null +++ b/tests/unit/less-experimental-object-observer-test.js @@ -0,0 +1,66 @@ +import EmberObject from '@ember/object'; +import { module, test, setupStoreTest } from '../helpers/setup'; +import ObjectObserver from 'ember-cli-zuglet/-private/less-experimental/util/object-observer'; +import { A } from '@ember/array'; + +module('less-experimental-object-observer', function(hooks) { + setupStoreTest(hooks); + + hooks.beforeEach(async function(assert) { + this.object = props => EmberObject.create(props); + this.observer = (object, keys) => { + let log = A(); + let observer = new ObjectObserver({ + object, + observe: keys, + delegate: { + updated: (o, key) => { + assert.ok(o === object); + log.push(key); + } + } + }); + + return { log, observer }; + }; + }); + + test('observation not enabled if there is no keys to observe', async function(assert) { + let object = this.object({ name: 'duck', color: 'yellow' }); + let { log, observer } = this.observer(object, []); + + assert.equal(observer._enabled, false); + + observer.destroy(); + assert.equal(log.length, 0); + }); + + test('enabled if there are observation keys', async function(assert) { + let object = this.object({ name: 'duck', color: 'yellow' }); + let { log, observer } = this.observer(object, [ 'name', 'color' ]); + + assert.equal(observer._enabled, true); + + observer.destroy(); + assert.equal(log.length, 0); + }); + + test('observe property changes until destroyed', async function(assert) { + let object = this.object({ name: 'duck', color: 'yellow' }); + let { log, observer } = this.observer(object, [ 'name', 'color' ]); + + assert.deepEqual(log, []); + + object.setProperties({ name: 'duck', color: 'yellow' }); + assert.deepEqual(log, []); + + object.setProperties({ name: 'hamster', color: 'brown' }); + assert.deepEqual(log, [ 'name', 'color' ]); + + observer.destroy(); + + object.setProperties({ name: 'duck', color: 'green' }); + assert.deepEqual(log, [ 'name', 'color' ]); + }); + +}); From 809e4f19033d94bff49e6f651de9f14e9b12c1bb Mon Sep 17 00:00:00 2001 From: ampatspell Date: Thu, 9 Aug 2018 19:22:27 +0300 Subject: [PATCH 05/63] add array-observer which observes array adds, removes and each object properties --- .../less-experimental/util/array-observer.js | 92 +++++++++++++++ .../less-experimental-array-observer-test.js | 110 ++++++++++++++++++ 2 files changed, 202 insertions(+) create mode 100644 addon/-private/less-experimental/util/array-observer.js create mode 100644 tests/unit/less-experimental-array-observer-test.js diff --git a/addon/-private/less-experimental/util/array-observer.js b/addon/-private/less-experimental/util/array-observer.js new file mode 100644 index 00000000..4dbd9bcb --- /dev/null +++ b/addon/-private/less-experimental/util/array-observer.js @@ -0,0 +1,92 @@ +import { A } from '@ember/array'; +import { startObservingObjects, stopObservingObjects } from './object-observer'; +import { assert } from '@ember/debug'; +import { typeOf } from '@ember/utils'; +import { get } from '@ember/object'; + +export default class ArrayObserver { + + // array: Enumerable + // observe: [ 'id', 'type' ] + // delegate: { removed(array), added(array), updated(object, key) } + constructor({ array, observe, delegate }) { + assert(`array must be array`, typeOf(array) === 'array'); + assert(`observe must be array`, typeOf(observe) === 'array'); + assert(`delegate is required`, !!delegate); + assert(`delegate.added must be function`, typeOf(delegate.added) === 'function'); + assert(`delegate.removed must be function`, typeOf(delegate.removed) === 'function'); + assert(`delegate.updated must be function`, typeOf(delegate.updated) === 'function'); + this._array = array; + this._observe = observe; + this._delegate = delegate; + this._enabled = get(observe, 'length') > 0; + this.isDestroyed = false; + this._startObserving(); + } + + // + + _startObservingObjects(objects) { + if(!this._enabled) { + return; + } + startObservingObjects(objects, this._observe, this, this._objectDidChange); + } + + _stopObservingObjects(objects) { + if(!this._enabled) { + return; + } + stopObservingObjects(objects, this._observe, this, this._objectDidChange); + } + + _objectDidChange(object, key) { + this._delegate.updated(object, key); + } + + // + + get _arrayObserverOptions() { + return { + willChange: this._arrayWillChange, + didChange: this._arrayDidChange + }; + } + + _startObserving() { + let array = this._array; + array.addArrayObserver(this, this._arrayObserverOptions); + this._startObservingObjects(array); + } + + _stopObserving() { + let array = this._array; + array.removeArrayObserver(this, this._arrayObserverOptions); + this._stopObservingObjects(array); + } + + _arrayWillChange(array, start, removeCount) { + if(removeCount) { + let removing = A(array.slice(start, start + removeCount)); + this._delegate.removed(removing, start, removeCount); + this._stopObservingObjects(removing); + } + } + + _arrayDidChange(array, start, removeCount, addCount) { + if(addCount) { + let adding = A(array.slice(start, start + addCount)); + this._delegate.added(adding, start, addCount); + this._startObservingObjects(adding); + } + } + + destroy() { + if(this.isDestroyed) { + return; + } + this.isDestroyed = true; + this._stopObserving(); + } + +} diff --git a/tests/unit/less-experimental-array-observer-test.js b/tests/unit/less-experimental-array-observer-test.js new file mode 100644 index 00000000..9a2190bd --- /dev/null +++ b/tests/unit/less-experimental-array-observer-test.js @@ -0,0 +1,110 @@ +import EmberObject from '@ember/object'; +import { module, test, setupStoreTest } from '../helpers/setup'; +import ArrayObserver from 'ember-cli-zuglet/-private/less-experimental/util/array-observer'; +import { A } from '@ember/array'; + +module('less-experimental-array-observer', function(hooks) { + setupStoreTest(hooks); + + hooks.beforeEach(async function() { + this.object = props => EmberObject.create(props); + this.observer = (array, keys) => { + let log = A(); + let observer = new ArrayObserver({ + array, + observe: keys, + delegate: { + added: (objects, start, count) => { + log.push([ 'added', objects, start, count ]); + }, + removed: (objects, start, count) => { + log.push([ 'removed', objects, start, count ]); + }, + updated: (o, key) => { + log.push([ 'updated', o, key ]) + } + } + }); + + return { log, observer }; + }; + }); + + test('observation not enabled if there is no keys to observe', async function(assert) { + let array = A(); + let { log, observer } = this.observer(array, []); + + assert.equal(observer._enabled, false); + + observer.destroy(); + assert.equal(log.length, 0); + }); + + test('enabled if there are observation keys', async function(assert) { + let array = A(); + let { log, observer } = this.observer(array, [ 'name', 'color' ]); + + assert.equal(observer._enabled, true); + + observer.destroy(); + assert.equal(log.length, 0); + }); + + test('object added and removed', async function(assert) { + let array = A(); + + let { log, observer } = this.observer(array, [ 'name', 'color' ]); + + let duck = this.object({ name: 'duck' }); + let hamster = this.object({ name: 'hamster' }); + let otter = this.object({ name: 'otter' }); + + array.pushObject(duck); + array.pushObjects([ hamster ]); + array.clear(); + array.pushObject(otter); + array.pushObjects([ duck, hamster ]); + array.clear(); + + assert.deepEqual(log, [ + [ 'added', [ duck ], 0, 1 ], + [ 'added', [ hamster ], 1, 1 ], + [ 'removed', [ duck, hamster ], 0, 2 ], + [ 'added', [ otter ], 0, 1 ], + [ 'added', [ duck, hamster ], 1, 2 ], + [ 'removed', [ otter, duck, hamster ], 0, 3 ], + ]); + + observer.destroy(); + }); + + test('added objects are observed', async function(assert) { + let array = A(); + + let { log, observer } = this.observer(array, [ 'name', 'color' ]); + + let duck = this.object({ name: 'duck' }); + + array.pushObject(duck); + duck.set('name', 'ducky'); + array.removeAt(0); + + duck.set('name', 'duck'); + + array.pushObject(duck); + duck.set('name', 'ducky'); + + observer.destroy(); + + duck.set('name', 'duck'); + + assert.deepEqual(log, [ + [ 'added', [ duck ], 0, 1 ], + [ 'updated', duck, 'name' ], + [ 'removed', [ duck ], 0, 1 ], + [ 'added', [ duck ], 0, 1 ], + [ 'updated', duck, 'name' ] + ]); + }); + +}); From aefc1b750fb8dc07e6351b1aedee0e7fc617479c Mon Sep 17 00:00:00 2001 From: ampatspell Date: Thu, 9 Aug 2018 19:47:21 +0300 Subject: [PATCH 06/63] named and source as resolvable values --- .../less-experimental/models/index.js | 26 ++++++++++++++++--- .../less-experimental/models/property.js | 3 +-- 2 files changed, 23 insertions(+), 6 deletions(-) diff --git a/addon/-private/less-experimental/models/index.js b/addon/-private/less-experimental/models/index.js index 1ac6f29d..5e5bf9e7 100644 --- a/addon/-private/less-experimental/models/index.js +++ b/addon/-private/less-experimental/models/index.js @@ -4,25 +4,43 @@ import property from './property'; const compact = array => A(array).compact(); +// foo('docs') +// foo('type', owner => owner.type) +const key = args => { + args = compact(args); + if(args.length === 1) { + return { dependencies: args, key: args[0] }; + } else if(args.length > 1) { + let last = args[args.length - 1]; + if(typeof last === 'function') { + let key = args.pop(); + return { dependencies: args, key }; + } + } + return undefined; +} + const build = (opts, nested={}) => { opts = assign({}, opts, nested); let prop = property(opts); prop.owner = (...args) => build(opts, { owner: compact(args) }); prop.object = (...args) => build(opts, { object: compact(args) }); prop.inline = arg => build(opts, { inline: arg }); - prop.named = arg => build(opts, { named: arg }); + prop.named = (...args) => build(opts, { named: key(args) }); prop.mapping = arg => build(opts, { mapping: arg }); + prop.source = (...args) => build(opts, { source: key(args) }); return prop; } -export default source => { +export default (...args) => { let opts = { - source, + source: undefined, owner: [], object: [], inline: undefined, named: undefined, mapping: undefined }; - return build(opts); + + return build(opts, {}).source(...args); } diff --git a/addon/-private/less-experimental/models/property.js b/addon/-private/less-experimental/models/property.js index 22763af3..579e2ef9 100644 --- a/addon/-private/less-experimental/models/property.js +++ b/addon/-private/less-experimental/models/property.js @@ -12,8 +12,7 @@ const create = opts => function(key) { } export default opts => { - let dependencies = A().compact(); - return destroyable(...dependencies, { + return destroyable({ create: create(opts), reusable, get From 7e4e3e8670739179bc3240832ced63dada33a195 Mon Sep 17 00:00:00 2001 From: ampatspell Date: Thu, 9 Aug 2018 19:58:04 +0300 Subject: [PATCH 07/63] have a models runtime as an es6 class --- .../less-experimental/models/internal.js | 15 +++++++--- .../less-experimental/models/runtime.js | 14 +++++++++ tests/unit/less-experimental-models-test.js | 30 ++++++++++++++++--- 3 files changed, 51 insertions(+), 8 deletions(-) create mode 100644 addon/-private/less-experimental/models/runtime.js diff --git a/addon/-private/less-experimental/models/internal.js b/addon/-private/less-experimental/models/internal.js index ea3a4c34..b93e357b 100644 --- a/addon/-private/less-experimental/models/internal.js +++ b/addon/-private/less-experimental/models/internal.js @@ -1,6 +1,7 @@ import Internal from '../../internal/internal'; import { getOwner } from '@ember/application'; import { A } from '@ember/array'; +import Runtime from './runtime'; export default Internal.extend({ @@ -8,18 +9,24 @@ export default Internal.extend({ key: null, opts: null, // { source, owner, object, inline, named, mapping } - init() { - this._super(...arguments); - this.content = A(); + runtime(create) { + let runtime = this._runtime; + if(!runtime && create) { + let { parent, key, opts } = this.getProperties('parent', 'key', 'opts'); + runtime = new Runtime(parent, key, opts); + this._runtime = runtime; + } + return runtime; }, createModel() { - let content = this.content; + let content = this.runtime(true).content; return getOwner(this.parent).factoryFor('zuglet:less-experimental/models').create({ _internal: this, content }); }, willDestroy() { this._super(...arguments); + this._runtime && this._runtime.destroy(); } }); diff --git a/addon/-private/less-experimental/models/runtime.js b/addon/-private/less-experimental/models/runtime.js new file mode 100644 index 00000000..e2e6f520 --- /dev/null +++ b/addon/-private/less-experimental/models/runtime.js @@ -0,0 +1,14 @@ +export default class ModelsRuntime { + + constructor(parent, key, opts) { + this.parent = parent; + this.key = key; + this.opts = opts; + console.log('init', this); + } + + destroy() { + console.log('destroy', this); + } + +} diff --git a/tests/unit/less-experimental-models-test.js b/tests/unit/less-experimental-models-test.js index ce5437b2..8ced3211 100644 --- a/tests/unit/less-experimental-models-test.js +++ b/tests/unit/less-experimental-models-test.js @@ -3,6 +3,7 @@ import { module, test, setupStoreTest } from '../helpers/setup'; import { getOwner } from '@ember/application'; import { models } from 'ember-cli-zuglet/less-experimental'; import { A } from '@ember/array'; +import { run } from '@ember/runloop'; const Owner = EmberObject.extend({ }); @@ -12,6 +13,7 @@ module('less-experimental-models', function(hooks) { hooks.beforeEach(async function() { this.getOwner = () => getOwner(this.store); + this.object = props => EmberObject.create(props); this.registerModel = (name, factory) => this.getOwner().register(`model:${name}`, factory); this.subject = (props, name='subject') => { let factory = Owner.extend(props); @@ -22,14 +24,34 @@ module('less-experimental-models', function(hooks) { }; }); - test('model is created', async function(assert) { + test('acceptance', async function(assert) { + let duck = this.object({ name: 'duck' }); + let hamster = this.object({ name: 'hamster' }); + let otter = this.object({ name: 'otter' }); + let subject = this.subject({ - docs: A(), - models: models('docs').inline({ + + all: A([ duck, hamster, otter ]), + selected: A([ duck, otter ]), + type: 'all', + + models: models('type', owner => owner.type).object('name').inline({ + prepare(object) { + this.setProperties({ name: object.name }); + } }) + }); - assert.ok(subject.models); + assert.deepEqual(subject.models.mapBy('name'), [ 'duck', 'hamster', 'otter' ]); + + subject.set('type', 'selected'); + + assert.deepEqual(subject.models.mapBy('name'), [ 'duck', 'otter' ]); + + // hamster destroy + + run(() => subject.destroy()); }); }); From c8487a91c7b7723df96481e9217db82558d20918 Mon Sep 17 00:00:00 2001 From: ampatspell Date: Thu, 9 Aug 2018 19:59:31 +0300 Subject: [PATCH 08/63] move content array to runtime --- addon/-private/less-experimental/models/internal.js | 1 - addon/-private/less-experimental/models/runtime.js | 3 +++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/addon/-private/less-experimental/models/internal.js b/addon/-private/less-experimental/models/internal.js index b93e357b..c255924d 100644 --- a/addon/-private/less-experimental/models/internal.js +++ b/addon/-private/less-experimental/models/internal.js @@ -1,6 +1,5 @@ import Internal from '../../internal/internal'; import { getOwner } from '@ember/application'; -import { A } from '@ember/array'; import Runtime from './runtime'; export default Internal.extend({ diff --git a/addon/-private/less-experimental/models/runtime.js b/addon/-private/less-experimental/models/runtime.js index e2e6f520..a7898ad7 100644 --- a/addon/-private/less-experimental/models/runtime.js +++ b/addon/-private/less-experimental/models/runtime.js @@ -1,9 +1,12 @@ +import { A } from '@ember/array'; + export default class ModelsRuntime { constructor(parent, key, opts) { this.parent = parent; this.key = key; this.opts = opts; + this.content = A([]); console.log('init', this); } From e4801cf4b446c373a021b00b91f7b910efff8d4a Mon Sep 17 00:00:00 2001 From: ampatspell Date: Thu, 9 Aug 2018 20:15:43 +0300 Subject: [PATCH 09/63] add object observer which observes parent keys --- .../-private/less-experimental/models/index.js | 4 ++-- .../less-experimental/models/runtime.js | 17 +++++++++++++++++ 2 files changed, 19 insertions(+), 2 deletions(-) diff --git a/addon/-private/less-experimental/models/index.js b/addon/-private/less-experimental/models/index.js index 5e5bf9e7..af72ad8b 100644 --- a/addon/-private/less-experimental/models/index.js +++ b/addon/-private/less-experimental/models/index.js @@ -23,7 +23,7 @@ const key = args => { const build = (opts, nested={}) => { opts = assign({}, opts, nested); let prop = property(opts); - prop.owner = (...args) => build(opts, { owner: compact(args) }); + prop.owner = (...args) => build(opts, { parent: compact(args) }); prop.object = (...args) => build(opts, { object: compact(args) }); prop.inline = arg => build(opts, { inline: arg }); prop.named = (...args) => build(opts, { named: key(args) }); @@ -35,7 +35,7 @@ const build = (opts, nested={}) => { export default (...args) => { let opts = { source: undefined, - owner: [], + parent: [], object: [], inline: undefined, named: undefined, diff --git a/addon/-private/less-experimental/models/runtime.js b/addon/-private/less-experimental/models/runtime.js index a7898ad7..f5b806d8 100644 --- a/addon/-private/less-experimental/models/runtime.js +++ b/addon/-private/less-experimental/models/runtime.js @@ -1,4 +1,5 @@ import { A } from '@ember/array'; +import ObjectObserver from '../util/object-observer'; export default class ModelsRuntime { @@ -7,11 +8,27 @@ export default class ModelsRuntime { this.key = key; this.opts = opts; this.content = A([]); + this.isDestroyed = false; console.log('init', this); + this.parentObserver = this.createParentObserver(); + } + + createParentObserver() { + let { parent: object, opts } = this; + let observe = A([ ...opts.source.dependencies, ...opts.parent ]).uniq(); + let updated = (object, key) => { + console.log('parentKeyDidChange', key); + }; + return new ObjectObserver({ object, observe, delegate: { updated } }); } destroy() { console.log('destroy', this); + if(this.isDestroyed) { + return; + } + this.isDestroyed = true; + this.parentObserver.destroy(); } } From 143f22d065f3fcc6216b1beacbb5853cb0d54ba0 Mon Sep 17 00:00:00 2001 From: ampatspell Date: Thu, 9 Aug 2018 20:48:53 +0300 Subject: [PATCH 10/63] add ValueProvider which observes object and holds current value for key (string / fn) --- .../less-experimental/util/value-provider.js | 54 +++++++++++++++++++ 1 file changed, 54 insertions(+) create mode 100644 addon/-private/less-experimental/util/value-provider.js diff --git a/addon/-private/less-experimental/util/value-provider.js b/addon/-private/less-experimental/util/value-provider.js new file mode 100644 index 00000000..4ffed791 --- /dev/null +++ b/addon/-private/less-experimental/util/value-provider.js @@ -0,0 +1,54 @@ +import ObjectObserver from './object-observer'; +import { get } from '@ember/object'; + +export default class ValueProvider { + + // dependencies: array or keys + // key: function or string + // delegate: { updated } + constructor({ object, dependencies, key, delegate }) { + this.object = object; + this.dependencies = dependencies; + this.key = key; + this.delegate = delegate; + this.observe = typeof key === 'function'; + if(this.observe) { + this.observer = new ObjectObserver({ + object, + observe: dependencies, + delegate: { + updated: () => this.update() + } + }); + } + this.update(); + } + + update() { + let object = this.object; + + let key; + if(this.observe) { + key = this.key(object); + } else { + key = this.key; + } + + let value; + if(key) { + value = get(object, key); + } + + if(this.value === value) { + return; + } + + this.value = value; + this.delegate.updated(value); + } + + destroy() { + this.observer && this.observer.destroy(); + } + +} From ceee3ccbce69b96e37818cac47577641c4e21c11 Mon Sep 17 00:00:00 2001 From: ampatspell Date: Thu, 9 Aug 2018 21:08:15 +0300 Subject: [PATCH 11/63] cleanup value-provider --- .../less-experimental/util/value-provider.js | 27 ++++++++++--------- 1 file changed, 15 insertions(+), 12 deletions(-) diff --git a/addon/-private/less-experimental/util/value-provider.js b/addon/-private/less-experimental/util/value-provider.js index 4ffed791..77dd4c30 100644 --- a/addon/-private/less-experimental/util/value-provider.js +++ b/addon/-private/less-experimental/util/value-provider.js @@ -3,32 +3,32 @@ import { get } from '@ember/object'; export default class ValueProvider { - // dependencies: array or keys + // object + // observe: array or keys // key: function or string - // delegate: { updated } - constructor({ object, dependencies, key, delegate }) { + // delegate: { updated(value) } + constructor({ object, observe, key, delegate }) { this.object = object; - this.dependencies = dependencies; this.key = key; this.delegate = delegate; - this.observe = typeof key === 'function'; - if(this.observe) { + this.resolve = typeof key === 'function'; + if(this.resolve) { this.observer = new ObjectObserver({ object, - observe: dependencies, + observe, delegate: { - updated: () => this.update() + updated: () => this.update(true) } }); } - this.update(); + this.update(false); } - update() { + update(notify) { let object = this.object; let key; - if(this.observe) { + if(this.resolve) { key = this.key(object); } else { key = this.key; @@ -44,7 +44,10 @@ export default class ValueProvider { } this.value = value; - this.delegate.updated(value); + + if(notify) { + this.delegate.updated(value); + } } destroy() { From abf59c5e67f04b4ca028a6ea5e136b33f496fb2e Mon Sep 17 00:00:00 2001 From: ampatspell Date: Thu, 9 Aug 2018 21:08:22 +0300 Subject: [PATCH 12/63] array observer docs --- addon/-private/less-experimental/util/array-observer.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/addon/-private/less-experimental/util/array-observer.js b/addon/-private/less-experimental/util/array-observer.js index 4dbd9bcb..7eb6749c 100644 --- a/addon/-private/less-experimental/util/array-observer.js +++ b/addon/-private/less-experimental/util/array-observer.js @@ -8,7 +8,7 @@ export default class ArrayObserver { // array: Enumerable // observe: [ 'id', 'type' ] - // delegate: { removed(array), added(array), updated(object, key) } + // delegate: { removed(objects, start, len), added(objects, start, len), updated(object, key) } constructor({ array, observe, delegate }) { assert(`array must be array`, typeOf(array) === 'array'); assert(`observe must be array`, typeOf(observe) === 'array'); From b29702f3b4d987530ac8687cc8170cc40ebb45c0 Mon Sep 17 00:00:00 2001 From: ampatspell Date: Thu, 9 Aug 2018 21:08:43 +0300 Subject: [PATCH 13/63] source manager which observes source change, observes array contents --- .../models/runtime/source.js | 56 +++++++++++++++++++ 1 file changed, 56 insertions(+) create mode 100644 addon/-private/less-experimental/models/runtime/source.js diff --git a/addon/-private/less-experimental/models/runtime/source.js b/addon/-private/less-experimental/models/runtime/source.js new file mode 100644 index 00000000..7d240f02 --- /dev/null +++ b/addon/-private/less-experimental/models/runtime/source.js @@ -0,0 +1,56 @@ +import ValueProvider from '../../util/value-provider'; +import ArrayObserver from '../../util/array-observer'; + +export default class SourceManager { + + // parent + // source: { dependencies, key } + constructor({ parent, source }) { + this.parent = parent; + this.provider = new ValueProvider({ + object: parent, + observe: source.dependencies, + key: source.key, + delegate: { + updated: value => this.update(true) + } + }); + this.source = this.provider.value; + this.update(false); + } + + update(notify) { + let observer = this.observer; + if(observer) { + observer.destroy(); + this.observer = null; + } + + let source = this.source; + + if(source) { + this.observer = new ArrayObserver({ + array: source, + observe: [], + delegate: { + added: (objects, start, len) => { + console.log('source objects added', objects.slice(), start, len); + }, + removed: (objects, start, len) => { + console.log('source objects removed', objects.slice(), start, len); + }, + updated: (object, key) => { + console.log('source object updated', object, key); + } + } + }); + } + + console.log('source replaced', source); + } + + destroy() { + this.provider.destroy(); + } + +} From 849d44f79db534a5a877e0d9f3e491afe5931218 Mon Sep 17 00:00:00 2001 From: ampatspell Date: Thu, 9 Aug 2018 21:09:01 +0300 Subject: [PATCH 14/63] sync --- .../less-experimental/models/runtime.js | 21 +++--------- .../models/runtime/parent.js | 21 ++++++++++++ tests/unit/less-experimental-models-test.js | 32 ++++++++++++++++++- 3 files changed, 56 insertions(+), 18 deletions(-) create mode 100644 addon/-private/less-experimental/models/runtime/parent.js diff --git a/addon/-private/less-experimental/models/runtime.js b/addon/-private/less-experimental/models/runtime.js index f5b806d8..ed191415 100644 --- a/addon/-private/less-experimental/models/runtime.js +++ b/addon/-private/less-experimental/models/runtime.js @@ -1,5 +1,6 @@ import { A } from '@ember/array'; -import ObjectObserver from '../util/object-observer'; +// import ParentManager from './runtime/parent'; +import SourceManager from './runtime/source'; export default class ModelsRuntime { @@ -8,27 +9,13 @@ export default class ModelsRuntime { this.key = key; this.opts = opts; this.content = A([]); - this.isDestroyed = false; console.log('init', this); - this.parentObserver = this.createParentObserver(); - } - - createParentObserver() { - let { parent: object, opts } = this; - let observe = A([ ...opts.source.dependencies, ...opts.parent ]).uniq(); - let updated = (object, key) => { - console.log('parentKeyDidChange', key); - }; - return new ObjectObserver({ object, observe, delegate: { updated } }); + this.sourceManager = new SourceManager({ parent, source: opts.source }); } destroy() { console.log('destroy', this); - if(this.isDestroyed) { - return; - } - this.isDestroyed = true; - this.parentObserver.destroy(); + this.sourceManager.destroy(); } } diff --git a/addon/-private/less-experimental/models/runtime/parent.js b/addon/-private/less-experimental/models/runtime/parent.js new file mode 100644 index 00000000..0f231047 --- /dev/null +++ b/addon/-private/less-experimental/models/runtime/parent.js @@ -0,0 +1,21 @@ +import ObjectObserver from '../../util/object-observer'; +import { A } from '@ember/array'; + +export default class ParentManager { + + constructor(object, key, opts) { + this.object = object; + this.key = key; + this.opts = opts; + let observe = A([ ...opts.source.dependencies, ...opts.parent ]).uniq(); + let updated = (object, key) => { + console.log('parentKeyDidChange', key); + }; + this.observer = new ObjectObserver({ object, observe, delegate: { updated } }); + } + + destroy() { + this.observer.destroy(); + } + +} diff --git a/tests/unit/less-experimental-models-test.js b/tests/unit/less-experimental-models-test.js index 8ced3211..12408886 100644 --- a/tests/unit/less-experimental-models-test.js +++ b/tests/unit/less-experimental-models-test.js @@ -24,7 +24,7 @@ module('less-experimental-models', function(hooks) { }; }); - test('acceptance', async function(assert) { + test('source change', async function(assert) { let duck = this.object({ name: 'duck' }); let hamster = this.object({ name: 'hamster' }); let otter = this.object({ name: 'otter' }); @@ -54,4 +54,34 @@ module('less-experimental-models', function(hooks) { run(() => subject.destroy()); }); + test('source content mutations', async function(assert) { + let duck = this.object({ name: 'duck' }); + let hamster = this.object({ name: 'hamster' }); + let otter = this.object({ name: 'otter' }); + + let subject = this.subject({ + + all: A([ otter ]), + + models: models('all').object('name').inline({ + prepare(object) { + this.setProperties({ name: object.name }); + } + }) + + }); + + assert.deepEqual(subject.models.mapBy('name'), [ 'otter' ]); + + subject.get('all').insertAt(0, duck); + + assert.deepEqual(subject.models.mapBy('name'), [ 'duck', 'otter' ]); + + subject.get('all').removeAt(0); + + assert.deepEqual(subject.models.mapBy('name'), [ 'otter' ]); + + run(() => subject.destroy()); + }); + }); From 4efc1232da5ff92c1d80625e3e460f90bbb68716 Mon Sep 17 00:00:00 2001 From: ampatspell Date: Thu, 9 Aug 2018 21:19:51 +0300 Subject: [PATCH 15/63] validate functions, expose source manager delegate --- .../less-experimental/models/runtime.js | 19 +++++++++- .../models/runtime/source.js | 35 +++++++++++++------ .../less-experimental/util/array-observer.js | 16 +++++---- .../less-experimental/util/object-observer.js | 12 ++++--- .../less-experimental/util/value-provider.js | 11 ++++++ 5 files changed, 71 insertions(+), 22 deletions(-) diff --git a/addon/-private/less-experimental/models/runtime.js b/addon/-private/less-experimental/models/runtime.js index ed191415..f4294cec 100644 --- a/addon/-private/less-experimental/models/runtime.js +++ b/addon/-private/less-experimental/models/runtime.js @@ -10,7 +10,24 @@ export default class ModelsRuntime { this.opts = opts; this.content = A([]); console.log('init', this); - this.sourceManager = new SourceManager({ parent, source: opts.source }); + this.sourceManager = new SourceManager({ + parent, + source: opts.source, + delegate: { + replaced(array) { + console.log('source replaced', array); + }, + added(objects, start, len) { + console.log('source objects added', objects.slice(), start, len); + }, + removed(objects, start, len) { + console.log('source objects removed', objects.slice(), start, len); + }, + updated(object, key) { + console.log('source object updated', object, key); + } + } + }); } destroy() { diff --git a/addon/-private/less-experimental/models/runtime/source.js b/addon/-private/less-experimental/models/runtime/source.js index 7d240f02..24536c72 100644 --- a/addon/-private/less-experimental/models/runtime/source.js +++ b/addon/-private/less-experimental/models/runtime/source.js @@ -1,12 +1,29 @@ import ValueProvider from '../../util/value-provider'; import ArrayObserver from '../../util/array-observer'; +import { assert } from '@ember/debug'; +import { typeOf } from '@ember/utils'; + +const validate = (parent, source, delegate) => { + assert(`parent is required`, !!parent); + assert(`source must be object`, typeOf(source) === 'object'); + assert(`source.dependencies must be array`, typeOf(source.dependencies) === 'array'); + assert(`source.key is required`, !!source.key); + assert(`delegate is required`, !!delegate); + assert(`delegate.replaced must be function`, typeOf(delegate.replaced) === 'function'); + assert(`delegate.added must be function`, typeOf(delegate.added) === 'function'); + assert(`delegate.removed must be function`, typeOf(delegate.removed) === 'function'); + assert(`delegate.updated must be function`, typeOf(delegate.updated) === 'function'); +} export default class SourceManager { // parent // source: { dependencies, key } - constructor({ parent, source }) { + // delegate: { replaced, added, removed, updated } + constructor({ parent, source, delegate }) { + validate(parent, source, delegate); this.parent = parent; + this.delegate = delegate; this.provider = new ValueProvider({ object: parent, observe: source.dependencies, @@ -33,20 +50,16 @@ export default class SourceManager { array: source, observe: [], delegate: { - added: (objects, start, len) => { - console.log('source objects added', objects.slice(), start, len); - }, - removed: (objects, start, len) => { - console.log('source objects removed', objects.slice(), start, len); - }, - updated: (object, key) => { - console.log('source object updated', object, key); - } + added: (objects, start, len) => this.delegate.added(objects, start, len), + removed: (objects, start, len) => this.delegate.removed(objects, start, len), + updated: (object, key) => this.delegate.updated(object, key) } }); } - console.log('source replaced', source); + if(notify) { + this.delegate.replaced(source); + } } destroy() { diff --git a/addon/-private/less-experimental/util/array-observer.js b/addon/-private/less-experimental/util/array-observer.js index 7eb6749c..e6ea6804 100644 --- a/addon/-private/less-experimental/util/array-observer.js +++ b/addon/-private/less-experimental/util/array-observer.js @@ -4,18 +4,22 @@ import { assert } from '@ember/debug'; import { typeOf } from '@ember/utils'; import { get } from '@ember/object'; +const validate = (array, observe, delegate) => { + assert(`array must be array`, typeOf(array) === 'array'); + assert(`observe must be array`, typeOf(observe) === 'array'); + assert(`delegate is required`, !!delegate); + assert(`delegate.added must be function`, typeOf(delegate.added) === 'function'); + assert(`delegate.removed must be function`, typeOf(delegate.removed) === 'function'); + assert(`delegate.updated must be function`, typeOf(delegate.updated) === 'function'); +} + export default class ArrayObserver { // array: Enumerable // observe: [ 'id', 'type' ] // delegate: { removed(objects, start, len), added(objects, start, len), updated(object, key) } constructor({ array, observe, delegate }) { - assert(`array must be array`, typeOf(array) === 'array'); - assert(`observe must be array`, typeOf(observe) === 'array'); - assert(`delegate is required`, !!delegate); - assert(`delegate.added must be function`, typeOf(delegate.added) === 'function'); - assert(`delegate.removed must be function`, typeOf(delegate.removed) === 'function'); - assert(`delegate.updated must be function`, typeOf(delegate.updated) === 'function'); + validate(array, observe, delegate); this._array = array; this._observe = observe; this._delegate = delegate; diff --git a/addon/-private/less-experimental/util/object-observer.js b/addon/-private/less-experimental/util/object-observer.js index 8e6c1a25..1a44b9fd 100644 --- a/addon/-private/less-experimental/util/object-observer.js +++ b/addon/-private/less-experimental/util/object-observer.js @@ -26,16 +26,20 @@ export const stopObservingObjects = (objects, keys, target, method) => { objects.map(object => stopObservingObject(object, keys, target, method)); } +const validate = (object, observe, delegate) => { + assert(`object is required`, !!object); + assert(`observe must be array`, typeOf(observe) === 'array'); + assert(`delegate is required`, !!delegate); + assert(`delegate.updated must be function`, typeOf(delegate.updated) === 'function'); +} + export default class ObjectObserver { // object: EmberObject // observe: [ 'id', 'type' ] // delegate: { updated(object, key) } constructor({ object, observe, delegate }) { - assert(`object is required`, !!object); - assert(`observe must be array`, typeOf(observe) === 'array'); - assert(`delegate is required`, !!delegate); - assert(`delegate.updated must be function`, typeOf(delegate.updated) === 'function'); + validate(object, observe, delegate); this._object = object; this._observe = observe; this._delegate = delegate; diff --git a/addon/-private/less-experimental/util/value-provider.js b/addon/-private/less-experimental/util/value-provider.js index 77dd4c30..9850a98a 100644 --- a/addon/-private/less-experimental/util/value-provider.js +++ b/addon/-private/less-experimental/util/value-provider.js @@ -1,5 +1,15 @@ import ObjectObserver from './object-observer'; import { get } from '@ember/object'; +import { assert } from '@ember/debug'; +import { typeOf } from '@ember/utils'; + +const validate = (object, observe, key, delegate) => { + assert(`object is required`, !!object); + assert(`observe must be array`, typeOf(observe) === 'array'); + assert(`key must be string or function`, typeOf(key) === 'function' || typeOf(key) === 'string'); + assert(`delegate is required`, !!delegate); + assert(`delegate.updated must be function`, typeOf(delegate.updated) === 'function'); +} export default class ValueProvider { @@ -8,6 +18,7 @@ export default class ValueProvider { // key: function or string // delegate: { updated(value) } constructor({ object, observe, key, delegate }) { + validate(object, observe, key, delegate); this.object = object; this.key = key; this.delegate = delegate; From 8bc9c36be73ca6894b24de462a5bef21741f2824 Mon Sep 17 00:00:00 2001 From: ampatspell Date: Thu, 9 Aug 2018 21:56:10 +0300 Subject: [PATCH 16/63] partial model factory implementation --- .../less-experimental/models/runtime.js | 64 +++++++++++++++---- .../models/runtime/parent.js | 31 ++++++--- .../less-experimental/util/model-factory.js | 63 ++++++++++++++++++ 3 files changed, 135 insertions(+), 23 deletions(-) create mode 100644 addon/-private/less-experimental/util/model-factory.js diff --git a/addon/-private/less-experimental/models/runtime.js b/addon/-private/less-experimental/models/runtime.js index f4294cec..0bbc13a9 100644 --- a/addon/-private/less-experimental/models/runtime.js +++ b/addon/-private/less-experimental/models/runtime.js @@ -1,6 +1,7 @@ import { A } from '@ember/array'; -// import ParentManager from './runtime/parent'; +import ParentManager from './runtime/parent'; import SourceManager from './runtime/source'; +import ModelFactory from '../util/model-factory'; export default class ModelsRuntime { @@ -10,28 +11,65 @@ export default class ModelsRuntime { this.opts = opts; this.content = A([]); console.log('init', this); + + this.modelFactory = new ModelFactory({ + parent, + key, + inline: opts.inline, + named: opts.named, + mapping: opts.mapping, + prepare: object => [ object, parent ], + }); + + this.parentManager = new ParentManager({ + parent, + observe: opts.parent, + delegate: { + updated: (object, key) => this.onParentPropertyUpdated(object, key) + } + }); + this.sourceManager = new SourceManager({ parent, source: opts.source, delegate: { - replaced(array) { - console.log('source replaced', array); - }, - added(objects, start, len) { - console.log('source objects added', objects.slice(), start, len); - }, - removed(objects, start, len) { - console.log('source objects removed', objects.slice(), start, len); - }, - updated(object, key) { - console.log('source object updated', object, key); - } + replaced: array => this.onSourceArrayReplaced(array), + added: (objects, start, len) => this.onSourceObjectsAdded(objects, start, len), + removed: (objects, start, len) => this.onSourceObjectsRemoved(objects, start, len), + updated: (object, key) => this.onSourceObjectUpdated(object, key) } }); } + onSourceArrayReplaced(source) { + console.log('source array replaced', source.slice()); + if(source) { + let factory = this.modelFactory; + source.map(object => { + let { model, promise } = factory.create(object); + }); + } + } + + onSourceObjectsAdded(objects, start, len) { + console.log('source objects added', objects.slice(), start, len); + } + + onSourceObjectsRemoved(objects, start, len) { + console.log('source objects removed', objects.slice(), start, len); + } + + onSourceObjectUpdated(object, key) { + console.log('source object updated', object, key); + } + + onParentPropertyUpdated(object, key) { + console.log('parent updated', object, key); + } + destroy() { console.log('destroy', this); + this.parentManager.destroy(); this.sourceManager.destroy(); } diff --git a/addon/-private/less-experimental/models/runtime/parent.js b/addon/-private/less-experimental/models/runtime/parent.js index 0f231047..7e16170b 100644 --- a/addon/-private/less-experimental/models/runtime/parent.js +++ b/addon/-private/less-experimental/models/runtime/parent.js @@ -1,17 +1,28 @@ import ObjectObserver from '../../util/object-observer'; -import { A } from '@ember/array'; +import { assert } from '@ember/debug'; +import { typeOf } from '@ember/utils'; + +const validate = (parent, observe, delegate) => { + assert(`parent is required`, !!parent); + assert(`observe must be array`, typeOf(observe) === 'array'); + assert(`delegate is required`, !!delegate); + assert(`delegate.updated must be function`, typeOf(delegate.updated) === 'function'); +} export default class ParentManager { - constructor(object, key, opts) { - this.object = object; - this.key = key; - this.opts = opts; - let observe = A([ ...opts.source.dependencies, ...opts.parent ]).uniq(); - let updated = (object, key) => { - console.log('parentKeyDidChange', key); - }; - this.observer = new ObjectObserver({ object, observe, delegate: { updated } }); + // parent + + constructor({ parent, observe, delegate }) { + validate(parent, observe, delegate); + this.parent = parent; + this.observer = new ObjectObserver({ + object: parent, + observe, + delegate: { + updated: (object, key) => delegate.updated(object, key) + } + }); } destroy() { diff --git a/addon/-private/less-experimental/util/model-factory.js b/addon/-private/less-experimental/util/model-factory.js new file mode 100644 index 00000000..01bf9ee0 --- /dev/null +++ b/addon/-private/less-experimental/util/model-factory.js @@ -0,0 +1,63 @@ +import { assert } from '@ember/debug'; +import { typeOf } from '@ember/utils'; +import { generateModelClass } from '../../util/model'; + +const validate = (parent, key, inline, named, mapping, prepare) => { + assert(`parent is required`, !!parent); + assert(`key is required`, typeOf(key) === 'string'); + if(inline) { + assert(`inline must be object`, typeOf(inline) === 'object'); + } + if(named) { + throw new Error('named not implemented'); + } + if(mapping) { + throw new Error('mapping not implemented'); + } + assert(`prepare must be function`, typeOf(prepare) === 'function'); +} + +export default class ModelFactory { + + constructor({ parent, key, inline, named, mapping, prepare }) { + validate(parent, key, inline, named, mapping, prepare); + this.parent = parent; + this.key = key; + this.inline = inline; + this.named = named; + this.mapping = mapping; + this.prepare = prepare; + } + + createProcess() { + let { parent, key, inline } = this; + if(inline) { + let modelClass = generateModelClass(parent, key, inline); + return props => modelClass.create(props); + } + throw new Error('not implemented'); + } + + get factory() { + let process = this._process; + if(!process) { + process = this.createProcess(); + this._process = process; + } + return process; + } + + prepare(model, ...args) { + let prepare = this.prepare(...args); + assert(`'prepare' function is required for ${instance}`, typeOf(instance.prepare) === 'function'); + return model.prepare(...prepare); + } + + create(...args) { + let factory = this.factory; + let model = factory(); + let promise = this.prepare(model, ...args); + return { model, promise }; + } + +} From 68290c1726bdfa21d307d19e8f46fce4186baf50 Mon Sep 17 00:00:00 2001 From: ampatspell Date: Thu, 9 Aug 2018 22:28:45 +0300 Subject: [PATCH 17/63] tests pass. basics implemented --- .../less-experimental/models/runtime.js | 53 +++++++++++++++---- .../models/runtime/source.js | 5 +- .../less-experimental/util/model-factory.js | 6 +-- tests/unit/less-experimental-models-test.js | 37 +++++++++---- 4 files changed, 77 insertions(+), 24 deletions(-) diff --git a/addon/-private/less-experimental/models/runtime.js b/addon/-private/less-experimental/models/runtime.js index 0bbc13a9..0548d10c 100644 --- a/addon/-private/less-experimental/models/runtime.js +++ b/addon/-private/less-experimental/models/runtime.js @@ -39,24 +39,56 @@ export default class ModelsRuntime { updated: (object, key) => this.onSourceObjectUpdated(object, key) } }); + + this.rebuildModels(); } - onSourceArrayReplaced(source) { - console.log('source array replaced', source.slice()); - if(source) { - let factory = this.modelFactory; - source.map(object => { - let { model, promise } = factory.create(object); - }); + // + + + createModels(objects) { + let factory = this.modelFactory; + return objects.map(object => { + let { model } = factory.create(object); + return model; + }); + } + + replaceModels(start, remove, models) { + let removed; + let content = this.content; + if(remove) { + removed = content.slice(start, start + remove); + } + content.replace(start, remove, models); + if(removed) { + removed.map(model => model.destroy()); } } - onSourceObjectsAdded(objects, start, len) { - console.log('source objects added', objects.slice(), start, len); + rebuildModels() { + let objects = this.sourceManager.source; + let models; + if(objects) { + models = this.createModels(objects); + } + let remove = this.content.get('length'); + this.replaceModels(0, remove, models); + } + + // + + onSourceArrayReplaced(soure) { + this.rebuildModels(); + } + + onSourceObjectsAdded(objects, start) { + let models = this.createModels(objects); + this.replaceModels(start, 0, models); } onSourceObjectsRemoved(objects, start, len) { - console.log('source objects removed', objects.slice(), start, len); + this.replaceModels(start, len); } onSourceObjectUpdated(object, key) { @@ -71,6 +103,7 @@ export default class ModelsRuntime { console.log('destroy', this); this.parentManager.destroy(); this.sourceManager.destroy(); + this.content.map(model => model.destroy()); } } diff --git a/addon/-private/less-experimental/models/runtime/source.js b/addon/-private/less-experimental/models/runtime/source.js index 24536c72..e813b8c0 100644 --- a/addon/-private/less-experimental/models/runtime/source.js +++ b/addon/-private/less-experimental/models/runtime/source.js @@ -32,10 +32,13 @@ export default class SourceManager { updated: value => this.update(true) } }); - this.source = this.provider.value; this.update(false); } + get source() { + return this.provider.value; + } + update(notify) { let observer = this.observer; if(observer) { diff --git a/addon/-private/less-experimental/util/model-factory.js b/addon/-private/less-experimental/util/model-factory.js index 01bf9ee0..333d079d 100644 --- a/addon/-private/less-experimental/util/model-factory.js +++ b/addon/-private/less-experimental/util/model-factory.js @@ -47,16 +47,16 @@ export default class ModelFactory { return process; } - prepare(model, ...args) { + prepareModel(model, ...args) { let prepare = this.prepare(...args); - assert(`'prepare' function is required for ${instance}`, typeOf(instance.prepare) === 'function'); + assert(`'prepare' function is required for ${model}`, typeOf(model.prepare) === 'function'); return model.prepare(...prepare); } create(...args) { let factory = this.factory; let model = factory(); - let promise = this.prepare(model, ...args); + let promise = this.prepareModel(model, ...args); return { model, promise }; } diff --git a/tests/unit/less-experimental-models-test.js b/tests/unit/less-experimental-models-test.js index 12408886..9f7a4d95 100644 --- a/tests/unit/less-experimental-models-test.js +++ b/tests/unit/less-experimental-models-test.js @@ -43,20 +43,26 @@ module('less-experimental-models', function(hooks) { }); - assert.deepEqual(subject.models.mapBy('name'), [ 'duck', 'hamster', 'otter' ]); + assert.deepEqual(subject.get('models').mapBy('name'), [ 'duck', 'hamster', 'otter' ]); - subject.set('type', 'selected'); + let all = subject.get('models').slice(); + assert.deepEqual(all.map(m => m.isDestroying), [ false, false, false ]); - assert.deepEqual(subject.models.mapBy('name'), [ 'duck', 'otter' ]); + run(() => subject.set('type', 'selected')); - // hamster destroy + assert.deepEqual(all.map(m => m.isDestroying), [ true, true, true ]); + + assert.deepEqual(subject.get('models').mapBy('name'), [ 'duck', 'otter' ]); + + let selected = subject.get('models').slice(); run(() => subject.destroy()); + + assert.deepEqual(selected.map(m => m.isDestroying), [ true, true ]); }); test('source content mutations', async function(assert) { let duck = this.object({ name: 'duck' }); - let hamster = this.object({ name: 'hamster' }); let otter = this.object({ name: 'otter' }); let subject = this.subject({ @@ -65,23 +71,34 @@ module('less-experimental-models', function(hooks) { models: models('all').object('name').inline({ prepare(object) { - this.setProperties({ name: object.name }); + this.setProperties({ name: object.get('name') }); } }) }); - assert.deepEqual(subject.models.mapBy('name'), [ 'otter' ]); + assert.deepEqual(subject.get('models').mapBy('name'), [ 'otter' ]); + + let otterModel = subject.get('models.firstObject'); + assert.ok(otterModel); subject.get('all').insertAt(0, duck); - assert.deepEqual(subject.models.mapBy('name'), [ 'duck', 'otter' ]); + assert.deepEqual(subject.get('models').mapBy('name'), [ 'duck', 'otter' ]); + + let duckModel = subject.get('models').objectAt(0); + assert.ok(!duckModel.isDestroying); - subject.get('all').removeAt(0); + run(() => subject.get('all').removeAt(0)); + assert.ok(duckModel.isDestroying); - assert.deepEqual(subject.models.mapBy('name'), [ 'otter' ]); + assert.deepEqual(subject.get('models').mapBy('name'), [ 'otter' ]); + + assert.ok(subject.get('models.firstObject') === otterModel); run(() => subject.destroy()); + + assert.ok(otterModel.isDestroying); }); }); From d7b61070d798788423a50b9d63582e6baa0f8f74 Mon Sep 17 00:00:00 2001 From: ampatspell Date: Thu, 9 Aug 2018 22:58:38 +0300 Subject: [PATCH 18/63] object deps recreate models --- .../less-experimental/models/runtime.js | 12 ++++++-- .../models/runtime/source.js | 14 +++++---- tests/unit/less-experimental-models-test.js | 30 +++++++++++++++++++ 3 files changed, 48 insertions(+), 8 deletions(-) diff --git a/addon/-private/less-experimental/models/runtime.js b/addon/-private/less-experimental/models/runtime.js index 0548d10c..1257acae 100644 --- a/addon/-private/less-experimental/models/runtime.js +++ b/addon/-private/less-experimental/models/runtime.js @@ -32,11 +32,12 @@ export default class ModelsRuntime { this.sourceManager = new SourceManager({ parent, source: opts.source, + observe: opts.object, delegate: { replaced: array => this.onSourceArrayReplaced(array), added: (objects, start, len) => this.onSourceObjectsAdded(objects, start, len), removed: (objects, start, len) => this.onSourceObjectsRemoved(objects, start, len), - updated: (object, key) => this.onSourceObjectUpdated(object, key) + updated: (object, key, idx) => this.onSourceObjectUpdated(object, key, idx) } }); @@ -66,6 +67,11 @@ export default class ModelsRuntime { } } + replaceModel(idx, object) { + let { model } = this.modelFactory.create(object); + this.replaceModels(idx, 1, [ model ]); + } + rebuildModels() { let objects = this.sourceManager.source; let models; @@ -91,8 +97,8 @@ export default class ModelsRuntime { this.replaceModels(start, len); } - onSourceObjectUpdated(object, key) { - console.log('source object updated', object, key); + onSourceObjectUpdated(object, key, idx) { + this.replaceModel(idx, object); } onParentPropertyUpdated(object, key) { diff --git a/addon/-private/less-experimental/models/runtime/source.js b/addon/-private/less-experimental/models/runtime/source.js index e813b8c0..6bfb6f04 100644 --- a/addon/-private/less-experimental/models/runtime/source.js +++ b/addon/-private/less-experimental/models/runtime/source.js @@ -3,10 +3,11 @@ import ArrayObserver from '../../util/array-observer'; import { assert } from '@ember/debug'; import { typeOf } from '@ember/utils'; -const validate = (parent, source, delegate) => { +const validate = (parent, source, observe, delegate) => { assert(`parent is required`, !!parent); assert(`source must be object`, typeOf(source) === 'object'); assert(`source.dependencies must be array`, typeOf(source.dependencies) === 'array'); + assert(`observe must be array`, typeOf(observe) === 'array'); assert(`source.key is required`, !!source.key); assert(`delegate is required`, !!delegate); assert(`delegate.replaced must be function`, typeOf(delegate.replaced) === 'function'); @@ -19,11 +20,13 @@ export default class SourceManager { // parent // source: { dependencies, key } + // observe: [ key, ... ] object deps array // delegate: { replaced, added, removed, updated } - constructor({ parent, source, delegate }) { - validate(parent, source, delegate); + constructor({ parent, source, observe, delegate }) { + validate(parent, source, observe, delegate); this.parent = parent; this.delegate = delegate; + this.observe = observe; this.provider = new ValueProvider({ object: parent, observe: source.dependencies, @@ -51,11 +54,11 @@ export default class SourceManager { if(source) { this.observer = new ArrayObserver({ array: source, - observe: [], + observe: this.observe, delegate: { added: (objects, start, len) => this.delegate.added(objects, start, len), removed: (objects, start, len) => this.delegate.removed(objects, start, len), - updated: (object, key) => this.delegate.updated(object, key) + updated: (object, key) => this.delegate.updated(object, key, source.indexOf(object)) } }); } @@ -67,6 +70,7 @@ export default class SourceManager { destroy() { this.provider.destroy(); + this.observer && this.observer.destroy(); } } diff --git a/tests/unit/less-experimental-models-test.js b/tests/unit/less-experimental-models-test.js index 9f7a4d95..336bdfad 100644 --- a/tests/unit/less-experimental-models-test.js +++ b/tests/unit/less-experimental-models-test.js @@ -101,4 +101,34 @@ module('less-experimental-models', function(hooks) { assert.ok(otterModel.isDestroying); }); + test('object dependencies recreate models', async function(assert) { + let duck = this.object({ name: 'duck' }); + let hamster = this.object({ name: 'hamster' }); + let otter = this.object({ name: 'otter' }); + + let subject = this.subject({ + all: A([ duck, hamster, otter ]), + models: models('all').object('name').inline({ + prepare(object) { + this.setProperties({ name: object.get('name') }); + } + }) + }); + + let start = subject.get('models').slice(); + assert.deepEqual(start.mapBy('name'), [ 'duck', 'hamster', 'otter' ]); + + let first = subject.get('models').objectAt(1); + assert.equal(first.get('name'), 'hamster'); + + run(() => hamster.set('name', 'The Hamster')); + + assert.ok(first.isDestroying); + assert.deepEqual(start.map(i => i.isDestroying), [ false, true, false ]); + + assert.deepEqual(subject.get('models').mapBy('name'), [ 'duck', 'The Hamster', 'otter' ]); + + run(() => subject.destroy()); + }); + }); From fd7eb4436ceab38987bb5c6e89158f863d25e995 Mon Sep 17 00:00:00 2001 From: ampatspell Date: Thu, 9 Aug 2018 22:58:45 +0300 Subject: [PATCH 19/63] failing owner deps test --- tests/unit/less-experimental-models-test.js | 27 +++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/tests/unit/less-experimental-models-test.js b/tests/unit/less-experimental-models-test.js index 336bdfad..2676294a 100644 --- a/tests/unit/less-experimental-models-test.js +++ b/tests/unit/less-experimental-models-test.js @@ -131,4 +131,31 @@ module('less-experimental-models', function(hooks) { run(() => subject.destroy()); }); + test('owner dependencies recreate models', async function(assert) { + let subject = this.subject({ + all: A([ this.object({ name: 'duck' }) ]), + type: 'nice', + models: models('all').owner('type').inline({ + prepare(object, owner) { + this.setProperties({ + name: object.get('name'), + type: owner.get('type') + }); + } + }) + }); + + let first = subject.get('models').objectAt(0); + assert.equal(first.get('type'), 'nice'); + + run(() => subject.set('type', 'awesome')); + + assert.ok(first.isDestroying); + + let second = subject.get('models').objectAt(0); + assert.equal(second.get('type'), 'awesome'); + + run(() => subject.destroy()); + }); + }); From efaa3f24d11b04f056470d702650541a23782047 Mon Sep 17 00:00:00 2001 From: ampatspell Date: Thu, 9 Aug 2018 22:59:50 +0300 Subject: [PATCH 20/63] rebuild models on owner (parent) prop change --- addon/-private/less-experimental/models/runtime.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/addon/-private/less-experimental/models/runtime.js b/addon/-private/less-experimental/models/runtime.js index 1257acae..c209c9dc 100644 --- a/addon/-private/less-experimental/models/runtime.js +++ b/addon/-private/less-experimental/models/runtime.js @@ -102,7 +102,7 @@ export default class ModelsRuntime { } onParentPropertyUpdated(object, key) { - console.log('parent updated', object, key); + this.rebuildModels(); } destroy() { From 42f24c4c81791644e2b1debefe867ef31a8e2fb2 Mon Sep 17 00:00:00 2001 From: ampatspell Date: Thu, 9 Aug 2018 23:05:17 +0300 Subject: [PATCH 21/63] failing tests --- .../less-experimental/models/internal.js | 2 +- .../less-experimental/models/models.js | 3 +- .../less-experimental/models/runtime.js | 2 -- tests/unit/less-experimental-models-test.js | 36 +++++++++++++++++++ 4 files changed, 38 insertions(+), 5 deletions(-) diff --git a/addon/-private/less-experimental/models/internal.js b/addon/-private/less-experimental/models/internal.js index c255924d..03cf6fb5 100644 --- a/addon/-private/less-experimental/models/internal.js +++ b/addon/-private/less-experimental/models/internal.js @@ -6,7 +6,7 @@ export default Internal.extend({ parent: null, key: null, - opts: null, // { source, owner, object, inline, named, mapping } + opts: null, runtime(create) { let runtime = this._runtime; diff --git a/addon/-private/less-experimental/models/models.js b/addon/-private/less-experimental/models/models.js index 99200961..b38239b0 100644 --- a/addon/-private/less-experimental/models/models.js +++ b/addon/-private/less-experimental/models/models.js @@ -1,5 +1,4 @@ import ArrayProxy from '@ember/array/proxy'; import ModelMixin from '../../internal/model-mixin'; -export default ArrayProxy.extend(ModelMixin, { -}); +export default ArrayProxy.extend(ModelMixin); diff --git a/addon/-private/less-experimental/models/runtime.js b/addon/-private/less-experimental/models/runtime.js index c209c9dc..dec96c62 100644 --- a/addon/-private/less-experimental/models/runtime.js +++ b/addon/-private/less-experimental/models/runtime.js @@ -10,7 +10,6 @@ export default class ModelsRuntime { this.key = key; this.opts = opts; this.content = A([]); - console.log('init', this); this.modelFactory = new ModelFactory({ parent, @@ -106,7 +105,6 @@ export default class ModelsRuntime { } destroy() { - console.log('destroy', this); this.parentManager.destroy(); this.sourceManager.destroy(); this.content.map(model => model.destroy()); diff --git a/tests/unit/less-experimental-models-test.js b/tests/unit/less-experimental-models-test.js index 2676294a..b0216b61 100644 --- a/tests/unit/less-experimental-models-test.js +++ b/tests/unit/less-experimental-models-test.js @@ -158,4 +158,40 @@ module('less-experimental-models', function(hooks) { run(() => subject.destroy()); }); + test('source starts as null', async function(assert) { + let subject = this.subject({ + all: null, + models: models('all').inline({ + prepare(object) { + this.setProperties({ name: object.name }); + } + }) + }); + + assert.deepEqual(subject.get('models').mapBy('name'), []); + + subject.set('all', A([ this.object({ name: 'duck' }) ])); + + assert.deepEqual(subject.get('models').mapBy('name'), [ 'duck' ]); + + run(() => subject.destroy()); + }); + + test('source becomes null', async function(assert) { + let subject = this.subject({ + all: A([ this.object({ name: 'duck' }) ]), + models: models('all').inline({ + prepare(object) { + this.setProperties({ name: object.name }); + } + }) + }); + + assert.deepEqual(subject.get('models').mapBy('name'), [ 'duck' ]); + subject.set('all', null); + assert.deepEqual(subject.get('models').mapBy('name'), []); + + run(() => subject.destroy()); + }); + }); From 821f6766b6dd73dc6c85727c89ce156578cb7474 Mon Sep 17 00:00:00 2001 From: ampatspell Date: Thu, 9 Aug 2018 23:09:16 +0300 Subject: [PATCH 22/63] fix value provider -- always observe key --- .../less-experimental/util/value-provider.js | 18 ++++++++---------- tests/unit/less-experimental-models-test.js | 4 +++- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/addon/-private/less-experimental/util/value-provider.js b/addon/-private/less-experimental/util/value-provider.js index 9850a98a..0df7e1bf 100644 --- a/addon/-private/less-experimental/util/value-provider.js +++ b/addon/-private/less-experimental/util/value-provider.js @@ -23,15 +23,13 @@ export default class ValueProvider { this.key = key; this.delegate = delegate; this.resolve = typeof key === 'function'; - if(this.resolve) { - this.observer = new ObjectObserver({ - object, - observe, - delegate: { - updated: () => this.update(true) - } - }); - } + this.observer = new ObjectObserver({ + object, + observe, + delegate: { + updated: () => this.update(true) + } + }); this.update(false); } @@ -62,7 +60,7 @@ export default class ValueProvider { } destroy() { - this.observer && this.observer.destroy(); + this.observer.destroy(); } } diff --git a/tests/unit/less-experimental-models-test.js b/tests/unit/less-experimental-models-test.js index b0216b61..ecebdbcd 100644 --- a/tests/unit/less-experimental-models-test.js +++ b/tests/unit/less-experimental-models-test.js @@ -188,7 +188,9 @@ module('less-experimental-models', function(hooks) { }); assert.deepEqual(subject.get('models').mapBy('name'), [ 'duck' ]); - subject.set('all', null); + + run(() => subject.set('all', null)); + assert.deepEqual(subject.get('models').mapBy('name'), []); run(() => subject.destroy()); From 38d8c1e975ce5bd74a67b362fec95e4a608ce7b9 Mon Sep 17 00:00:00 2001 From: ampatspell Date: Thu, 9 Aug 2018 23:10:04 +0300 Subject: [PATCH 23/63] add isDestroying test --- tests/unit/less-experimental-models-test.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/tests/unit/less-experimental-models-test.js b/tests/unit/less-experimental-models-test.js index ecebdbcd..4820af7c 100644 --- a/tests/unit/less-experimental-models-test.js +++ b/tests/unit/less-experimental-models-test.js @@ -189,10 +189,14 @@ module('less-experimental-models', function(hooks) { assert.deepEqual(subject.get('models').mapBy('name'), [ 'duck' ]); + let first = subject.get('models.firstObject'); + run(() => subject.set('all', null)); assert.deepEqual(subject.get('models').mapBy('name'), []); + assert.ok(first.isDestroying); + run(() => subject.destroy()); }); From 5b0cef47336865c18e9befe9922f9d95eb28c561 Mon Sep 17 00:00:00 2001 From: ampatspell Date: Thu, 9 Aug 2018 23:13:02 +0300 Subject: [PATCH 24/63] failing mapping test --- tests/unit/less-experimental-models-test.js | 26 +++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/tests/unit/less-experimental-models-test.js b/tests/unit/less-experimental-models-test.js index 4820af7c..b13b787c 100644 --- a/tests/unit/less-experimental-models-test.js +++ b/tests/unit/less-experimental-models-test.js @@ -200,4 +200,30 @@ module('less-experimental-models', function(hooks) { run(() => subject.destroy()); }); + test('mapping is used', async function(assert) { + let subject = this.subject({ + + all: A([ this.object({ name: 'duck' }) ]), + color: 'yellow', + + models: models('all').inline({ + prepare({ name, color }) { + this.setProperties({ name, color }); + } + }).mapping((object, owner) => { + let name = object.get('name'); + let color = owner.get('color'); + return { name, color }; + }) + + }); + + let first = subject.get('models.firstObject'); + + assert.equal(first.get('name'), 'duck'); + assert.equal(first.get('color'), 'yellow'); + + run(() => subject.destroy()); + }); + }); From 17aad7078f8834743f739978a6852ddb63ca4165 Mon Sep 17 00:00:00 2001 From: ampatspell Date: Thu, 9 Aug 2018 23:22:35 +0300 Subject: [PATCH 25/63] support mapping --- .../less-experimental/util/model-factory.js | 52 ++++++++++++++----- 1 file changed, 38 insertions(+), 14 deletions(-) diff --git a/addon/-private/less-experimental/util/model-factory.js b/addon/-private/less-experimental/util/model-factory.js index 333d079d..b71e2f93 100644 --- a/addon/-private/less-experimental/util/model-factory.js +++ b/addon/-private/less-experimental/util/model-factory.js @@ -12,7 +12,7 @@ const validate = (parent, key, inline, named, mapping, prepare) => { throw new Error('named not implemented'); } if(mapping) { - throw new Error('mapping not implemented'); + assert(`mapping must be function`, typeOf(mapping) === 'function'); } assert(`prepare must be function`, typeOf(prepare) === 'function'); } @@ -23,14 +23,17 @@ export default class ModelFactory { validate(parent, key, inline, named, mapping, prepare); this.parent = parent; this.key = key; - this.inline = inline; - this.named = named; - this.mapping = mapping; - this.prepare = prepare; + this.opts = { + inline, + named, + mapping, + prepare + }; + this.process = {} } - createProcess() { - let { parent, key, inline } = this; + createFactory() { + let { parent, key, opts: { inline } } = this; if(inline) { let modelClass = generateModelClass(parent, key, inline); return props => modelClass.create(props); @@ -39,24 +42,45 @@ export default class ModelFactory { } get factory() { - let process = this._process; + let process = this.process.factory; if(!process) { - process = this.createProcess(); - this._process = process; + process = this.createFactory(); + this.process.factory = process; } return process; } - prepareModel(model, ...args) { - let prepare = this.prepare(...args); + createMapping() { + let { mapping } = this.opts; + if(mapping) { + return (...args) => { + let ret = mapping(...args); + return [ ret ]; + } + } + return (...args) => args; + } + + get mapping() { + let process = this.process.mapping; + if(!process) { + process = this.createMapping(); + this.process.mapping = process; + } + return process; + } + + prepare(model, ...args) { + let prepare = this.opts.prepare(...args); + let mapped = this.mapping(...prepare); assert(`'prepare' function is required for ${model}`, typeOf(model.prepare) === 'function'); - return model.prepare(...prepare); + return model.prepare(...mapped); } create(...args) { let factory = this.factory; let model = factory(); - let promise = this.prepareModel(model, ...args); + let promise = this.prepare(model, ...args); return { model, promise }; } From b4c46b8ee41797b79e4d591e20d14f183df7a21a Mon Sep 17 00:00:00 2001 From: ampatspell Date: Thu, 9 Aug 2018 23:26:47 +0300 Subject: [PATCH 26/63] one more test --- tests/unit/less-experimental-models-test.js | 24 +++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/tests/unit/less-experimental-models-test.js b/tests/unit/less-experimental-models-test.js index b13b787c..a98b71c7 100644 --- a/tests/unit/less-experimental-models-test.js +++ b/tests/unit/less-experimental-models-test.js @@ -226,4 +226,28 @@ module('less-experimental-models', function(hooks) { run(() => subject.destroy()); }); + test('noop changes are not propogated', async function(assert) { + let object = [ this.object({ name: 'duck' }) ]; + let subject = this.subject({ + + all: A(object), + color: 'yellow', + + models: models('all').owner('color').object('name').inline({ + prepare() { + } + }) + + }); + + let first = subject.get('models.firstObject'); + + subject.set('color', 'yellow'); + object.set('name', 'duck'); + + assert.ok(!first.isDestroying); + + run(() => subject.destroy()); + }); + }); From a713586e2ea418b4ebb16f67e32bf677ab05e4b8 Mon Sep 17 00:00:00 2001 From: ampatspell Date: Thu, 9 Aug 2018 23:53:42 +0300 Subject: [PATCH 27/63] support named models (fn and string) --- .../less-experimental/models/index.js | 2 +- .../less-experimental/models/runtime.js | 5 +- .../less-experimental/util/model-factory.js | 36 ++++++--- tests/unit/less-experimental-models-test.js | 74 +++++++++++++++++++ 4 files changed, 104 insertions(+), 13 deletions(-) diff --git a/addon/-private/less-experimental/models/index.js b/addon/-private/less-experimental/models/index.js index af72ad8b..1619b73e 100644 --- a/addon/-private/less-experimental/models/index.js +++ b/addon/-private/less-experimental/models/index.js @@ -26,7 +26,7 @@ const build = (opts, nested={}) => { prop.owner = (...args) => build(opts, { parent: compact(args) }); prop.object = (...args) => build(opts, { object: compact(args) }); prop.inline = arg => build(opts, { inline: arg }); - prop.named = (...args) => build(opts, { named: key(args) }); + prop.named = arg => build(opts, { named: arg }); prop.mapping = arg => build(opts, { mapping: arg }); prop.source = (...args) => build(opts, { source: key(args) }); return prop; diff --git a/addon/-private/less-experimental/models/runtime.js b/addon/-private/less-experimental/models/runtime.js index dec96c62..cf53d78e 100644 --- a/addon/-private/less-experimental/models/runtime.js +++ b/addon/-private/less-experimental/models/runtime.js @@ -17,7 +17,10 @@ export default class ModelsRuntime { inline: opts.inline, named: opts.named, mapping: opts.mapping, - prepare: object => [ object, parent ], + delegate: { + prepare: object => [ object, parent ], + named: object => [ object, parent ] + } }); this.parentManager = new ParentManager({ diff --git a/addon/-private/less-experimental/util/model-factory.js b/addon/-private/less-experimental/util/model-factory.js index b71e2f93..100bedf5 100644 --- a/addon/-private/less-experimental/util/model-factory.js +++ b/addon/-private/less-experimental/util/model-factory.js @@ -1,44 +1,58 @@ import { assert } from '@ember/debug'; import { typeOf } from '@ember/utils'; -import { generateModelClass } from '../../util/model'; +import { generateModelClass, modelFactoryForShortName } from '../../util/model'; -const validate = (parent, key, inline, named, mapping, prepare) => { +const validate = (parent, key, inline, named, mapping, delegate) => { assert(`parent is required`, !!parent); assert(`key is required`, typeOf(key) === 'string'); if(inline) { assert(`inline must be object`, typeOf(inline) === 'object'); } if(named) { - throw new Error('named not implemented'); + assert(`named must be string or function`, typeOf(named) === 'string' || typeOf(named) === 'function'); } if(mapping) { assert(`mapping must be function`, typeOf(mapping) === 'function'); } - assert(`prepare must be function`, typeOf(prepare) === 'function'); + assert(`inline or named is requied, not both`, !inline || !named); + assert(`inline or named is required`, inline || named); + assert(`delegate must be object`, typeOf(delegate) === 'object'); + assert(`delegate.prepare must be function`, typeOf(delegate.prepare) === 'function'); + assert(`delegate.named must be function`, typeOf(delegate.named) === 'function'); } export default class ModelFactory { - constructor({ parent, key, inline, named, mapping, prepare }) { - validate(parent, key, inline, named, mapping, prepare); + constructor({ parent, key, inline, named, mapping, delegate }) { + validate(parent, key, inline, named, mapping, delegate); this.parent = parent; this.key = key; this.opts = { inline, named, mapping, - prepare }; + this.delegate = delegate; this.process = {} } createFactory() { - let { parent, key, opts: { inline } } = this; + let { parent, key, opts: { inline, named } } = this; if(inline) { let modelClass = generateModelClass(parent, key, inline); return props => modelClass.create(props); + } else if(named) { + if(typeof named === 'string') { + let modelClass = modelFactoryForShortName(parent, named); + return props => modelClass.create(props); + } else { + return (props, args) => { + let name = named(...this.delegate.named(...args)); + let modelClass = modelFactoryForShortName(parent, name); + return modelClass.create(props); + } + } } - throw new Error('not implemented'); } get factory() { @@ -71,7 +85,7 @@ export default class ModelFactory { } prepare(model, ...args) { - let prepare = this.opts.prepare(...args); + let prepare = this.delegate.prepare(...args); let mapped = this.mapping(...prepare); assert(`'prepare' function is required for ${model}`, typeOf(model.prepare) === 'function'); return model.prepare(...mapped); @@ -79,7 +93,7 @@ export default class ModelFactory { create(...args) { let factory = this.factory; - let model = factory(); + let model = factory({}, args); let promise = this.prepare(model, ...args); return { model, promise }; } diff --git a/tests/unit/less-experimental-models-test.js b/tests/unit/less-experimental-models-test.js index a98b71c7..0feb84d3 100644 --- a/tests/unit/less-experimental-models-test.js +++ b/tests/unit/less-experimental-models-test.js @@ -250,4 +250,78 @@ module('less-experimental-models', function(hooks) { run(() => subject.destroy()); }); + test('statically named model', async function(assert) { + this.registerModel('duck', EmberObject.extend({ + modelName: 'duck', + prepare(object) { + let name = object.get('name'); + this.setProperties({ name }); + } + })); + + let subject = this.subject({ + all: A([ this.object({ name: 'hamster' }) ]), + models: models('all').named('duck') + }); + + let first = subject.get('models.firstObject'); + + assert.equal(first.get('modelName'), 'duck'); + assert.equal(first.get('name'), 'hamster'); + + run(() => subject.destroy()); + }); + + test('dynamically named model', async function(assert) { + let Model = EmberObject.extend({ + prepare(object) { + let name = object.get('name'); + this.setProperties({ name }); + } + }); + + this.registerModel('awesome/hamster', Model.extend({ + modelName: 'awesome/hamster' + })); + + this.registerModel('cute/hamster', Model.extend({ + modelName: 'cute/hamster' + })); + + this.registerModel('cute/duck', Model.extend({ + modelName: 'cute/duck' + })); + + let object = this.object({ name: 'hamster' }); + + let subject = this.subject({ + type: 'awesome', + all: A([ object ]), + models: models('all').owner('type').object('name').named((object, owner) => { + let name = object.get('name'); + let type = owner.get('type'); + return `${type}/${name}`; + }) + }); + + let first = subject.get('models.firstObject'); + assert.equal(first.get('modelName'), 'awesome/hamster'); + + run(() => subject.set('type', 'cute')); + + assert.ok(first.isDestroying); + + let second = subject.get('models.firstObject'); + assert.equal(second.get('modelName'), 'cute/hamster'); + + run(() => object.set('name', 'duck')); + + assert.ok(second.isDestroying); + + let third = subject.get('models.firstObject'); + assert.equal(third.get('modelName'), 'cute/duck'); + + run(() => subject.destroy()); + }); + }); From e5c1999063eb2ea905ebb0a47efb99d672b2b5c9 Mon Sep 17 00:00:00 2001 From: ampatspell Date: Fri, 10 Aug 2018 00:24:21 +0300 Subject: [PATCH 28/63] cleanup --- addon/-private/less-experimental/models/runtime.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/addon/-private/less-experimental/models/runtime.js b/addon/-private/less-experimental/models/runtime.js index cf53d78e..4fb57787 100644 --- a/addon/-private/less-experimental/models/runtime.js +++ b/addon/-private/less-experimental/models/runtime.js @@ -86,7 +86,7 @@ export default class ModelsRuntime { // - onSourceArrayReplaced(soure) { + onSourceArrayReplaced() { this.rebuildModels(); } From 9487a9c31f3952c4761b26c0b564a8e4255c99ab Mon Sep 17 00:00:00 2001 From: ampatspell Date: Fri, 10 Aug 2018 00:33:12 +0300 Subject: [PATCH 29/63] back to using rsyncy --- tests/dummy/config/environment.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/dummy/config/environment.js b/tests/dummy/config/environment.js index ddad71bb..28e0284c 100644 --- a/tests/dummy/config/environment.js +++ b/tests/dummy/config/environment.js @@ -15,7 +15,7 @@ module.exports = function(environment) { }, version: require('../../../package.json').version, fastboot: { - hostWhitelist: [ 'ember-cli-zuglet.firebaseapp.com', /^localhost:\d+$/ ] + hostWhitelist: [ 'ember-cli-zuglet.firebaseapp.com', /^dev\.amateurinmotion\.com:\d+$/, /^localhost:\d+$/ ] } }; From 49c2c892911aaa2f49e7c938642d21ebce0fc82f Mon Sep 17 00:00:00 2001 From: ampatspell Date: Fri, 10 Aug 2018 00:44:59 +0300 Subject: [PATCH 30/63] allow array proxies as a array-observer source --- addon/-private/less-experimental/util/array-observer.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/addon/-private/less-experimental/util/array-observer.js b/addon/-private/less-experimental/util/array-observer.js index e6ea6804..78720a68 100644 --- a/addon/-private/less-experimental/util/array-observer.js +++ b/addon/-private/less-experimental/util/array-observer.js @@ -3,9 +3,10 @@ import { startObservingObjects, stopObservingObjects } from './object-observer'; import { assert } from '@ember/debug'; import { typeOf } from '@ember/utils'; import { get } from '@ember/object'; +import ArrayProxy from '@ember/array/proxy'; const validate = (array, observe, delegate) => { - assert(`array must be array`, typeOf(array) === 'array'); + assert(`array must be array or array proxy`, typeOf(array) === 'array' || ArrayProxy.detectInstance(array)); assert(`observe must be array`, typeOf(observe) === 'array'); assert(`delegate is required`, !!delegate); assert(`delegate.added must be function`, typeOf(delegate.added) === 'function'); From ac03f31939566ee1d8c59097b08ccb06f455c135 Mon Sep 17 00:00:00 2001 From: ampatspell Date: Fri, 10 Aug 2018 00:45:06 +0300 Subject: [PATCH 31/63] cleanup --- addon/-private/less-experimental/models/index.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/addon/-private/less-experimental/models/index.js b/addon/-private/less-experimental/models/index.js index 1619b73e..99956bb8 100644 --- a/addon/-private/less-experimental/models/index.js +++ b/addon/-private/less-experimental/models/index.js @@ -6,7 +6,7 @@ const compact = array => A(array).compact(); // foo('docs') // foo('type', owner => owner.type) -const key = args => { +const source = args => { args = compact(args); if(args.length === 1) { return { dependencies: args, key: args[0] }; @@ -28,7 +28,7 @@ const build = (opts, nested={}) => { prop.inline = arg => build(opts, { inline: arg }); prop.named = arg => build(opts, { named: arg }); prop.mapping = arg => build(opts, { mapping: arg }); - prop.source = (...args) => build(opts, { source: key(args) }); + prop.source = (...args) => build(opts, { source: source(args) }); return prop; } From 5aaf8e5938acb28a4d9e3c667273c73cbf0f1f2f Mon Sep 17 00:00:00 2001 From: ampatspell Date: Fri, 10 Aug 2018 00:45:20 +0300 Subject: [PATCH 32/63] dummy models now uses less-experimental models --- .../components/ui-route/experiments/models/template.hbs | 2 +- tests/dummy/app/routes/experiments/models.js | 8 +++++--- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/tests/dummy/app/components/ui-route/experiments/models/template.hbs b/tests/dummy/app/components/ui-route/experiments/models/template.hbs index 06575cb5..f4bb17a8 100644 --- a/tests/dummy/app/components/ui-route/experiments/models/template.hbs +++ b/tests/dummy/app/components/ui-route/experiments/models/template.hbs @@ -28,7 +28,7 @@ {{model.hamsters}} {{/rows.row}} - {{#each model.hamsters.content as |model|}} + {{#each model.hamsters as |model|}} {{#rows.row}}
{{model.type}} : {{model.name}} : {{model.id}}
{{model}}
diff --git a/tests/dummy/app/routes/experiments/models.js b/tests/dummy/app/routes/experiments/models.js index d896bb7a..be599a14 100644 --- a/tests/dummy/app/routes/experiments/models.js +++ b/tests/dummy/app/routes/experiments/models.js @@ -1,6 +1,6 @@ import Route from '@ember/routing/route'; import model from 'ember-cli-zuglet/experimental/model/route'; -import models from 'ember-cli-zuglet/experimental/models'; +import models from 'ember-cli-zuglet/less-experimental/models'; import observed from 'ember-cli-zuglet/experimental/observed'; import { readOnly } from '@ember/object/computed'; @@ -10,7 +10,7 @@ export default Route.extend({ query: observed(), - hamsters: models('query.content', { + hamsters: models('query.content').inline({ doc: null, @@ -20,7 +20,7 @@ export default Route.extend({ prepare(doc) { this.setProperties({ doc }); - } + }, }), @@ -53,6 +53,8 @@ export default Route.extend({ let query = this.store.collection('hamsters').query(); this.setProperties({ query }); + window.route = this; + return query.observers.promise.then(() => this.insert()); } From 9629713a14ba31363df17fd52021481fc6b90952 Mon Sep 17 00:00:00 2001 From: ampatspell Date: Fri, 10 Aug 2018 00:54:20 +0300 Subject: [PATCH 33/63] assert duplicate dependencies between source deps and owner --- .../-private/less-experimental/models/runtime.js | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/addon/-private/less-experimental/models/runtime.js b/addon/-private/less-experimental/models/runtime.js index 4fb57787..61849b57 100644 --- a/addon/-private/less-experimental/models/runtime.js +++ b/addon/-private/less-experimental/models/runtime.js @@ -2,10 +2,26 @@ import { A } from '@ember/array'; import ParentManager from './runtime/parent'; import SourceManager from './runtime/source'; import ModelFactory from '../util/model-factory'; +import { assert } from '@ember/debug'; +import { typeOf } from '@ember/utils'; + +const validate = opts => { + assert(`opts must be object`, typeOf(opts) === 'object'); + let { source, parent } = opts; + assert(`opts.source must be object`, typeOf(source) === 'object'); + assert(`opts.parent must be array`, typeOf(parent) === 'array'); + let { dependencies, key } = source; + assert(`opts.source.dependencies must be array`, typeOf(dependencies) === 'array'); + assert(`duplicate owner dependency '${key}'`, !parent.includes(key)); + dependencies.forEach(key => { + assert(`duplicate owner dependency '${key}`, !parent.includes(key)) + }); +} export default class ModelsRuntime { constructor(parent, key, opts) { + validate(opts); this.parent = parent; this.key = key; this.opts = opts; From e01f7c7950495f362dba93c74ed8bcfbf2ea4a8e Mon Sep 17 00:00:00 2001 From: ampatspell Date: Fri, 10 Aug 2018 00:56:13 +0300 Subject: [PATCH 34/63] eslint --- addon/-private/less-experimental/models/property.js | 1 - addon/-private/less-experimental/models/runtime.js | 2 +- addon/-private/less-experimental/models/runtime/source.js | 2 +- 3 files changed, 2 insertions(+), 3 deletions(-) diff --git a/addon/-private/less-experimental/models/property.js b/addon/-private/less-experimental/models/property.js index 579e2ef9..c68030cf 100644 --- a/addon/-private/less-experimental/models/property.js +++ b/addon/-private/less-experimental/models/property.js @@ -1,5 +1,4 @@ import destroyable from '../../util/destroyable'; -import { A } from '@ember/array'; import { getOwner } from '@ember/application'; const get = internal => internal.model(true); diff --git a/addon/-private/less-experimental/models/runtime.js b/addon/-private/less-experimental/models/runtime.js index 61849b57..8cacd8f5 100644 --- a/addon/-private/less-experimental/models/runtime.js +++ b/addon/-private/less-experimental/models/runtime.js @@ -119,7 +119,7 @@ export default class ModelsRuntime { this.replaceModel(idx, object); } - onParentPropertyUpdated(object, key) { + onParentPropertyUpdated() { this.rebuildModels(); } diff --git a/addon/-private/less-experimental/models/runtime/source.js b/addon/-private/less-experimental/models/runtime/source.js index 6bfb6f04..07fd9b9e 100644 --- a/addon/-private/less-experimental/models/runtime/source.js +++ b/addon/-private/less-experimental/models/runtime/source.js @@ -32,7 +32,7 @@ export default class SourceManager { observe: source.dependencies, key: source.key, delegate: { - updated: value => this.update(true) + updated: () => this.update(true) } }); this.update(false); From c368996bfcd6b21d3ce8387b93ba52000bbd05d9 Mon Sep 17 00:00:00 2001 From: ampatspell Date: Fri, 10 Aug 2018 01:02:16 +0300 Subject: [PATCH 35/63] fix test --- tests/unit/less-experimental-models-test.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/unit/less-experimental-models-test.js b/tests/unit/less-experimental-models-test.js index 0feb84d3..f02ba15f 100644 --- a/tests/unit/less-experimental-models-test.js +++ b/tests/unit/less-experimental-models-test.js @@ -115,7 +115,7 @@ module('less-experimental-models', function(hooks) { }) }); - let start = subject.get('models').slice(); + let start = A(subject.get('models')).slice(); assert.deepEqual(start.mapBy('name'), [ 'duck', 'hamster', 'otter' ]); let first = subject.get('models').objectAt(1); From 6959ffdb3d2a8e95f8ca9b7e4864dbd8bfa76a82 Mon Sep 17 00:00:00 2001 From: ampatspell Date: Fri, 10 Aug 2018 01:06:15 +0300 Subject: [PATCH 36/63] really fix test --- tests/unit/less-experimental-models-test.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/unit/less-experimental-models-test.js b/tests/unit/less-experimental-models-test.js index f02ba15f..3572b2c7 100644 --- a/tests/unit/less-experimental-models-test.js +++ b/tests/unit/less-experimental-models-test.js @@ -115,7 +115,7 @@ module('less-experimental-models', function(hooks) { }) }); - let start = A(subject.get('models')).slice(); + let start = A(subject.get('models').slice()); assert.deepEqual(start.mapBy('name'), [ 'duck', 'hamster', 'otter' ]); let first = subject.get('models').objectAt(1); From 918b69a94df3e265ac7e9930f659e441b484fdf3 Mon Sep 17 00:00:00 2001 From: ampatspell Date: Fri, 10 Aug 2018 01:14:36 +0300 Subject: [PATCH 37/63] normalize model names to dasherized form --- addon/-private/util/model.js | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/addon/-private/util/model.js b/addon/-private/util/model.js index 96e2bff4..08dab383 100644 --- a/addon/-private/util/model.js +++ b/addon/-private/util/model.js @@ -1,6 +1,7 @@ import { getOwner } from '@ember/application'; import EmberObject from '@ember/object'; import { assert } from '@ember/debug'; +import { dasherize } from '@ember/string'; const containerKey = instance => { // https://github.com/emberjs/ember.js/issues/10742 @@ -18,12 +19,14 @@ const generateModelName = (owner, key) => { const generateModelClassForProperties = props => EmberObject.extend(props); +export const normalizeModelName = name => dasherize(name); export const modelFullName = name => `model:${name}`; export const modelFactoryForShortName = (parent, name) => { - let fullName = modelFullName(name); + let normalizedName = normalizeModelName(name); + let fullName = modelFullName(normalizedName); let factory = getOwner(parent).factoryFor(fullName); - assert(`model '${name}' is not registered`, !!factory); + assert(`model '${normalizedName}' is not registered`, !!factory); return factory; } From a423b2e81a1d78c20b6edab5fb6e92ee0d8ac916 Mon Sep 17 00:00:00 2001 From: ampatspell Date: Fri, 10 Aug 2018 01:16:39 +0300 Subject: [PATCH 38/63] make sure _debugContainerKey is not missing for instances --- addon/-private/util/model.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/addon/-private/util/model.js b/addon/-private/util/model.js index 08dab383..b38f4379 100644 --- a/addon/-private/util/model.js +++ b/addon/-private/util/model.js @@ -5,7 +5,9 @@ import { dasherize } from '@ember/string'; const containerKey = instance => { // https://github.com/emberjs/ember.js/issues/10742 - return instance._debugContainerKey; + let key = instance._debugContainerKey; + assert(`_debugContainerKey for ${instance} is not set`, !!key); + return key; } const generateModelName = (owner, key) => { From 6d4323df9829794ffcff8c3a02a731960a0f48cb Mon Sep 17 00:00:00 2001 From: ampatspell Date: Fri, 10 Aug 2018 01:25:53 +0300 Subject: [PATCH 39/63] move code from util/model to less-experimental module. keep the same in experimental --- .../{util/model.js => experimental/-model.js} | 0 addon/-private/experimental/model/factory.js | 2 +- .../-private/experimental/models/internal.js | 2 +- .../less-experimental/util/model-factory.js | 50 +++++++++++++++++-- 4 files changed, 48 insertions(+), 6 deletions(-) rename addon/-private/{util/model.js => experimental/-model.js} (100%) diff --git a/addon/-private/util/model.js b/addon/-private/experimental/-model.js similarity index 100% rename from addon/-private/util/model.js rename to addon/-private/experimental/-model.js diff --git a/addon/-private/experimental/model/factory.js b/addon/-private/experimental/model/factory.js index 5b8f57a5..3997ad70 100644 --- a/addon/-private/experimental/model/factory.js +++ b/addon/-private/experimental/model/factory.js @@ -1,6 +1,6 @@ import { typeOf } from '@ember/utils'; import { assert } from '@ember/debug'; -import { generateModelClass, modelFactoryForShortName } from '../../util/model'; +import { generateModelClass, modelFactoryForShortName } from '../-model'; const resolveObject = (parent, key, arg) => { let factory = generateModelClass(parent, key, arg); diff --git a/addon/-private/experimental/models/internal.js b/addon/-private/experimental/models/internal.js index 37d123fa..428b8c3b 100644 --- a/addon/-private/experimental/models/internal.js +++ b/addon/-private/experimental/models/internal.js @@ -4,7 +4,7 @@ import { typeOf } from '@ember/utils'; import Internal from '../../internal/internal'; import { instances, destroyInstances } from './computed/instances'; import models from './computed/models'; -import { generateModelClass, modelFactoryForShortName } from '../../util/model'; +import { generateModelClass, modelFactoryForShortName } from '../-model'; import { assert } from '@ember/debug'; export default Internal.extend({ diff --git a/addon/-private/less-experimental/util/model-factory.js b/addon/-private/less-experimental/util/model-factory.js index 100bedf5..cb8c9349 100644 --- a/addon/-private/less-experimental/util/model-factory.js +++ b/addon/-private/less-experimental/util/model-factory.js @@ -1,6 +1,48 @@ +import { getOwner } from '@ember/application'; +import EmberObject from '@ember/object'; +import { dasherize } from '@ember/string'; import { assert } from '@ember/debug'; import { typeOf } from '@ember/utils'; -import { generateModelClass, modelFactoryForShortName } from '../../util/model'; + +const instanceContainerKey = instance => { + // https://github.com/emberjs/ember.js/issues/10742 + let key = instance._debugContainerKey; + assert(`_debugContainerKey for ${instance} is not set`, !!key); + return key; +} + +const modelNameForParentKeyProperty = (owner, key) => { + owner = instanceContainerKey(owner).replace(':', '/'); + let name = `${owner}/property/${key}`; + if(!owner.startsWith('model/generated')) { + name = `generated/${name}`; + } + return name; +} + +export const modelFullName = name => `model:${name}`; + +const createModelClass = (parent, key, props) => { + let normalizedName = modelNameForParentKeyProperty(parent, key); + let fullName = modelFullName(normalizedName); + let owner = getOwner(parent); + let factory = owner.factoryFor(fullName); + if(!factory) { + owner.register(fullName, EmberObject.extend(props)); + factory = owner.factoryFor(fullName); + } + return factory; +} + +export const normalizedModelName = name => dasherize(name); + +const modelClassForName = (parent, name) => { + let normalizedName = normalizedModelName(name); + let fullName = modelFullName(normalizedName); + let factory = getOwner(parent).factoryFor(fullName); + assert(`model '${normalizedName}' is not registered`, !!factory); + return factory; +} const validate = (parent, key, inline, named, mapping, delegate) => { assert(`parent is required`, !!parent); @@ -39,16 +81,16 @@ export default class ModelFactory { createFactory() { let { parent, key, opts: { inline, named } } = this; if(inline) { - let modelClass = generateModelClass(parent, key, inline); + let modelClass = createModelClass(parent, key, inline); return props => modelClass.create(props); } else if(named) { if(typeof named === 'string') { - let modelClass = modelFactoryForShortName(parent, named); + let modelClass = modelClassForName(parent, named); return props => modelClass.create(props); } else { return (props, args) => { let name = named(...this.delegate.named(...args)); - let modelClass = modelFactoryForShortName(parent, name); + let modelClass = modelClassForName(parent, name); return modelClass.create(props); } } From 563ae0b7755e3717c30c983f3670cee406f2dbe9 Mon Sep 17 00:00:00 2001 From: ampatspell Date: Fri, 10 Aug 2018 01:29:09 +0300 Subject: [PATCH 40/63] reference less-experimental model class helpers to previous impl --- addon/-private/experimental/-model.js | 45 ------------------- addon/-private/experimental/model/factory.js | 8 ++-- .../-private/experimental/models/internal.js | 8 ++-- .../less-experimental/util/model-factory.js | 10 ++--- 4 files changed, 13 insertions(+), 58 deletions(-) delete mode 100644 addon/-private/experimental/-model.js diff --git a/addon/-private/experimental/-model.js b/addon/-private/experimental/-model.js deleted file mode 100644 index b38f4379..00000000 --- a/addon/-private/experimental/-model.js +++ /dev/null @@ -1,45 +0,0 @@ -import { getOwner } from '@ember/application'; -import EmberObject from '@ember/object'; -import { assert } from '@ember/debug'; -import { dasherize } from '@ember/string'; - -const containerKey = instance => { - // https://github.com/emberjs/ember.js/issues/10742 - let key = instance._debugContainerKey; - assert(`_debugContainerKey for ${instance} is not set`, !!key); - return key; -} - -const generateModelName = (owner, key) => { - owner = containerKey(owner).replace(':', '/'); - let name = `${owner}/property/${key}`; - if(!owner.startsWith('model/generated')) { - name = `generated/${name}`; - } - return name; -} - -const generateModelClassForProperties = props => EmberObject.extend(props); - -export const normalizeModelName = name => dasherize(name); -export const modelFullName = name => `model:${name}`; - -export const modelFactoryForShortName = (parent, name) => { - let normalizedName = normalizeModelName(name); - let fullName = modelFullName(normalizedName); - let factory = getOwner(parent).factoryFor(fullName); - assert(`model '${normalizedName}' is not registered`, !!factory); - return factory; -} - -export const generateModelClass = (parent, key, props) => { - let normalizedName = generateModelName(parent, key); - let fullName = modelFullName(normalizedName); - let owner = getOwner(parent); - let factory = owner.factoryFor(fullName); - if(!factory) { - owner.register(fullName, generateModelClassForProperties(props)); - factory = owner.factoryFor(fullName); - } - return factory; -} diff --git a/addon/-private/experimental/model/factory.js b/addon/-private/experimental/model/factory.js index 3997ad70..3bf6b8e8 100644 --- a/addon/-private/experimental/model/factory.js +++ b/addon/-private/experimental/model/factory.js @@ -1,9 +1,9 @@ import { typeOf } from '@ember/utils'; import { assert } from '@ember/debug'; -import { generateModelClass, modelFactoryForShortName } from '../-model'; +import { modelClassForName, createModelClassFromProperties } from '../../less-experimental/util/model-factory'; const resolveObject = (parent, key, arg) => { - let factory = generateModelClass(parent, key, arg); + let factory = createModelClassFromProperties(parent, key, arg); return { factory, requiresMapping: false @@ -11,7 +11,7 @@ const resolveObject = (parent, key, arg) => { } const resolveString = (parent, arg) => { - let factory = modelFactoryForShortName(parent, arg); + let factory = modelClassForName(parent, arg); return { factory, @@ -24,7 +24,7 @@ const resolveFunction = (parent, arg) => { let factory; if(name) { - factory = modelFactoryForShortName(parent, name); + factory = modelClassForName(parent, name); } return { diff --git a/addon/-private/experimental/models/internal.js b/addon/-private/experimental/models/internal.js index 428b8c3b..a1aadd80 100644 --- a/addon/-private/experimental/models/internal.js +++ b/addon/-private/experimental/models/internal.js @@ -4,7 +4,7 @@ import { typeOf } from '@ember/utils'; import Internal from '../../internal/internal'; import { instances, destroyInstances } from './computed/instances'; import models from './computed/models'; -import { generateModelClass, modelFactoryForShortName } from '../-model'; +import { modelClassForName, createModelClassFromProperties } from '../../less-experimental/util/model-factory'; import { assert } from '@ember/debug'; export default Internal.extend({ @@ -17,11 +17,11 @@ export default Internal.extend({ let type = typeOf(factory); const result = (type, prop) => ({ type, prop }); if(type === 'object') { - return result('class', generateModelClass(owner, key, factory)); + return result('class', createModelClassFromProperties(owner, key, factory)); } else if(type === 'string') { - return result('class', modelFactoryForShortName(owner, factory)); + return result('class', modelClassForName(owner, factory)); } else if(type === 'function') { - let fn = (...args) => modelFactoryForShortName(owner, factory(...args)); + let fn = (...args) => modelClassForName(owner, factory(...args)); return result('function', fn); } assert(`models last argument must be object, string or function`, false); diff --git a/addon/-private/less-experimental/util/model-factory.js b/addon/-private/less-experimental/util/model-factory.js index cb8c9349..7f78d814 100644 --- a/addon/-private/less-experimental/util/model-factory.js +++ b/addon/-private/less-experimental/util/model-factory.js @@ -20,9 +20,9 @@ const modelNameForParentKeyProperty = (owner, key) => { return name; } -export const modelFullName = name => `model:${name}`; +const modelFullName = name => `model:${name}`; -const createModelClass = (parent, key, props) => { +export const createModelClassFromProperties = (parent, key, props) => { let normalizedName = modelNameForParentKeyProperty(parent, key); let fullName = modelFullName(normalizedName); let owner = getOwner(parent); @@ -34,9 +34,9 @@ const createModelClass = (parent, key, props) => { return factory; } -export const normalizedModelName = name => dasherize(name); +const normalizedModelName = name => dasherize(name); -const modelClassForName = (parent, name) => { +export const modelClassForName = (parent, name) => { let normalizedName = normalizedModelName(name); let fullName = modelFullName(normalizedName); let factory = getOwner(parent).factoryFor(fullName); @@ -81,7 +81,7 @@ export default class ModelFactory { createFactory() { let { parent, key, opts: { inline, named } } = this; if(inline) { - let modelClass = createModelClass(parent, key, inline); + let modelClass = createModelClassFromProperties(parent, key, inline); return props => modelClass.create(props); } else if(named) { if(typeof named === 'string') { From abc4e43e944347774466c7523d4a2140ec8e4760 Mon Sep 17 00:00:00 2001 From: ampatspell Date: Fri, 10 Aug 2018 01:52:29 +0300 Subject: [PATCH 41/63] cleanup models --- .../less-experimental/models/runtime.js | 16 +++++----- .../models/runtime/parent.js | 32 ------------------- .../{runtime/source.js => source-observer.js} | 6 ++-- 3 files changed, 11 insertions(+), 43 deletions(-) delete mode 100644 addon/-private/less-experimental/models/runtime/parent.js rename addon/-private/less-experimental/models/{runtime/source.js => source-observer.js} (93%) diff --git a/addon/-private/less-experimental/models/runtime.js b/addon/-private/less-experimental/models/runtime.js index 8cacd8f5..7ea801a7 100644 --- a/addon/-private/less-experimental/models/runtime.js +++ b/addon/-private/less-experimental/models/runtime.js @@ -1,6 +1,6 @@ import { A } from '@ember/array'; -import ParentManager from './runtime/parent'; -import SourceManager from './runtime/source'; +import ObjectObserver from '../util/object-observer'; +import SourceObserver from './source-observer'; import ModelFactory from '../util/model-factory'; import { assert } from '@ember/debug'; import { typeOf } from '@ember/utils'; @@ -39,15 +39,15 @@ export default class ModelsRuntime { } }); - this.parentManager = new ParentManager({ - parent, + this.parentObserver = new ObjectObserver({ + object: parent, observe: opts.parent, delegate: { updated: (object, key) => this.onParentPropertyUpdated(object, key) } }); - this.sourceManager = new SourceManager({ + this.sourceObserver = new SourceObserver({ parent, source: opts.source, observe: opts.object, @@ -91,7 +91,7 @@ export default class ModelsRuntime { } rebuildModels() { - let objects = this.sourceManager.source; + let objects = this.sourceObserver.source; let models; if(objects) { models = this.createModels(objects); @@ -124,8 +124,8 @@ export default class ModelsRuntime { } destroy() { - this.parentManager.destroy(); - this.sourceManager.destroy(); + this.parentObserver.destroy(); + this.sourceObserver.destroy(); this.content.map(model => model.destroy()); } diff --git a/addon/-private/less-experimental/models/runtime/parent.js b/addon/-private/less-experimental/models/runtime/parent.js deleted file mode 100644 index 7e16170b..00000000 --- a/addon/-private/less-experimental/models/runtime/parent.js +++ /dev/null @@ -1,32 +0,0 @@ -import ObjectObserver from '../../util/object-observer'; -import { assert } from '@ember/debug'; -import { typeOf } from '@ember/utils'; - -const validate = (parent, observe, delegate) => { - assert(`parent is required`, !!parent); - assert(`observe must be array`, typeOf(observe) === 'array'); - assert(`delegate is required`, !!delegate); - assert(`delegate.updated must be function`, typeOf(delegate.updated) === 'function'); -} - -export default class ParentManager { - - // parent - - constructor({ parent, observe, delegate }) { - validate(parent, observe, delegate); - this.parent = parent; - this.observer = new ObjectObserver({ - object: parent, - observe, - delegate: { - updated: (object, key) => delegate.updated(object, key) - } - }); - } - - destroy() { - this.observer.destroy(); - } - -} diff --git a/addon/-private/less-experimental/models/runtime/source.js b/addon/-private/less-experimental/models/source-observer.js similarity index 93% rename from addon/-private/less-experimental/models/runtime/source.js rename to addon/-private/less-experimental/models/source-observer.js index 07fd9b9e..cde313e8 100644 --- a/addon/-private/less-experimental/models/runtime/source.js +++ b/addon/-private/less-experimental/models/source-observer.js @@ -1,5 +1,5 @@ -import ValueProvider from '../../util/value-provider'; -import ArrayObserver from '../../util/array-observer'; +import ValueProvider from '../util/value-provider'; +import ArrayObserver from '../util/array-observer'; import { assert } from '@ember/debug'; import { typeOf } from '@ember/utils'; @@ -16,7 +16,7 @@ const validate = (parent, source, observe, delegate) => { assert(`delegate.updated must be function`, typeOf(delegate.updated) === 'function'); } -export default class SourceManager { +export default class SourceObserver { // parent // source: { dependencies, key } From e875d74eb58aab2d62c36618816400f10106af45 Mon Sep 17 00:00:00 2001 From: ampatspell Date: Fri, 10 Aug 2018 02:04:20 +0300 Subject: [PATCH 42/63] fix notes --- NOTES.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/NOTES.md b/NOTES.md index 58ea06a0..81b07b1d 100644 --- a/NOTES.md +++ b/NOTES.md @@ -123,14 +123,14 @@ models('query.content').owner('product.type').object('data.type').named((doc, ow ## Model ``` javascript -model().owner('doc', 'product.type').object('data.type').inline({ +model().owner('doc', 'product.type').inline({ prepare(owner) { } }); ``` ``` javascript -model().owner('doc', 'product.type').object('data.type').inline({ +model().owner('doc', 'product.type').inline({ prepare({ doc, product }) { this.setProperties({ doc, product }); } @@ -145,7 +145,7 @@ model().owner('doc').named('book'); ``` ``` javascript -model().owner('doc', 'product.type').object('data.type').named(({ doc, product }) => { +model().owner('doc', 'product.type').named(({ doc, product }) => { let type = doc.get('data.type'); return `products/${product.type}/components/${type}`; }).mapping(owner => { From 3275f9ec0c6afb3ea8bb956af0954df09c2f573c Mon Sep 17 00:00:00 2001 From: ampatspell Date: Fri, 10 Aug 2018 02:04:36 +0300 Subject: [PATCH 43/63] initial model implementation --- .../-private/less-experimental/model/index.js | 27 +++++++ .../less-experimental/model/internal.js | 40 ++++++++++ .../less-experimental/model/property.js | 19 +++++ .../less-experimental/model/runtime.js | 74 +++++++++++++++++++ addon/less-experimental/index.js | 4 +- addon/less-experimental/model.js | 3 + app/initializers/zuglet-internal.js | 2 + tests/unit/less-experimental-model-test.js | 74 +++++++++++++++++++ 8 files changed, 242 insertions(+), 1 deletion(-) create mode 100644 addon/-private/less-experimental/model/index.js create mode 100644 addon/-private/less-experimental/model/internal.js create mode 100644 addon/-private/less-experimental/model/property.js create mode 100644 addon/-private/less-experimental/model/runtime.js create mode 100644 addon/less-experimental/model.js create mode 100644 tests/unit/less-experimental-model-test.js diff --git a/addon/-private/less-experimental/model/index.js b/addon/-private/less-experimental/model/index.js new file mode 100644 index 00000000..c09a3397 --- /dev/null +++ b/addon/-private/less-experimental/model/index.js @@ -0,0 +1,27 @@ +import { A } from '@ember/array'; +import { assign } from '@ember/polyfills'; +import property from './property'; + +const compact = array => A(array).compact(); + +const build = (opts, nested={}) => { + opts = assign({}, opts, nested); + let prop = property(opts); + prop.owner = (...args) => build(opts, { parent: compact(args) }); + prop.inline = arg => build(opts, { inline: arg }); + prop.named = arg => build(opts, { named: arg }); + prop.mapping = arg => build(opts, { mapping: arg }); + return prop; +} + +export default () => { + let opts = { + parent: [], + object: [], + inline: undefined, + named: undefined, + mapping: undefined + }; + + return build(opts, {}); +} diff --git a/addon/-private/less-experimental/model/internal.js b/addon/-private/less-experimental/model/internal.js new file mode 100644 index 00000000..366ae14e --- /dev/null +++ b/addon/-private/less-experimental/model/internal.js @@ -0,0 +1,40 @@ +import Internal from '../../internal/internal'; +import Runtime from './runtime'; + +export default Internal.extend({ + + parent: null, + key: null, + opts: null, + + runtime(create) { + let runtime = this._runtime; + if(!runtime && create) { + let { parent, key, opts } = this.getProperties('parent', 'key', 'opts'); + runtime = new Runtime(parent, key, opts, { + updated: () => this.notifyPropertyChange() + }); + this._runtime = runtime; + } + return runtime; + }, + + notifyPropertyChange() { + let { parent, key } = this; + if(parent.isDestroying) { + return; + } + parent.notifyPropertyChange(key); + }, + + model(create) { + let runtime = this.runtime(create); + return runtime && runtime.content; + }, + + willDestroy() { + this._super(...arguments); + this._runtime && this._runtime.destroy(); + } + +}); diff --git a/addon/-private/less-experimental/model/property.js b/addon/-private/less-experimental/model/property.js new file mode 100644 index 00000000..f8857541 --- /dev/null +++ b/addon/-private/less-experimental/model/property.js @@ -0,0 +1,19 @@ +import destroyable from '../../util/destroyable'; +import { getOwner } from '@ember/application'; + +const get = internal => internal.model(true); +const reusable = () => false; + +const create = opts => function(key) { + let parent = this; + let owner = getOwner(this); + return owner.factoryFor('zuglet:less-experimental/model/internal').create({ parent, key, opts }); +} + +export default opts => { + return destroyable({ + create: create(opts), + reusable, + get + }); +} diff --git a/addon/-private/less-experimental/model/runtime.js b/addon/-private/less-experimental/model/runtime.js new file mode 100644 index 00000000..990dcfdb --- /dev/null +++ b/addon/-private/less-experimental/model/runtime.js @@ -0,0 +1,74 @@ +import ModelFactory from '../util/model-factory'; +import ObjectObserver from '../util/object-observer'; +import { assert } from '@ember/debug'; +import { typeOf } from '@ember/utils'; + +const validate = (opts, delegate) => { + assert(`opts must be object`, typeOf(opts) === 'object'); + assert(`delegate must be object`, typeOf(delegate) === 'object'); + assert(`delegate.updated must be function`, typeOf(delegate.updated) === 'function'); +} + +export default class ModelsRuntime { + + constructor(parent, key, opts, delegate) { + validate(opts, delegate); + this.parent = parent; + this.key = key; + this.opts = opts; + this.delegate = delegate; + + this.modelFactory = new ModelFactory({ + parent, + key, + inline: opts.inline, + named: opts.named, + mapping: opts.mapping, + delegate: { + prepare: () => [ parent ], + named: () => [ parent ] + } + }); + + this.parentObserver = new ObjectObserver({ + object: parent, + observe: opts.parent, + delegate: { + updated: (object, key) => this.onParentPropertyUpdated(object, key) + } + }); + + this.rebuildModel(false); + } + + createModel() { + let { model } = this.modelFactory.create(); + return model; + } + + rebuildModel(notify) { + let current = this.content; + + let model = this.createModel() || undefined; + this.content = model; + + if(current) { + current.destroy(); + } + + if(current !== model && notify) { + this.delegate.updated(model); + } + } + + onParentPropertyUpdated(object, key) { + this.rebuildModel(true); + } + + destroy() { + this.parentObserver.destroy(); + let content = this.content; + content && content.destroy(); + } + +} diff --git a/addon/less-experimental/index.js b/addon/less-experimental/index.js index cb447884..da89e216 100644 --- a/addon/less-experimental/index.js +++ b/addon/less-experimental/index.js @@ -1,5 +1,7 @@ import models from './models'; +import model from './model'; export { - models + models, + model } diff --git a/addon/less-experimental/model.js b/addon/less-experimental/model.js new file mode 100644 index 00000000..b4384bd2 --- /dev/null +++ b/addon/less-experimental/model.js @@ -0,0 +1,3 @@ +import model from '../-private/less-experimental/model'; + +export default model; diff --git a/app/initializers/zuglet-internal.js b/app/initializers/zuglet-internal.js index 0fb06266..6d2e00d5 100644 --- a/app/initializers/zuglet-internal.js +++ b/app/initializers/zuglet-internal.js @@ -102,6 +102,7 @@ import ComputedModels from 'ember-cli-zuglet/-private/experimental/models/models import LessExperimentalModelsInternal from 'ember-cli-zuglet/-private/less-experimental/models/internal'; import LessExperimentalModels from 'ember-cli-zuglet/-private/less-experimental/models/models'; +import LessExperimentalModelInternal from 'ember-cli-zuglet/-private/less-experimental/model/internal'; export default { name: 'zuglet:internal', @@ -250,6 +251,7 @@ export default { container.register('zuglet:computed/models', ComputedModels); // less-experimental + container.register('zuglet:less-experimental/model/internal', LessExperimentalModelInternal); container.register('zuglet:less-experimental/models/internal', LessExperimentalModelsInternal); container.register('zuglet:less-experimental/models', LessExperimentalModels); } diff --git a/tests/unit/less-experimental-model-test.js b/tests/unit/less-experimental-model-test.js new file mode 100644 index 00000000..8a37aeb4 --- /dev/null +++ b/tests/unit/less-experimental-model-test.js @@ -0,0 +1,74 @@ +import EmberObject from '@ember/object'; +import { module, test, setupStoreTest } from '../helpers/setup'; +import { getOwner } from '@ember/application'; +import { model } from 'ember-cli-zuglet/less-experimental'; +import { A } from '@ember/array'; +import { run } from '@ember/runloop'; + +const Owner = EmberObject.extend({ +}); + +module('less-experimental-model', function(hooks) { + setupStoreTest(hooks); + + hooks.beforeEach(async function() { + this.getOwner = () => getOwner(this.store); + this.object = props => EmberObject.create(props); + this.registerModel = (name, factory) => this.getOwner().register(`model:${name}`, factory); + this.subject = (props, name='subject') => { + let factory = Owner.extend(props); + let owner = this.getOwner(); + let fullName = `component:${name}`; + owner.register(fullName, factory); + return owner.factoryFor(fullName).create(); + }; + }); + + test('model is created', async function(assert) { + let subject = this.subject({ + name: 'duck', + model: model().inline({ + prepare(owner) { + this.setProperties(owner.getProperties('name')); + } + }) + }); + + let first = subject.get('model'); + assert.ok(first); + + assert.equal(first.get('name'), 'duck'); + + run(() => subject.destroy()); + + assert.ok(first.isDestroying); + }); + + test('model is recreated on owner dep change', async function(assert) { + let subject = this.subject({ + name: 'duck', + model: model().owner('name').inline({ + prepare(owner) { + this.setProperties(owner.getProperties('name')); + } + }) + }); + + let first = subject.get('model'); + assert.equal(first.get('name'), 'duck'); + + run(() => subject.set('name', 'hamster')); + + assert.ok(first.isDestroying); + + let second = run(() => subject.get('model')); + + assert.ok(second !== first); + assert.equal(second.get('name'), 'hamster'); + + run(() => subject.destroy()); + + assert.ok(second.isDestroying); + }); + +}); From b3fbff39b50aab517cccfe78d94bdec67f813505 Mon Sep 17 00:00:00 2001 From: ampatspell Date: Fri, 10 Aug 2018 02:20:14 +0300 Subject: [PATCH 44/63] allow null model name from `named(o => ...)` --- .../less-experimental/model/runtime.js | 14 +-- .../less-experimental/util/model-factory.js | 10 +- tests/unit/less-experimental-model-test.js | 99 +++++++++++++++++++ tests/unit/less-experimental-models-test.js | 4 + 4 files changed, 118 insertions(+), 9 deletions(-) diff --git a/addon/-private/less-experimental/model/runtime.js b/addon/-private/less-experimental/model/runtime.js index 990dcfdb..c1db137a 100644 --- a/addon/-private/less-experimental/model/runtime.js +++ b/addon/-private/less-experimental/model/runtime.js @@ -47,17 +47,17 @@ export default class ModelsRuntime { } rebuildModel(notify) { - let current = this.content; + let previous = this.content; - let model = this.createModel() || undefined; - this.content = model; + let content = this.createModel(); + this.content = content; - if(current) { - current.destroy(); + if(previous) { + previous.destroy(); } - if(current !== model && notify) { - this.delegate.updated(model); + if(previous !== content && notify) { + this.delegate.updated(content); } } diff --git a/addon/-private/less-experimental/util/model-factory.js b/addon/-private/less-experimental/util/model-factory.js index 7f78d814..7482a69b 100644 --- a/addon/-private/less-experimental/util/model-factory.js +++ b/addon/-private/less-experimental/util/model-factory.js @@ -90,6 +90,9 @@ export default class ModelFactory { } else { return (props, args) => { let name = named(...this.delegate.named(...args)); + if(!name) { + return; + } let modelClass = modelClassForName(parent, name); return modelClass.create(props); } @@ -135,8 +138,11 @@ export default class ModelFactory { create(...args) { let factory = this.factory; - let model = factory({}, args); - let promise = this.prepare(model, ...args); + let model = factory({}, args) || null; + let promise; + if(model) { + promise = this.prepare(model, ...args); + } return { model, promise }; } diff --git a/tests/unit/less-experimental-model-test.js b/tests/unit/less-experimental-model-test.js index 8a37aeb4..9f955f90 100644 --- a/tests/unit/less-experimental-model-test.js +++ b/tests/unit/less-experimental-model-test.js @@ -71,4 +71,103 @@ module('less-experimental-model', function(hooks) { assert.ok(second.isDestroying); }); + test('statically named model', async function(assert) { + this.registerModel('duck', EmberObject.extend({ + modelName: 'duck', + prepare({ message }) { + this.setProperties({ message }); + } + })); + + let subject = this.subject({ + message: 'hello', + model: model().named('duck').mapping(owner => { + let message = owner.get('message'); + return { message }; + }) + }); + + let first = subject.get('model'); + assert.equal(first.get('modelName'), 'duck'); + assert.equal(first.get('message'), 'hello'); + + run(() => subject.destroy()); + }); + + test('model name lookup', async function(assert) { + let Model = EmberObject.extend({ + prepare(owner) { + this.setProperties(owner.getProperties('message')); + } + }); + this.registerModel('duck', Model.extend({ + modelName: 'duck', + })); + this.registerModel('hamster', Model.extend({ + modelName: 'hamster', + })); + + let subject = this.subject({ + message: 'hello', + type: 'duck', + model: model().owner('type').named(owner => owner.get('type')) + }); + + let first = subject.get('model'); + assert.equal(first.get('modelName'), 'duck'); + assert.equal(first.get('message'), 'hello'); + + run(() => subject.set('type', 'hamster')); + + assert.ok(first.isDestroying); + + let second = run(() => subject.get('model')); + assert.ok(second !== first); + assert.equal(second.get('modelName'), 'hamster'); + + run(() => subject.destroy()); + + assert.ok(second.isDestroying); + }); + + test('model name lookup yields null', async function(assert) { + let Model = EmberObject.extend({ + prepare(owner) { + this.setProperties(owner.getProperties('message')); + } + }); + this.registerModel('duck', Model.extend({ + modelName: 'duck', + })); + + let subject = this.subject({ + message: 'hello', + model: model().owner('type').named(owner => owner.get('type')) + }); + + let first = subject.get('model'); + assert.ok(first === null); + + run(() => subject.set('type', 'duck')); + + let second = run(() => subject.get('model')); + assert.equal(second.get('modelName'), 'duck'); + + run(() => subject.set('type', undefined)); + + assert.ok(second.isDestroying); + + let third = run(() => subject.get('model')); + assert.ok(third === null); + + run(() => subject.set('type', 'duck')); + + let fourth = run(() => subject.get('model')); + assert.equal(fourth.get('type', 'duck')); + + run(() => subject.destroy()); + + assert.ok(fourth.isDestroying); + }); + }); diff --git a/tests/unit/less-experimental-models-test.js b/tests/unit/less-experimental-models-test.js index 3572b2c7..069e125b 100644 --- a/tests/unit/less-experimental-models-test.js +++ b/tests/unit/less-experimental-models-test.js @@ -324,4 +324,8 @@ module('less-experimental-models', function(hooks) { run(() => subject.destroy()); }); + test('model class lookup yields null', async function(assert) { + assert.ok(false, 'todo'); + }); + }); From 743e91ead58d86511a86f5a5a417ffc834e523ea Mon Sep 17 00:00:00 2001 From: ampatspell Date: Fri, 10 Aug 2018 02:33:34 +0300 Subject: [PATCH 45/63] support null models in models array --- .../less-experimental/models/runtime.js | 4 +- tests/unit/less-experimental-models-test.js | 43 ++++++++++++++++++- 2 files changed, 44 insertions(+), 3 deletions(-) diff --git a/addon/-private/less-experimental/models/runtime.js b/addon/-private/less-experimental/models/runtime.js index 7ea801a7..a17508b4 100644 --- a/addon/-private/less-experimental/models/runtime.js +++ b/addon/-private/less-experimental/models/runtime.js @@ -81,7 +81,7 @@ export default class ModelsRuntime { } content.replace(start, remove, models); if(removed) { - removed.map(model => model.destroy()); + removed.map(model => model && model.destroy()); } } @@ -126,7 +126,7 @@ export default class ModelsRuntime { destroy() { this.parentObserver.destroy(); this.sourceObserver.destroy(); - this.content.map(model => model.destroy()); + this.content.map(model => model && model.destroy()); } } diff --git a/tests/unit/less-experimental-models-test.js b/tests/unit/less-experimental-models-test.js index 069e125b..5135f5ab 100644 --- a/tests/unit/less-experimental-models-test.js +++ b/tests/unit/less-experimental-models-test.js @@ -325,7 +325,48 @@ module('less-experimental-models', function(hooks) { }); test('model class lookup yields null', async function(assert) { - assert.ok(false, 'todo'); + this.registerModel('duck', EmberObject.extend({ + prepare(object) { + this.setProperties(object.getProperties('name')); + } + })); + + let yellow = this.object({ type: 'duck', name: 'yellow' }); + let green = this.object({ type: null, name: 'green' }); + let red = this.object({ type: 'duck', name: 'red' }); + + let subject = this.subject({ + all: A([ yellow, green, red ]), + models: models('all').object('type').named(object => object.get('type')) + }); + + let map = () => subject.get('models').map(i => i ? i.get('name') : i); + + assert.deepEqual(map(), [ 'yellow', null, 'red' ]); + + let first = subject.get('models').slice(); + + green.set('type', 'duck'); + + assert.deepEqual(map(), [ 'yellow', 'green', 'red' ]); + + assert.ok(!first[0].isDestroying); + assert.ok(!first[2].isDestroying); + + let second = subject.get('models').slice(); + + run(() => green.set('type')); + + assert.deepEqual(map(), [ 'yellow', null, 'red' ]); + + assert.ok(!second[0].isDestroying); + assert.ok(second[1].isDestroying); + assert.ok(!second[2].isDestroying); + + run(() => subject.destroy()); + + assert.ok(second[0].isDestroying); + assert.ok(second[2].isDestroying); }); }); From fc35d967aba71f3b442dc3208220e792ef92031b Mon Sep 17 00:00:00 2001 From: ampatspell Date: Fri, 10 Aug 2018 02:34:30 +0300 Subject: [PATCH 46/63] eslint --- addon/-private/less-experimental/model/runtime.js | 2 +- tests/unit/less-experimental-model-test.js | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/addon/-private/less-experimental/model/runtime.js b/addon/-private/less-experimental/model/runtime.js index c1db137a..6a0bdfdf 100644 --- a/addon/-private/less-experimental/model/runtime.js +++ b/addon/-private/less-experimental/model/runtime.js @@ -61,7 +61,7 @@ export default class ModelsRuntime { } } - onParentPropertyUpdated(object, key) { + onParentPropertyUpdated() { this.rebuildModel(true); } diff --git a/tests/unit/less-experimental-model-test.js b/tests/unit/less-experimental-model-test.js index 9f955f90..a3422b69 100644 --- a/tests/unit/less-experimental-model-test.js +++ b/tests/unit/less-experimental-model-test.js @@ -2,7 +2,6 @@ import EmberObject from '@ember/object'; import { module, test, setupStoreTest } from '../helpers/setup'; import { getOwner } from '@ember/application'; import { model } from 'ember-cli-zuglet/less-experimental'; -import { A } from '@ember/array'; import { run } from '@ember/runloop'; const Owner = EmberObject.extend({ From d7a6f31cf995eb7d78ea21dde01cae1b3c342ce4 Mon Sep 17 00:00:00 2001 From: ampatspell Date: Fri, 10 Aug 2018 02:43:36 +0300 Subject: [PATCH 47/63] deprecate experimental model and models --- addon/experimental/-deprecate.js | 6 ++++++ addon/experimental/index.js | 2 ++ addon/experimental/model.js | 2 ++ addon/experimental/models.js | 2 ++ addon/register.js | 2 +- 5 files changed, 13 insertions(+), 1 deletion(-) create mode 100644 addon/experimental/-deprecate.js diff --git a/addon/experimental/-deprecate.js b/addon/experimental/-deprecate.js new file mode 100644 index 00000000..33e81779 --- /dev/null +++ b/addon/experimental/-deprecate.js @@ -0,0 +1,6 @@ +import { deprecate } from '@ember/application/deprecations'; + +deprecate(`'ember-cli-zuglet/experimental is deprecated in favour of ember-cli-zuglet/less-experimental`, false, { + id: 'ember-cli-zuglet-experimental', + until: '0.6.0' +}); diff --git a/addon/experimental/index.js b/addon/experimental/index.js index c708b881..08c9eeab 100644 --- a/addon/experimental/index.js +++ b/addon/experimental/index.js @@ -3,6 +3,8 @@ import models from './models'; import observed from './observed'; import route from './model/route'; +import './-deprecate'; + export { model, models, diff --git a/addon/experimental/model.js b/addon/experimental/model.js index c04a0e65..9bc1949c 100644 --- a/addon/experimental/model.js +++ b/addon/experimental/model.js @@ -1,3 +1,5 @@ import model from '../-private/experimental/model'; +import './-deprecate'; + export default model; diff --git a/addon/experimental/models.js b/addon/experimental/models.js index f22b30e9..c9a7a45c 100644 --- a/addon/experimental/models.js +++ b/addon/experimental/models.js @@ -1,3 +1,5 @@ import models from '../-private/experimental/models'; +import './-deprecate'; + export default models; diff --git a/addon/register.js b/addon/register.js index ea1ca0d3..6ad81336 100644 --- a/addon/register.js +++ b/addon/register.js @@ -21,7 +21,7 @@ export default { }) };`, false, { id: 'ember-cli-zuglet-register', - until: '0.3.0' + until: '0.7.0' }); export default register; From 45e54aefa6ad86c27cfa117f0d81d684d276ff97 Mon Sep 17 00:00:00 2001 From: ampatspell Date: Fri, 10 Aug 2018 02:46:52 +0300 Subject: [PATCH 48/63] dummy --- .../components/ui-route/experiments/model/inline/component.js | 4 ++-- .../components/ui-route/experiments/model/model/component.js | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/dummy/app/components/ui-route/experiments/model/inline/component.js b/tests/dummy/app/components/ui-route/experiments/model/inline/component.js index d4d4b9e8..35fdff22 100644 --- a/tests/dummy/app/components/ui-route/experiments/model/inline/component.js +++ b/tests/dummy/app/components/ui-route/experiments/model/inline/component.js @@ -1,7 +1,7 @@ import Component from '@ember/component'; import layout from './template'; import observed from 'ember-cli-zuglet/experimental/observed'; -import model from 'ember-cli-zuglet/experimental/model'; +import { model } from 'ember-cli-zuglet/less-experimental'; const isKindaValidPath = path => { if(!path) { @@ -20,7 +20,7 @@ const isKindaValidPath = path => { export default Component.extend({ layout, - model: model('path', { + model: model().owner('path').inline({ doc: observed(), diff --git a/tests/dummy/app/components/ui-route/experiments/model/model/component.js b/tests/dummy/app/components/ui-route/experiments/model/model/component.js index 6489576f..5339ad07 100644 --- a/tests/dummy/app/components/ui-route/experiments/model/model/component.js +++ b/tests/dummy/app/components/ui-route/experiments/model/model/component.js @@ -1,11 +1,11 @@ import Component from '@ember/component'; import layout from './template'; -import model from 'ember-cli-zuglet/experimental/model'; +import { model } from 'ember-cli-zuglet/less-experimental'; export default Component.extend({ layout, - model: model('path', 'experiments/document-by-path').mapping(owner => { + model: model().owner('path').named('experiments/document-by-path').mapping(owner => { let path = owner.get('path'); return { path From 042b87011fd9c8076cd1215bf7107d455383e3f9 Mon Sep 17 00:00:00 2001 From: ampatspell Date: Fri, 10 Aug 2018 03:59:45 +0300 Subject: [PATCH 49/63] route model implementation --- .../-private/less-experimental/route/hooks.js | 21 ++++++ .../-private/less-experimental/route/index.js | 21 ++++++ .../less-experimental/route/internal.js | 72 +++++++++++++++++++ .../less-experimental/route/property.js | 22 ++++++ .../less-experimental/util/model-factory.js | 3 +- addon/experimental/model/route.js | 2 + addon/less-experimental/index.js | 4 +- addon/less-experimental/route.js | 3 + app/initializers/zuglet-internal.js | 2 + 9 files changed, 148 insertions(+), 2 deletions(-) create mode 100644 addon/-private/less-experimental/route/hooks.js create mode 100644 addon/-private/less-experimental/route/index.js create mode 100644 addon/-private/less-experimental/route/internal.js create mode 100644 addon/-private/less-experimental/route/property.js create mode 100644 addon/less-experimental/route.js diff --git a/addon/-private/less-experimental/route/hooks.js b/addon/-private/less-experimental/route/hooks.js new file mode 100644 index 00000000..19576e96 --- /dev/null +++ b/addon/-private/less-experimental/route/hooks.js @@ -0,0 +1,21 @@ +import { A } from '@ember/array'; + +const key = '__zuglet_hooks__'; + +const _get = (object, functionName) => object[functionName][key]; + +const extend = (object, functionName, cb) => { + let arr = _get(object, functionName); + if(!arr) { + arr = A(); + let fn = object[functionName]; + object[functionName] = function() { + fn.call(this, ...arguments); + _get(this, functionName).map(fn => fn.call(this, ...arguments)); + } + object[functionName][key] = arr; + } + arr.addObject(cb); +} + +export const onResetController = (route, cb) => extend(route, 'resetController', cb); diff --git a/addon/-private/less-experimental/route/index.js b/addon/-private/less-experimental/route/index.js new file mode 100644 index 00000000..5e465cd6 --- /dev/null +++ b/addon/-private/less-experimental/route/index.js @@ -0,0 +1,21 @@ +import { assign } from '@ember/polyfills'; +import property from './property'; + +const build = (opts, nested={}) => { + opts = assign({}, opts, nested); + let prop = property(opts); + prop.inline = arg => build(opts, { inline: arg }); + prop.named = arg => build(opts, { named: arg }); + prop.mapping = arg => build(opts, { mapping: arg }); + return prop; +} + +export default () => { + let opts = { + inline: undefined, + named: undefined, + mapping: undefined + }; + + return build(opts, {}); +} diff --git a/addon/-private/less-experimental/route/internal.js b/addon/-private/less-experimental/route/internal.js new file mode 100644 index 00000000..5d0a6eba --- /dev/null +++ b/addon/-private/less-experimental/route/internal.js @@ -0,0 +1,72 @@ +import Internal from '../../internal/internal'; +import ModelFactory from '../util/model-factory'; +import { resolve } from 'rsvp'; +import { assert } from '@ember/debug'; +import { typeOf } from '@ember/utils'; + +const validate = (opts, named) => { + assert(`opts must be object`, typeOf(opts) === 'object'); + if(!opts.named && !opts.inline) { + opts.named = named; + } +} + +const __zuglet_route_internal = '__zuglet_route_internal'; + +export const getInternal = (arg, route) => { + if(!arg) { + return; + } + if(arg.__zuglet_route_internal !== true) { + return; + } + let internal = arg._internal; + if(!internal) { + return; + } + if(internal.route !== route) { + return; + } + return internal; +} + +const keyFromRouteName = routeName => `route/${routeName.replace(/\./g, '/')}`; + +export default Internal.extend({ + + route: null, + params: null, + opts: null, + + init() { + this._super(...arguments); + let { route, params, opts } = this.getProperties('route', 'params', 'opts'); + let key = keyFromRouteName(route.routeName); + validate(opts, key); + this.modelFactory = new ModelFactory({ + parent: route, + key, + inline: opts.inline, + named: opts.named, + mapping: opts.mapping, + delegate: { + props: () => ({ [__zuglet_route_internal]: true, _internal: this }), + prepare: () => [ route, params ], + named: () => [ route, params ] + } + }); + }, + + createModel() { + let { model, promise } = this.modelFactory.create(); + let destroy = () => model.destroy(); + return { model, promise, destroy }; + }, + + load() { + let hash = this.model(true); + assert(`route model is required`, !!hash.model); + return resolve(hash.promise).then(() => hash.model); + } + +}); diff --git a/addon/-private/less-experimental/route/property.js b/addon/-private/less-experimental/route/property.js new file mode 100644 index 00000000..bf4d88b5 --- /dev/null +++ b/addon/-private/less-experimental/route/property.js @@ -0,0 +1,22 @@ +import { getOwner } from '@ember/application'; +import { onResetController } from './hooks'; +import { getInternal } from './internal'; + +export const resetController = function() { + let model = this.currentModel; + let internal = getInternal(model, this); + internal && internal.destroy(); +} + +const create = (route, params, opts) => { + let owner = getOwner(route); + return owner.factoryFor('zuglet:less-experimental/route/internal').create({ route, params, opts }); +} + +export default opts => { + return function(params) { + onResetController(this, resetController); + let internal = create(this, params, opts); + return internal.load(); + } +} diff --git a/addon/-private/less-experimental/util/model-factory.js b/addon/-private/less-experimental/util/model-factory.js index 7482a69b..30f8e6bf 100644 --- a/addon/-private/less-experimental/util/model-factory.js +++ b/addon/-private/less-experimental/util/model-factory.js @@ -138,7 +138,8 @@ export default class ModelFactory { create(...args) { let factory = this.factory; - let model = factory({}, args) || null; + let props = this.delegate.props && this.delegate.props() + let model = factory(props, args) || null; let promise; if(model) { promise = this.prepare(model, ...args); diff --git a/addon/experimental/model/route.js b/addon/experimental/model/route.js index 6e4c12ec..086521ec 100644 --- a/addon/experimental/model/route.js +++ b/addon/experimental/model/route.js @@ -1,3 +1,5 @@ import route from '../../-private/experimental/route'; +import '../-deprecate'; + export default route; diff --git a/addon/less-experimental/index.js b/addon/less-experimental/index.js index da89e216..00d53359 100644 --- a/addon/less-experimental/index.js +++ b/addon/less-experimental/index.js @@ -1,7 +1,9 @@ import models from './models'; import model from './model'; +import route from './route'; export { models, - model + model, + route } diff --git a/addon/less-experimental/route.js b/addon/less-experimental/route.js new file mode 100644 index 00000000..f219fae1 --- /dev/null +++ b/addon/less-experimental/route.js @@ -0,0 +1,3 @@ +import route from '../-private/less-experimental/route'; + +export default route; diff --git a/app/initializers/zuglet-internal.js b/app/initializers/zuglet-internal.js index 6d2e00d5..12b4aa35 100644 --- a/app/initializers/zuglet-internal.js +++ b/app/initializers/zuglet-internal.js @@ -103,6 +103,7 @@ import ComputedModels from 'ember-cli-zuglet/-private/experimental/models/models import LessExperimentalModelsInternal from 'ember-cli-zuglet/-private/less-experimental/models/internal'; import LessExperimentalModels from 'ember-cli-zuglet/-private/less-experimental/models/models'; import LessExperimentalModelInternal from 'ember-cli-zuglet/-private/less-experimental/model/internal'; +import LessExperimentalRouteInternal from 'ember-cli-zuglet/-private/less-experimental/route/internal'; export default { name: 'zuglet:internal', @@ -254,5 +255,6 @@ export default { container.register('zuglet:less-experimental/model/internal', LessExperimentalModelInternal); container.register('zuglet:less-experimental/models/internal', LessExperimentalModelsInternal); container.register('zuglet:less-experimental/models', LessExperimentalModels); + container.register('zuglet:less-experimental/route/internal', LessExperimentalRouteInternal); } } From 5162a58eed61de066d91c4af9afa6610bc7528a8 Mon Sep 17 00:00:00 2001 From: ampatspell Date: Fri, 10 Aug 2018 03:59:49 +0300 Subject: [PATCH 50/63] tests --- tests/unit/less-experimental-route-test.js | 77 ++++++++++++++++++++++ 1 file changed, 77 insertions(+) create mode 100644 tests/unit/less-experimental-route-test.js diff --git a/tests/unit/less-experimental-route-test.js b/tests/unit/less-experimental-route-test.js new file mode 100644 index 00000000..b8161b73 --- /dev/null +++ b/tests/unit/less-experimental-route-test.js @@ -0,0 +1,77 @@ +import EmberObject from '@ember/object'; +import { module, test, setupStoreTest } from '../helpers/setup'; +import { getOwner } from '@ember/application'; +import { route } from 'ember-cli-zuglet/less-experimental'; +import { run } from '@ember/runloop'; +import { resolve } from 'rsvp'; + +const Route = EmberObject.extend({ + + init() { + this._super(...arguments); + this.models = {}; + }, + + _model(params) { + return resolve(this.model(params)).then(model => this.currentModel = model); + }, + + _modelFor(key, value) { + this.models[key] = value; + }, + + modelFor(key) { + return this.models[key]; + }, + + resetController() { + } + +}); + +module('less-experimental-route', function(hooks) { + setupStoreTest(hooks); + + hooks.beforeEach(async function() { + this.getOwner = () => getOwner(this.store); + this.object = props => EmberObject.create(props); + this.registerModel = (name, factory) => this.getOwner().register(`model:${name}`, factory); + this.route = (props, name='test') => { + let factory = Route.extend(props); + let owner = this.getOwner(); + let fullName = `route:${name}`; + owner.register(fullName, factory); + return owner.factoryFor(fullName).create({ routeName: name }); + }; + }); + + test('create and destroy', async function(assert) { + let subject = this.route({ + model: route().inline({ + prepare(route, params) { + assert.ok(route === subject); + assert.equal(params.id, 'yellow'); + this.setProperties({ ok: true }); + } + }) + }); + + let promise = subject._model({ id: 'yellow' }); + + assert.ok(promise); + assert.ok(promise.then); + + let instance = await promise; + + assert.equal(instance.get('ok'), true); + + assert.ok(instance.__zuglet_route_internal); + assert.ok(instance._internal); + + run(() => subject.resetController()); + + assert.ok(instance._internal.isDestroying); + assert.ok(instance.isDestroying); + }); + +}); From 01cb7591dbae2b6df595d9ab0f4c97fb508aefd8 Mon Sep 17 00:00:00 2001 From: ampatspell Date: Fri, 10 Aug 2018 04:00:01 +0300 Subject: [PATCH 51/63] dummy uses less-experimental route model --- tests/dummy/app/routes/experiments/blogs.js | 4 ++-- tests/dummy/app/routes/experiments/blogs/blog.js | 4 ++-- tests/dummy/app/routes/experiments/blogs/blog/posts/post.js | 4 ++-- tests/dummy/app/routes/experiments/models.js | 5 ++--- 4 files changed, 8 insertions(+), 9 deletions(-) diff --git a/tests/dummy/app/routes/experiments/blogs.js b/tests/dummy/app/routes/experiments/blogs.js index aa32759b..d653eef3 100644 --- a/tests/dummy/app/routes/experiments/blogs.js +++ b/tests/dummy/app/routes/experiments/blogs.js @@ -1,5 +1,5 @@ import Route from '@ember/routing/route'; -import model from 'ember-cli-zuglet/experimental/model/route'; +import { route } from 'ember-cli-zuglet/less-experimental'; import observed from 'ember-cli-zuglet/experimental/observed'; import { all } from 'rsvp'; @@ -7,7 +7,7 @@ let inserted = false; export default Route.extend({ - model: model({ + model: route().inline({ blogs: observed(), diff --git a/tests/dummy/app/routes/experiments/blogs/blog.js b/tests/dummy/app/routes/experiments/blogs/blog.js index 41205492..089cb693 100644 --- a/tests/dummy/app/routes/experiments/blogs/blog.js +++ b/tests/dummy/app/routes/experiments/blogs/blog.js @@ -1,11 +1,11 @@ import Route from '@ember/routing/route'; -import model from 'ember-cli-zuglet/experimental/model/route'; +import { route } from 'ember-cli-zuglet/less-experimental'; import observed from 'ember-cli-zuglet/experimental/observed'; import { reject } from 'rsvp'; export default Route.extend({ - model: model({ + model: route().inline({ blog: null, posts: observed(), diff --git a/tests/dummy/app/routes/experiments/blogs/blog/posts/post.js b/tests/dummy/app/routes/experiments/blogs/blog/posts/post.js index b558d9ba..eb1311b8 100644 --- a/tests/dummy/app/routes/experiments/blogs/blog/posts/post.js +++ b/tests/dummy/app/routes/experiments/blogs/blog/posts/post.js @@ -1,9 +1,9 @@ import Route from '@ember/routing/route'; -import model from 'ember-cli-zuglet/experimental/model/route'; +import { route } from 'ember-cli-zuglet/less-experimental'; export default Route.extend({ - model: model().mapping((route, params) => { + model: route().mapping((route, params) => { let blog = route.modelFor('experiments.blogs.blog'); let id = params.post_id; return { diff --git a/tests/dummy/app/routes/experiments/models.js b/tests/dummy/app/routes/experiments/models.js index be599a14..1c328745 100644 --- a/tests/dummy/app/routes/experiments/models.js +++ b/tests/dummy/app/routes/experiments/models.js @@ -1,12 +1,11 @@ import Route from '@ember/routing/route'; -import model from 'ember-cli-zuglet/experimental/model/route'; -import models from 'ember-cli-zuglet/less-experimental/models'; +import { route, models } from 'ember-cli-zuglet/less-experimental'; import observed from 'ember-cli-zuglet/experimental/observed'; import { readOnly } from '@ember/object/computed'; export default Route.extend({ - model: model({ + model: route().inline({ query: observed(), From e9d1644dd475b96f96de02617e94bd30ac0719d3 Mon Sep 17 00:00:00 2001 From: ampatspell Date: Fri, 10 Aug 2018 04:13:23 +0300 Subject: [PATCH 52/63] noop less-experimental observed --- .../less-experimental/observed/index.js | 4 ++ addon/experimental/-deprecate.js | 10 ++--- addon/less-experimental/index.js | 5 ++- addon/less-experimental/observed.js | 7 ++++ tests/unit/less-experimental-observed-test.js | 41 +++++++++++++++++++ 5 files changed, 61 insertions(+), 6 deletions(-) create mode 100644 addon/-private/less-experimental/observed/index.js create mode 100644 addon/less-experimental/observed.js create mode 100644 tests/unit/less-experimental-observed-test.js diff --git a/addon/-private/less-experimental/observed/index.js b/addon/-private/less-experimental/observed/index.js new file mode 100644 index 00000000..d4c82c0d --- /dev/null +++ b/addon/-private/less-experimental/observed/index.js @@ -0,0 +1,4 @@ +export const observerFor = () => {}; + +export default () => { +} diff --git a/addon/experimental/-deprecate.js b/addon/experimental/-deprecate.js index 33e81779..eb1e3d9a 100644 --- a/addon/experimental/-deprecate.js +++ b/addon/experimental/-deprecate.js @@ -1,6 +1,6 @@ -import { deprecate } from '@ember/application/deprecations'; +// import { deprecate } from '@ember/application/deprecations'; -deprecate(`'ember-cli-zuglet/experimental is deprecated in favour of ember-cli-zuglet/less-experimental`, false, { - id: 'ember-cli-zuglet-experimental', - until: '0.6.0' -}); +// deprecate(`'ember-cli-zuglet/experimental is deprecated in favour of ember-cli-zuglet/less-experimental`, false, { +// id: 'ember-cli-zuglet-experimental', +// until: '0.6.0' +// }); diff --git a/addon/less-experimental/index.js b/addon/less-experimental/index.js index 00d53359..582b2cb5 100644 --- a/addon/less-experimental/index.js +++ b/addon/less-experimental/index.js @@ -1,9 +1,12 @@ import models from './models'; import model from './model'; import route from './route'; +import observed, { observerFor } from './observed'; export { models, model, - route + route, + observed, + observerFor } diff --git a/addon/less-experimental/observed.js b/addon/less-experimental/observed.js new file mode 100644 index 00000000..9410df9a --- /dev/null +++ b/addon/less-experimental/observed.js @@ -0,0 +1,7 @@ +import observed, { observerFor } from '../-private/less-experimental/observed'; + +export { + observerFor +} + +export default observed; diff --git a/tests/unit/less-experimental-observed-test.js b/tests/unit/less-experimental-observed-test.js new file mode 100644 index 00000000..2a5eb3b7 --- /dev/null +++ b/tests/unit/less-experimental-observed-test.js @@ -0,0 +1,41 @@ +import EmberObject from '@ember/object'; +import { module, test, setupStoreTest } from '../helpers/setup'; +import { getOwner } from '@ember/application'; +import { observed, observerFor } from 'ember-cli-zuglet/less-experimental'; +import { run } from '@ember/runloop'; + +const Owner = EmberObject.extend({ +}); + +module('less-experimental-observed', function(hooks) { + setupStoreTest(hooks); + + hooks.beforeEach(async function() { + this.getOwner = () => getOwner(this.store); + this.object = props => EmberObject.create(props); + this.registerModel = (name, factory) => this.getOwner().register(`model:${name}`, factory); + this.subject = (props, name='subject') => { + let factory = Owner.extend(props); + let owner = this.getOwner(); + let fullName = `component:${name}`; + owner.register(fullName, factory); + return owner.factoryFor(fullName).create(); + }; + }); + + test('settable observed', async function(assert) { + let subject = this.subject({ + observed: observed(), + }); + + let doc = this.store.doc('ducks/yellow').existing(); + assert.ok(!doc.isObserved); + + subject.set('observed', doc); + assert.ok(doc.isObserved); + + run(() => subject.destroy()); + assert.ok(!doc.isObserved); + }); + +}); From 7200bcdc17a0efd02a9f3c850ee90f4c32eca9c4 Mon Sep 17 00:00:00 2001 From: ampatspell Date: Fri, 10 Aug 2018 05:17:37 +0300 Subject: [PATCH 53/63] less-experimental observers. dynamic and writable --- .../less-experimental/observed/index.js | 20 +++++- .../less-experimental/observed/internal.js | 69 +++++++++++++++++++ .../observed/internal/dynamic.js | 55 +++++++++++++++ .../observed/internal/writable.js | 4 ++ .../observed/observer-for.js | 6 ++ .../less-experimental/observed/property.js | 31 +++++++++ .../observed/resolve-observers.js | 4 ++ addon/experimental/-deprecate.js | 10 +-- addon/experimental/observed.js | 2 + addon/less-experimental/index.js | 5 +- addon/less-experimental/observed.js | 7 +- app/initializers/zuglet-internal.js | 4 ++ tests/unit/less-experimental-observed-test.js | 60 +++++++++++++++- 13 files changed, 264 insertions(+), 13 deletions(-) create mode 100644 addon/-private/less-experimental/observed/internal.js create mode 100644 addon/-private/less-experimental/observed/internal/dynamic.js create mode 100644 addon/-private/less-experimental/observed/internal/writable.js create mode 100644 addon/-private/less-experimental/observed/observer-for.js create mode 100644 addon/-private/less-experimental/observed/property.js create mode 100644 addon/-private/less-experimental/observed/resolve-observers.js diff --git a/addon/-private/less-experimental/observed/index.js b/addon/-private/less-experimental/observed/index.js index d4c82c0d..3dcfaf93 100644 --- a/addon/-private/less-experimental/observed/index.js +++ b/addon/-private/less-experimental/observed/index.js @@ -1,4 +1,22 @@ -export const observerFor = () => {}; +import { A } from '@ember/array'; +import { assign } from '@ember/polyfills'; +import property from './property'; + +const compact = array => A(array).compact(); + +const build = (opts, nested={}) => { + opts = assign({}, opts, nested); + let prop = property(opts); + prop.owner = (...args) => build(opts, { parent: compact(args) }); + prop.content = arg => build(opts, { content: arg }); + return prop; +} export default () => { + let opts = { + parent: [], + content: undefined + }; + + return build(opts, {}) } diff --git a/addon/-private/less-experimental/observed/internal.js b/addon/-private/less-experimental/observed/internal.js new file mode 100644 index 00000000..a4c5ed41 --- /dev/null +++ b/addon/-private/less-experimental/observed/internal.js @@ -0,0 +1,69 @@ +import EmberObject from '@ember/object'; +import { assert } from '@ember/debug'; +import { typeOf } from '@ember/utils'; + +const validate = opts => { + assert(`opts must be object`, typeOf(opts) === 'object'); + assert(`opts.parent must be array`, typeOf(opts.parent) === 'array'); + if(opts.content) { + assert(`opts.content must be function`, typeOf(opts.content) === 'function'); + } +} + +export default EmberObject.extend({ + + parent: null, + key: null, + opts: null, + + observable: null, + observer: null, + + init() { + this._super(...arguments); + validate(this.opts); + }, + + getObservable() { + return this.observable; + }, + + getObserver() { + return this.observer; + }, + + setObservable(next) { + if(this.observable === next) { + return next; + } + + this.stopObserving(); + this.observable = next; + this.startObserving(); + + return next; + }, + + stopObserving() { + let observer = this.observer; + if(!observer) { + return; + } + observer.cancel(); + this.observer = null; + }, + + startObserving() { + let observable = this.observable; + if(!observable) { + return; + } + this.observer = observable.observe(); + }, + + willDestroy() { + this.stopObserving(); + this._super(...arguments); + } + +}); diff --git a/addon/-private/less-experimental/observed/internal/dynamic.js b/addon/-private/less-experimental/observed/internal/dynamic.js new file mode 100644 index 00000000..ad8c1301 --- /dev/null +++ b/addon/-private/less-experimental/observed/internal/dynamic.js @@ -0,0 +1,55 @@ +import Internal from '../internal'; +import ObjectObserver from '../../util/object-observer'; + +export default Internal.extend({ + + init() { + this._super(...arguments); + + let { parent, opts } = this.getProperties('parent', 'opts'); + + this.parentObserver = new ObjectObserver({ + object: parent, + observe: opts.parent, + delegate: { + updated: (object, key) => this.onParentPropertyUpdated(object, key) + } + }); + + this.update(false); + }, + + notifyPropertyChange() { + let { parent, key } = this.getProperties('parent', 'key'); + if(parent.isDestroying) { + return; + } + parent.notifyPropertyChange(key); + }, + + update(notify) { + let { parent, opts: { content } } = this.getProperties('parent', 'opts'); + + let next = content(parent) || null; + + if(this.observable === next) { + return; + } + + this.setObservable(next); + + if(notify) { + this.notifyPropertyChange(); + } + }, + + onParentPropertyUpdated() { + this.update(true); + }, + + willDestroy() { + this._super(...arguments); + this.parentObserver.destroy(); + } + +}); diff --git a/addon/-private/less-experimental/observed/internal/writable.js b/addon/-private/less-experimental/observed/internal/writable.js new file mode 100644 index 00000000..5d6c4ac2 --- /dev/null +++ b/addon/-private/less-experimental/observed/internal/writable.js @@ -0,0 +1,4 @@ +import Internal from '../internal'; + +export default Internal.extend({ +}); diff --git a/addon/-private/less-experimental/observed/observer-for.js b/addon/-private/less-experimental/observed/observer-for.js new file mode 100644 index 00000000..55b73485 --- /dev/null +++ b/addon/-private/less-experimental/observed/observer-for.js @@ -0,0 +1,6 @@ +import { cacheFor } from '../../util/destroyable'; + +export default (owner, key) => { + let internal = cacheFor(owner, key); + return internal && internal.getObserver(); +} diff --git a/addon/-private/less-experimental/observed/property.js b/addon/-private/less-experimental/observed/property.js new file mode 100644 index 00000000..6c2ecddc --- /dev/null +++ b/addon/-private/less-experimental/observed/property.js @@ -0,0 +1,31 @@ +import destroyable from '../../util/destroyable'; +import { getOwner } from '@ember/application'; + +const reusable = () => true; +const get = internal => internal.getObservable(); +const set = (internal, value) => internal.setObservable(value); + +const factoryName = opts => { + let { type } = opts; + return `zuglet:less-experimental/observed/${type}/internal`; +} + +const create = opts => { + let factory = factoryName(opts); + return function(key) { + let parent = this; + let owner = getOwner(this); + let instance = owner.factoryFor(factory).create({ parent, key, opts }); + return instance; + } +} + +export default opts => { + opts.type = opts.content ? 'dynamic' : 'writable'; + return destroyable({ + reusable, + create: create(opts), + get, + set: opts.type === 'writable' ? set : null + }); +}; diff --git a/addon/-private/less-experimental/observed/resolve-observers.js b/addon/-private/less-experimental/observed/resolve-observers.js new file mode 100644 index 00000000..d21a3c1f --- /dev/null +++ b/addon/-private/less-experimental/observed/resolve-observers.js @@ -0,0 +1,4 @@ +import { all } from 'rsvp'; +import { get } from '@ember/object'; + +export default (...args) => all(args.map(prop => prop && get(prop, 'observers.promise'))); diff --git a/addon/experimental/-deprecate.js b/addon/experimental/-deprecate.js index eb1e3d9a..33e81779 100644 --- a/addon/experimental/-deprecate.js +++ b/addon/experimental/-deprecate.js @@ -1,6 +1,6 @@ -// import { deprecate } from '@ember/application/deprecations'; +import { deprecate } from '@ember/application/deprecations'; -// deprecate(`'ember-cli-zuglet/experimental is deprecated in favour of ember-cli-zuglet/less-experimental`, false, { -// id: 'ember-cli-zuglet-experimental', -// until: '0.6.0' -// }); +deprecate(`'ember-cli-zuglet/experimental is deprecated in favour of ember-cli-zuglet/less-experimental`, false, { + id: 'ember-cli-zuglet-experimental', + until: '0.6.0' +}); diff --git a/addon/experimental/observed.js b/addon/experimental/observed.js index b9681206..1f079cdf 100644 --- a/addon/experimental/observed.js +++ b/addon/experimental/observed.js @@ -1,5 +1,7 @@ import { observed, observerFor } from '../-private/experimental/observed'; +import './-deprecate'; + export { observerFor } diff --git a/addon/less-experimental/index.js b/addon/less-experimental/index.js index 582b2cb5..f8f58669 100644 --- a/addon/less-experimental/index.js +++ b/addon/less-experimental/index.js @@ -1,12 +1,13 @@ import models from './models'; import model from './model'; import route from './route'; -import observed, { observerFor } from './observed'; +import observed, { observerFor, resolveObservers } from './observed'; export { models, model, route, observed, - observerFor + observerFor, + resolveObservers } diff --git a/addon/less-experimental/observed.js b/addon/less-experimental/observed.js index 9410df9a..2983ed3c 100644 --- a/addon/less-experimental/observed.js +++ b/addon/less-experimental/observed.js @@ -1,7 +1,10 @@ -import observed, { observerFor } from '../-private/less-experimental/observed'; +import observed from '../-private/less-experimental/observed'; +import observerFor from '../-private/less-experimental/observed/observer-for'; +import resolveObservers from '../-private/less-experimental/observed/resolve-observers'; export { - observerFor + observerFor, + resolveObservers } export default observed; diff --git a/app/initializers/zuglet-internal.js b/app/initializers/zuglet-internal.js index 12b4aa35..f5b7198e 100644 --- a/app/initializers/zuglet-internal.js +++ b/app/initializers/zuglet-internal.js @@ -104,6 +104,8 @@ import LessExperimentalModelsInternal from 'ember-cli-zuglet/-private/less-exper import LessExperimentalModels from 'ember-cli-zuglet/-private/less-experimental/models/models'; import LessExperimentalModelInternal from 'ember-cli-zuglet/-private/less-experimental/model/internal'; import LessExperimentalRouteInternal from 'ember-cli-zuglet/-private/less-experimental/route/internal'; +import LessExperimentalObservedDynamicInternal from 'ember-cli-zuglet/-private/less-experimental/observed/internal/dynamic'; +import LessExperimentalObservedWritableInternal from 'ember-cli-zuglet/-private/less-experimental/observed/internal/writable'; export default { name: 'zuglet:internal', @@ -256,5 +258,7 @@ export default { container.register('zuglet:less-experimental/models/internal', LessExperimentalModelsInternal); container.register('zuglet:less-experimental/models', LessExperimentalModels); container.register('zuglet:less-experimental/route/internal', LessExperimentalRouteInternal); + container.register('zuglet:less-experimental/observed/writable/internal', LessExperimentalObservedWritableInternal); + container.register('zuglet:less-experimental/observed/dynamic/internal', LessExperimentalObservedDynamicInternal); } } diff --git a/tests/unit/less-experimental-observed-test.js b/tests/unit/less-experimental-observed-test.js index 2a5eb3b7..6d63382c 100644 --- a/tests/unit/less-experimental-observed-test.js +++ b/tests/unit/less-experimental-observed-test.js @@ -23,19 +23,73 @@ module('less-experimental-observed', function(hooks) { }; }); - test('settable observed', async function(assert) { + test('witable observed', async function(assert) { let subject = this.subject({ observed: observed(), }); let doc = this.store.doc('ducks/yellow').existing(); - assert.ok(!doc.isObserved); + assert.ok(doc.get('isObserving') === false); + + subject.set('observed', doc); + assert.ok(doc.get('isObserving') === true); + + subject.set('observed'); + assert.ok(doc.get('isObserving') === false); subject.set('observed', doc); - assert.ok(doc.isObserved); + assert.ok(doc.get('isObserving') === true); run(() => subject.destroy()); assert.ok(!doc.isObserved); }); + test('dynamic observed', async function(assert) { + let subject = this.subject({ + doc: null, + observed: observed().owner('doc').content(owner => owner.get('doc')) + }); + + let doc = this.store.doc('ducks/yellow').existing(); + assert.ok(subject.get('observed') === null); + + subject.set('doc', doc); + assert.ok(subject.get('observed.isObserving') === true); + assert.ok(doc.get('isObserving') === true); + + subject.set('doc', null); + assert.ok(subject.get('observed') === null); + assert.ok(doc.get('isObserving') === false); + + subject.set('doc', doc); + + assert.ok(subject.get('observed.isObserving') === true); + assert.ok(doc.get('isObserving') === true); + + run(() => subject.destroy()); + + assert.ok(doc.get('isObserving') === false); + }); + + test('observer for', async function(assert) { + let subject = this.subject({ + doc: null, + observed: observed().owner('doc').content(owner => owner.get('doc')) + }); + + let doc = this.store.doc('ducks/yellow').existing(); + + assert.ok(observerFor(subject, 'observed') === undefined); + + subject.set('doc', doc); + assert.ok(observerFor(subject, 'observed') === undefined); + + subject.get('observed'); + let instance = observerFor(subject, 'observed'); + assert.ok(instance); + assert.ok(instance.get('doc', doc)); + + run(() => subject.destroy()); + }); + }); From 17abfa1c1993b45ba685b617961ffede0fed3b07 Mon Sep 17 00:00:00 2001 From: ampatspell Date: Fri, 10 Aug 2018 05:17:52 +0300 Subject: [PATCH 54/63] dummy uses less-experimental everywhere --- .../ui-route/experiments/document/edit/component.js | 2 +- .../ui-route/experiments/model/inline/component.js | 3 +-- tests/dummy/app/models/experiments/document-by-path.js | 2 +- tests/dummy/app/routes/experiments/blogs.js | 3 +-- tests/dummy/app/routes/experiments/blogs/blog.js | 5 ++--- tests/dummy/app/routes/experiments/models.js | 3 +-- 6 files changed, 7 insertions(+), 11 deletions(-) diff --git a/tests/dummy/app/components/ui-route/experiments/document/edit/component.js b/tests/dummy/app/components/ui-route/experiments/document/edit/component.js index 3e2e8a92..e5a581de 100644 --- a/tests/dummy/app/components/ui-route/experiments/document/edit/component.js +++ b/tests/dummy/app/components/ui-route/experiments/document/edit/component.js @@ -1,6 +1,6 @@ import Component from '@ember/component'; import layout from './template'; -import observed from 'ember-cli-zuglet/experimental/observed'; +import { observed } from 'ember-cli-zuglet/less-experimental'; export default Component.extend({ layout, diff --git a/tests/dummy/app/components/ui-route/experiments/model/inline/component.js b/tests/dummy/app/components/ui-route/experiments/model/inline/component.js index 35fdff22..f11fff3b 100644 --- a/tests/dummy/app/components/ui-route/experiments/model/inline/component.js +++ b/tests/dummy/app/components/ui-route/experiments/model/inline/component.js @@ -1,7 +1,6 @@ import Component from '@ember/component'; import layout from './template'; -import observed from 'ember-cli-zuglet/experimental/observed'; -import { model } from 'ember-cli-zuglet/less-experimental'; +import { model, observed } from 'ember-cli-zuglet/less-experimental'; const isKindaValidPath = path => { if(!path) { diff --git a/tests/dummy/app/models/experiments/document-by-path.js b/tests/dummy/app/models/experiments/document-by-path.js index 1ad2b490..481d4b8e 100644 --- a/tests/dummy/app/models/experiments/document-by-path.js +++ b/tests/dummy/app/models/experiments/document-by-path.js @@ -1,5 +1,5 @@ import EmberObject from '@ember/object'; -import observed from 'ember-cli-zuglet/experimental/observed'; +import { observed } from 'ember-cli-zuglet/less-experimental'; const isKindaValidPath = path => { if(!path) { diff --git a/tests/dummy/app/routes/experiments/blogs.js b/tests/dummy/app/routes/experiments/blogs.js index d653eef3..afea3971 100644 --- a/tests/dummy/app/routes/experiments/blogs.js +++ b/tests/dummy/app/routes/experiments/blogs.js @@ -1,6 +1,5 @@ import Route from '@ember/routing/route'; -import { route } from 'ember-cli-zuglet/less-experimental'; -import observed from 'ember-cli-zuglet/experimental/observed'; +import { route, observed } from 'ember-cli-zuglet/less-experimental'; import { all } from 'rsvp'; let inserted = false; diff --git a/tests/dummy/app/routes/experiments/blogs/blog.js b/tests/dummy/app/routes/experiments/blogs/blog.js index 089cb693..c1568231 100644 --- a/tests/dummy/app/routes/experiments/blogs/blog.js +++ b/tests/dummy/app/routes/experiments/blogs/blog.js @@ -1,6 +1,5 @@ import Route from '@ember/routing/route'; -import { route } from 'ember-cli-zuglet/less-experimental'; -import observed from 'ember-cli-zuglet/experimental/observed'; +import { route, observed, resolveObservers } from 'ember-cli-zuglet/less-experimental'; import { reject } from 'rsvp'; export default Route.extend({ @@ -26,7 +25,7 @@ export default Route.extend({ posts }); - return posts.get('observers.promise'); + return resolveObservers(posts); } }) diff --git a/tests/dummy/app/routes/experiments/models.js b/tests/dummy/app/routes/experiments/models.js index 1c328745..3141bd00 100644 --- a/tests/dummy/app/routes/experiments/models.js +++ b/tests/dummy/app/routes/experiments/models.js @@ -1,6 +1,5 @@ import Route from '@ember/routing/route'; -import { route, models } from 'ember-cli-zuglet/less-experimental'; -import observed from 'ember-cli-zuglet/experimental/observed'; +import { route, models, observed } from 'ember-cli-zuglet/less-experimental'; import { readOnly } from '@ember/object/computed'; export default Route.extend({ From 83adc663630350b49d6c0d4330f195ef3aebcd37 Mon Sep 17 00:00:00 2001 From: ampatspell Date: Fri, 10 Aug 2018 06:04:15 +0300 Subject: [PATCH 55/63] support the case when mapping returns null --- .../less-experimental/util/model-factory.js | 32 +++++++++----- tests/unit/less-experimental-model-test.js | 42 ++++++++++++++++++- 2 files changed, 62 insertions(+), 12 deletions(-) diff --git a/addon/-private/less-experimental/util/model-factory.js b/addon/-private/less-experimental/util/model-factory.js index 30f8e6bf..7755872b 100644 --- a/addon/-private/less-experimental/util/model-factory.js +++ b/addon/-private/less-experimental/util/model-factory.js @@ -88,8 +88,8 @@ export default class ModelFactory { let modelClass = modelClassForName(parent, named); return props => modelClass.create(props); } else { - return (props, args) => { - let name = named(...this.delegate.named(...args)); + return (props, args, mapped) => { + let name = named(...this.delegate.named(...args), mapped); if(!name) { return; } @@ -114,6 +114,9 @@ export default class ModelFactory { if(mapping) { return (...args) => { let ret = mapping(...args); + if(!ret) { + return; + } return [ ret ]; } } @@ -129,20 +132,27 @@ export default class ModelFactory { return process; } - prepare(model, ...args) { + map(...args) { let prepare = this.delegate.prepare(...args); - let mapped = this.mapping(...prepare); + return this.mapping(...prepare); + } + + prepare(model, args) { assert(`'prepare' function is required for ${model}`, typeOf(model.prepare) === 'function'); - return model.prepare(...mapped); + return model.prepare(...args); } create(...args) { - let factory = this.factory; - let props = this.delegate.props && this.delegate.props() - let model = factory(props, args) || null; - let promise; - if(model) { - promise = this.prepare(model, ...args); + let mapped = this.map(...args); + let model = null; + let promise = null; + if(mapped) { + let factory = this.factory; + let props = this.delegate.props && this.delegate.props(); + model = factory(props, args, mapped) || null; + if(model) { + promise = this.prepare(model, mapped); + } } return { model, promise }; } diff --git a/tests/unit/less-experimental-model-test.js b/tests/unit/less-experimental-model-test.js index a3422b69..53e2c686 100644 --- a/tests/unit/less-experimental-model-test.js +++ b/tests/unit/less-experimental-model-test.js @@ -162,7 +162,47 @@ module('less-experimental-model', function(hooks) { run(() => subject.set('type', 'duck')); let fourth = run(() => subject.get('model')); - assert.equal(fourth.get('type', 'duck')); + assert.equal(fourth.get('modelName'), 'duck'); + + run(() => subject.destroy()); + + assert.ok(fourth.isDestroying); + }); + + test('mapping returns null', async function(assert) { + let subject = this.subject({ + model: model().owner('type').inline({ + prepare({ type }) { + this.setProperties({ type }); + } + }).mapping(owner => { + let type = owner.get('type'); + if(!type) { + return; + } + return { type }; + }) + }); + + let first = subject.get('model'); + assert.ok(first === null); + + run(() => subject.set('type', 'duck')); + + let second = run(() => subject.get('model')); + assert.equal(second.get('type'), 'duck'); + + run(() => subject.set('type', undefined)); + + assert.ok(second.isDestroying); + + let third = run(() => subject.get('model')); + assert.ok(third === null); + + run(() => subject.set('type', 'hamster')); + + let fourth = run(() => subject.get('model')); + assert.equal(fourth.get('type'), 'hamster'); run(() => subject.destroy()); From d008cc304f38fcc2b9eacb60a7c9d8db6860ac3a Mon Sep 17 00:00:00 2001 From: ampatspell Date: Fri, 10 Aug 2018 06:08:26 +0300 Subject: [PATCH 56/63] docs --- docs/api/models.md | 25 +++++++++++++++++++++--- docs/api/models/model.md | 38 ++++++------------------------------- docs/api/models/models.md | 19 +++++++++++-------- docs/api/models/observed.md | 31 ++++++++++++++++++++++++++++-- docs/api/models/route.md | 12 ++++-------- 5 files changed, 72 insertions(+), 53 deletions(-) diff --git a/docs/api/models.md b/docs/api/models.md index afcce637..d0455591 100644 --- a/docs/api/models.md +++ b/docs/api/models.md @@ -5,16 +5,35 @@ pos: 10 # Experimental Models +> Documentation is provided for 2nd iteration located in `less-experimental` module + Addon contains tools which allows you to easily create models that manages lifecycle of document and query observers in the scope of routes and components. ``` javascript -import observed, { observerFor } from 'ember-cli-zuglet/experimental/observed'; +import observed, { observerFor, resolveObservers } from 'ember-cli-zuglet/less-experimental/observed'; +``` + +``` javascript +import route from 'ember-cli-zuglet/less-experimental/route'; ``` ``` javascript -import model from 'ember-cli-zuglet/experimental/model/route'; +import models from 'ember-cli-zuglet/less-experimental/models'; ``` ``` javascript -import model from 'ember-cli-zuglet/experimental/model'; +import model from 'ember-cli-zuglet/less-experimental/model'; +``` + +or + +``` javascript +import { + route, + model, + models, + observed, + observerFor, + resolveObservers +} from 'ember-cli-zuglet/less-experimental'; ``` diff --git a/docs/api/models/model.md b/docs/api/models/model.md index aaef33cc..56e19841 100644 --- a/docs/api/models/model.md +++ b/docs/api/models/model.md @@ -3,20 +3,17 @@ pos: 3 --- ``` javascript -import model from 'ember-cli-zuglet/experimental/model'; +import { model } from 'ember-cli-zuglet/less-experimental'; ``` # Model -* inline -* provided name - ## Inline ``` javascript export default Component.extend({ - model: model('path', { + model: model().owner('path').inline({ prepare(owner) { } @@ -31,7 +28,7 @@ export default Component.extend({ ``` javascript export default Component.extend({ - model: model('path', { + model: model().owner('path').inline({ prepare({ path }) { } @@ -50,15 +47,13 @@ export default Component.extend({ ``` javascript export default Component.extend({ - model: model('path', 'document-by-path').mapping(owner => ({ + model: model().owner('path').named('document-by-path').mapping(owner => ({ path: owner.path })) }); ``` -* mapping is required - ## Resolved name ``` javascript @@ -66,34 +61,13 @@ export default Component.extend({ type: 'book', - model: model('type', 'path', owner => { + model: model().owner('type', 'path').named(owner => { let type = owner.type; if(!type) { return; } return `document/${type}`; - }).mapping(owner => ({ - path: owner.path - })) - -}); -``` - -* mapping is required -* doesn't support `reusable()` - -## Reusable (inline and provided) - -``` javascript -export default Component.extend({ - - model: model('path', { - - prepare(owner) { - // called every time owner.path changes - } - - }).reusable() + }).mapping(owner => ({ path: owner.path })) }); ``` diff --git a/docs/api/models/models.md b/docs/api/models/models.md index 35be8682..36e9a078 100644 --- a/docs/api/models/models.md +++ b/docs/api/models/models.md @@ -3,7 +3,7 @@ pos: 4 --- ``` javascript -import models from 'ember-cli-zuglet/experimental/models'; +import { models } from 'ember-cli-zuglet/less-experimental'; ``` # Models @@ -17,7 +17,7 @@ export default Component.extend({ query: observed(), - models: model('query.content', { + models: models('query.content').inline({ prepare(doc, owner) { this.setProperties({ doc }); @@ -35,7 +35,7 @@ export default Component.extend({ query: observed(), - models: model('query.content', 'book') + models: models('query.content').named('book') }); ``` @@ -56,7 +56,7 @@ export default Component.extend({ query: observed(), - models: model('query.content', 'book').mapping((doc, owner) => { + models: models('query.content').named('book').mapping((doc, owner) => { return { doc, owner }; }) @@ -79,7 +79,7 @@ export default Component.extend({ query: observed(), - models: model('query.content', 'data.type', (doc, owner) => { + models: models('query.content').object('data.type').named((doc, owner) => { let type = doc.data.getProperties('type'); return `book/${type}`; }) @@ -103,9 +103,12 @@ export default Component.extend({ query: observed(), - models: model('query.content', 'data.type', (doc, owner) => { - let type = doc.data.getProperties('type'); - return `book/${type}`; + type: 'book', + + models: model('query.content').owner('type').object('data.type').named((doc, owner) => { + let doc = doc.data.getProperties('type'); + let owner = owner.type; + return `${owner}/${type}`; }).mapping((doc, owner) => { return { doc, owner }; }); diff --git a/docs/api/models/observed.md b/docs/api/models/observed.md index e7bc29ec..e532cd75 100644 --- a/docs/api/models/observed.md +++ b/docs/api/models/observed.md @@ -3,18 +3,20 @@ pos: 0 --- ``` javascript -import observed, { observerFor } from 'ember-cli-zuglet/experimental/observed'; +import { observed, observerFor } from 'ember-cli-zuglet/less-experimental'; ``` # Observed `observed` is computed property which observes document or query. +## Writable (static) + ``` javascript // app/models/doc.js import EmberObject from '@ember/object'; -import observed from 'ember-cli-zuglet/experimental/observed'; +import { observed } from 'ember-cli-zuglet/less-experimental'; export default EmberObject.extend({ @@ -35,3 +37,28 @@ doc.isObserved // => true model.destroy(); // or model.set('doc', null); doc.isObserved // => false ``` + +## Dynamic + +Read only, resolved + +``` javascript +// app/models/doc.js + +import EmberObject from '@ember/object'; +import { observed } from 'ember-cli-zuglet/less-experimental'; + +export default EmberObject.extend({ + + id: null, + + doc: observed().owner('id').content(owner => { + let id = owner.id; + if(!id) { + return; + } + return this.store.doc(`document/${id}`); + }) + +}); +``` diff --git a/docs/api/models/route.md b/docs/api/models/route.md index 05040a2c..9874711d 100644 --- a/docs/api/models/route.md +++ b/docs/api/models/route.md @@ -3,21 +3,17 @@ pos: 2 --- ``` javascript -import model from 'ember-cli-zuglet/experimental/model/route'; +import { route } from 'ember-cli-zuglet/less-experimental'; ``` # Route -* inline -* looked up from route name -* provided name - ## Inline without mapping ``` javascript export default Route.extend({ - model: model({ + model: route().inline({ prepare(route, params) { } @@ -32,7 +28,7 @@ export default Route.extend({ ``` javascript export default Route.extend({ - model: model({ + model: route().inline({ prepare({ sources, id }) { } @@ -63,7 +59,7 @@ export default Route.extend({ ``` javascript export default Route.extend({ - model: model('route/source').mapping((route, params) => ({ + model: model().named('route/source').mapping((route, params) => ({ sources: route.modelFor('sources'), id: params.source_id })) From 6d79081b0fab42f9e6b95312f7f4ccf5d98a58d0 Mon Sep 17 00:00:00 2001 From: ampatspell Date: Fri, 10 Aug 2018 06:13:13 +0300 Subject: [PATCH 57/63] docs --- docs/api/models.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/api/models.md b/docs/api/models.md index d0455591..d5c3af76 100644 --- a/docs/api/models.md +++ b/docs/api/models.md @@ -3,9 +3,9 @@ title: Models pos: 10 --- -# Experimental Models +# (Less) Experimental Models -> Documentation is provided for 2nd iteration located in `less-experimental` module +> Documentation is provided for 2nd iteration located in `less-experimental` module. If all goes well, this will be moved to `models`. Addon contains tools which allows you to easily create models that manages lifecycle of document and query observers in the scope of routes and components. From 917dad4de64d0d36831660f6230f33a85fdc2114 Mon Sep 17 00:00:00 2001 From: ampatspell Date: Fri, 10 Aug 2018 06:44:14 +0300 Subject: [PATCH 58/63] heh, async functions are objects for Ember.typeOf --- addon/-private/less-experimental/util/model-factory.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/addon/-private/less-experimental/util/model-factory.js b/addon/-private/less-experimental/util/model-factory.js index 7755872b..7ac362f1 100644 --- a/addon/-private/less-experimental/util/model-factory.js +++ b/addon/-private/less-experimental/util/model-factory.js @@ -138,7 +138,7 @@ export default class ModelFactory { } prepare(model, args) { - assert(`'prepare' function is required for ${model}`, typeOf(model.prepare) === 'function'); + assert(`'prepare' function is required for ${model}`, typeof model.prepare === 'function'); return model.prepare(...args); } From 0a30b1bf7fd26657a1354ed044cc0e432e2371a0 Mon Sep 17 00:00:00 2001 From: ampatspell Date: Fri, 10 Aug 2018 10:05:11 +0300 Subject: [PATCH 59/63] add model.load only for route models --- addon/-private/less-experimental/route/internal.js | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/addon/-private/less-experimental/route/internal.js b/addon/-private/less-experimental/route/internal.js index 5d0a6eba..af912d47 100644 --- a/addon/-private/less-experimental/route/internal.js +++ b/addon/-private/less-experimental/route/internal.js @@ -32,6 +32,12 @@ export const getInternal = (arg, route) => { const keyFromRouteName = routeName => `route/${routeName.replace(/\./g, '/')}`; +const load = model => { + if(typeof model.load === 'function') { + return model.load(); + } +} + export default Internal.extend({ route: null, @@ -66,7 +72,10 @@ export default Internal.extend({ load() { let hash = this.model(true); assert(`route model is required`, !!hash.model); - return resolve(hash.promise).then(() => hash.model); + return resolve() + .then(() => hash.promise) + .then(() => load(hash.model)) + .then(() => hash.model); } }); From bc36b866dc5ace89aaf8832b9ce1bb87022daf04 Mon Sep 17 00:00:00 2001 From: ampatspell Date: Fri, 10 Aug 2018 10:12:20 +0300 Subject: [PATCH 60/63] few failing tests for default prepare --- .../less-experimental/util/model-factory.js | 2 +- tests/unit/less-experimental-model-test.js | 24 ++++++++++++++++ tests/unit/less-experimental-models-test.js | 28 +++++++++++++++++++ 3 files changed, 53 insertions(+), 1 deletion(-) diff --git a/addon/-private/less-experimental/util/model-factory.js b/addon/-private/less-experimental/util/model-factory.js index 7ac362f1..a9fd42fb 100644 --- a/addon/-private/less-experimental/util/model-factory.js +++ b/addon/-private/less-experimental/util/model-factory.js @@ -138,7 +138,7 @@ export default class ModelFactory { } prepare(model, args) { - assert(`'prepare' function is required for ${model}`, typeof model.prepare === 'function'); + assert(`models require 'prepare' prepare function if mapping is not provided`, typeof model.prepare === 'function'); return model.prepare(...args); } diff --git a/tests/unit/less-experimental-model-test.js b/tests/unit/less-experimental-model-test.js index 53e2c686..99fcd965 100644 --- a/tests/unit/less-experimental-model-test.js +++ b/tests/unit/less-experimental-model-test.js @@ -209,4 +209,28 @@ module('less-experimental-model', function(hooks) { assert.ok(fourth.isDestroying); }); + test('default prepare for mapping', async function(assert) { + let subject = this.subject({ + message: 'hello', + model: model().inline({ + }).mapping(owner => { + let message = owner.get('message'); + return { message }; + }) + }); + assert.equal(subject.get('model.message', 'hello')); + }); + + test('prepare is required if no mapping', async function(assert) { + let subject = this.subject({ + message: 'hello', + model: model().inline({}) + }); + try { + subject.get('model'); + } catch(err) { + assert.equal(err.message, `Assertion Failed: models require 'prepare' prepare function if mapping is not provided`); + } + }); + }); diff --git a/tests/unit/less-experimental-models-test.js b/tests/unit/less-experimental-models-test.js index 5135f5ab..db29503f 100644 --- a/tests/unit/less-experimental-models-test.js +++ b/tests/unit/less-experimental-models-test.js @@ -369,4 +369,32 @@ module('less-experimental-models', function(hooks) { assert.ok(second[2].isDestroying); }); + test('default prepare for mapping', async function(assert) { + let subject = this.subject({ + message: 'hello', + array: A([ Ember.Object.create() ]), + model: models('array').inline({ + }).mapping((doc, owner) => { + let message = owner.get('message'); + return { message }; + }) + }); + assert.equal(subject.get('model.message', 'hello')); + }); + + test('prepare is required if no mapping', async function(assert) { + let subject = this.subject({ + message: 'hello', + array: A([ Ember.Object.create() ]), + model: models('array').inline({ + }) + }); + try { + subject.get('model'); + } catch(err) { + assert.equal(err.message, `Assertion Failed: models require 'prepare' prepare function if mapping is not provided`); + } + }); + + }); From 1466c14373c8c1275f15bbeb1ec285e0e9d1fa4e Mon Sep 17 00:00:00 2001 From: ampatspell Date: Fri, 10 Aug 2018 10:36:25 +0300 Subject: [PATCH 61/63] default prepare if mapping is provided --- .../less-experimental/model/runtime.js | 2 +- .../less-experimental/models/runtime.js | 2 +- .../less-experimental/route/internal.js | 2 +- .../less-experimental/util/model-factory.js | 21 ++++++++++++---- tests/unit/less-experimental-model-test.js | 4 ++-- tests/unit/less-experimental-models-test.js | 6 ++--- tests/unit/less-experimental-route-test.js | 24 +++++++++++++++++++ 7 files changed, 48 insertions(+), 13 deletions(-) diff --git a/addon/-private/less-experimental/model/runtime.js b/addon/-private/less-experimental/model/runtime.js index 6a0bdfdf..c3b834b5 100644 --- a/addon/-private/less-experimental/model/runtime.js +++ b/addon/-private/less-experimental/model/runtime.js @@ -25,7 +25,7 @@ export default class ModelsRuntime { named: opts.named, mapping: opts.mapping, delegate: { - prepare: () => [ parent ], + mapping: () => [ parent ], named: () => [ parent ] } }); diff --git a/addon/-private/less-experimental/models/runtime.js b/addon/-private/less-experimental/models/runtime.js index a17508b4..41859d99 100644 --- a/addon/-private/less-experimental/models/runtime.js +++ b/addon/-private/less-experimental/models/runtime.js @@ -34,7 +34,7 @@ export default class ModelsRuntime { named: opts.named, mapping: opts.mapping, delegate: { - prepare: object => [ object, parent ], + mapping: object => [ object, parent ], named: object => [ object, parent ] } }); diff --git a/addon/-private/less-experimental/route/internal.js b/addon/-private/less-experimental/route/internal.js index af912d47..404a1ac9 100644 --- a/addon/-private/less-experimental/route/internal.js +++ b/addon/-private/less-experimental/route/internal.js @@ -57,7 +57,7 @@ export default Internal.extend({ mapping: opts.mapping, delegate: { props: () => ({ [__zuglet_route_internal]: true, _internal: this }), - prepare: () => [ route, params ], + mapping: () => [ route, params ], named: () => [ route, params ] } }); diff --git a/addon/-private/less-experimental/util/model-factory.js b/addon/-private/less-experimental/util/model-factory.js index a9fd42fb..fb6d4e29 100644 --- a/addon/-private/less-experimental/util/model-factory.js +++ b/addon/-private/less-experimental/util/model-factory.js @@ -59,7 +59,7 @@ const validate = (parent, key, inline, named, mapping, delegate) => { assert(`inline or named is requied, not both`, !inline || !named); assert(`inline or named is required`, inline || named); assert(`delegate must be object`, typeOf(delegate) === 'object'); - assert(`delegate.prepare must be function`, typeOf(delegate.prepare) === 'function'); + assert(`delegate.mapping must be function`, typeOf(delegate.mapping) === 'function'); assert(`delegate.named must be function`, typeOf(delegate.named) === 'function'); } @@ -133,13 +133,24 @@ export default class ModelFactory { } map(...args) { - let prepare = this.delegate.prepare(...args); + let prepare = this.delegate.mapping(...args); return this.mapping(...prepare); } prepare(model, args) { - assert(`models require 'prepare' prepare function if mapping is not provided`, typeof model.prepare === 'function'); - return model.prepare(...args); + if(typeof model.prepare === 'function') { + model.prepare(...args); + return; + } + + let mapping = this.opts.mapping; + if(mapping) { + let [ arg ] = args; + model.setProperties(arg); + return; + } + + assert(`models require 'prepare' function if mapping is not provided`, false); } create(...args) { @@ -151,7 +162,7 @@ export default class ModelFactory { let props = this.delegate.props && this.delegate.props(); model = factory(props, args, mapped) || null; if(model) { - promise = this.prepare(model, mapped); + this.prepare(model, mapped); } } return { model, promise }; diff --git a/tests/unit/less-experimental-model-test.js b/tests/unit/less-experimental-model-test.js index 99fcd965..697365e3 100644 --- a/tests/unit/less-experimental-model-test.js +++ b/tests/unit/less-experimental-model-test.js @@ -218,7 +218,7 @@ module('less-experimental-model', function(hooks) { return { message }; }) }); - assert.equal(subject.get('model.message', 'hello')); + assert.equal(subject.get('model.message'), 'hello'); }); test('prepare is required if no mapping', async function(assert) { @@ -229,7 +229,7 @@ module('less-experimental-model', function(hooks) { try { subject.get('model'); } catch(err) { - assert.equal(err.message, `Assertion Failed: models require 'prepare' prepare function if mapping is not provided`); + assert.equal(err.message, `Assertion Failed: models require 'prepare' function if mapping is not provided`); } }); diff --git a/tests/unit/less-experimental-models-test.js b/tests/unit/less-experimental-models-test.js index db29503f..baf13131 100644 --- a/tests/unit/less-experimental-models-test.js +++ b/tests/unit/less-experimental-models-test.js @@ -372,7 +372,7 @@ module('less-experimental-models', function(hooks) { test('default prepare for mapping', async function(assert) { let subject = this.subject({ message: 'hello', - array: A([ Ember.Object.create() ]), + array: A([ EmberObject.create() ]), model: models('array').inline({ }).mapping((doc, owner) => { let message = owner.get('message'); @@ -385,14 +385,14 @@ module('less-experimental-models', function(hooks) { test('prepare is required if no mapping', async function(assert) { let subject = this.subject({ message: 'hello', - array: A([ Ember.Object.create() ]), + array: A([ EmberObject.create() ]), model: models('array').inline({ }) }); try { subject.get('model'); } catch(err) { - assert.equal(err.message, `Assertion Failed: models require 'prepare' prepare function if mapping is not provided`); + assert.equal(err.message, `Assertion Failed: models require 'prepare' function if mapping is not provided`); } }); diff --git a/tests/unit/less-experimental-route-test.js b/tests/unit/less-experimental-route-test.js index b8161b73..56e1e3d7 100644 --- a/tests/unit/less-experimental-route-test.js +++ b/tests/unit/less-experimental-route-test.js @@ -74,4 +74,28 @@ module('less-experimental-route', function(hooks) { assert.ok(instance.isDestroying); }); + test('default prepare for mapping', async function(assert) { + let subject = this.route({ + model: route().inline({ + }).mapping((route, params) => { + return { id: params.id }; + }) + }); + + let instance = await subject._model({ id: 'yellow' }); + assert.equal(instance.get('id'), 'yellow'); + }); + + test('prepare is required if no mapping', async function(assert) { + let subject = this.route({ + model: route().inline({ + }) + }); + try { + await subject._model({ id: 'yellow' }); + } catch(err) { + assert.equal(err.message, `Assertion Failed: models require 'prepare' function if mapping is not provided`); + } + }); + }); From d47917ddffc6581d5bd2a3375837af54e395e73a Mon Sep 17 00:00:00 2001 From: ampatspell Date: Fri, 10 Aug 2018 10:41:16 +0300 Subject: [PATCH 62/63] remove assumption that prepare may return promise --- addon/-private/less-experimental/model/runtime.js | 3 +-- addon/-private/less-experimental/models/runtime.js | 7 ++----- addon/-private/less-experimental/route/internal.js | 13 ++++--------- .../less-experimental/util/model-factory.js | 3 +-- tests/unit/less-experimental-models-test.js | 3 +-- 5 files changed, 9 insertions(+), 20 deletions(-) diff --git a/addon/-private/less-experimental/model/runtime.js b/addon/-private/less-experimental/model/runtime.js index c3b834b5..403ab490 100644 --- a/addon/-private/less-experimental/model/runtime.js +++ b/addon/-private/less-experimental/model/runtime.js @@ -42,8 +42,7 @@ export default class ModelsRuntime { } createModel() { - let { model } = this.modelFactory.create(); - return model; + return this.modelFactory.create(); } rebuildModel(notify) { diff --git a/addon/-private/less-experimental/models/runtime.js b/addon/-private/less-experimental/models/runtime.js index 41859d99..d5d80796 100644 --- a/addon/-private/less-experimental/models/runtime.js +++ b/addon/-private/less-experimental/models/runtime.js @@ -67,10 +67,7 @@ export default class ModelsRuntime { createModels(objects) { let factory = this.modelFactory; - return objects.map(object => { - let { model } = factory.create(object); - return model; - }); + return objects.map(object => factory.create(object)); } replaceModels(start, remove, models) { @@ -86,7 +83,7 @@ export default class ModelsRuntime { } replaceModel(idx, object) { - let { model } = this.modelFactory.create(object); + let model = this.modelFactory.create(object); this.replaceModels(idx, 1, [ model ]); } diff --git a/addon/-private/less-experimental/route/internal.js b/addon/-private/less-experimental/route/internal.js index 404a1ac9..f4c1e6cc 100644 --- a/addon/-private/less-experimental/route/internal.js +++ b/addon/-private/less-experimental/route/internal.js @@ -64,18 +64,13 @@ export default Internal.extend({ }, createModel() { - let { model, promise } = this.modelFactory.create(); - let destroy = () => model.destroy(); - return { model, promise, destroy }; + return this.modelFactory.create(); }, load() { - let hash = this.model(true); - assert(`route model is required`, !!hash.model); - return resolve() - .then(() => hash.promise) - .then(() => load(hash.model)) - .then(() => hash.model); + let model = this.model(true); + assert(`route model is required`, !!model); + return resolve().then(() => load(model)).then(() => model); } }); diff --git a/addon/-private/less-experimental/util/model-factory.js b/addon/-private/less-experimental/util/model-factory.js index fb6d4e29..61a85c0f 100644 --- a/addon/-private/less-experimental/util/model-factory.js +++ b/addon/-private/less-experimental/util/model-factory.js @@ -156,7 +156,6 @@ export default class ModelFactory { create(...args) { let mapped = this.map(...args); let model = null; - let promise = null; if(mapped) { let factory = this.factory; let props = this.delegate.props && this.delegate.props(); @@ -165,7 +164,7 @@ export default class ModelFactory { this.prepare(model, mapped); } } - return { model, promise }; + return model; } } diff --git a/tests/unit/less-experimental-models-test.js b/tests/unit/less-experimental-models-test.js index baf13131..dfd7f015 100644 --- a/tests/unit/less-experimental-models-test.js +++ b/tests/unit/less-experimental-models-test.js @@ -386,8 +386,7 @@ module('less-experimental-models', function(hooks) { let subject = this.subject({ message: 'hello', array: A([ EmberObject.create() ]), - model: models('array').inline({ - }) + model: models('array').inline({}) }); try { subject.get('model'); From 887c657b0d49f4e2e63a118a3bb40bc3b12881d2 Mon Sep 17 00:00:00 2001 From: ampatspell Date: Fri, 10 Aug 2018 11:22:07 +0300 Subject: [PATCH 63/63] docs --- docs/api/models/model.md | 13 +++++++++++++ docs/api/models/models.md | 9 ++++++++- docs/api/models/route.md | 36 ++++++++++++++++++++++++++++++++++++ 3 files changed, 57 insertions(+), 1 deletion(-) diff --git a/docs/api/models/model.md b/docs/api/models/model.md index 56e19841..7ab6298b 100644 --- a/docs/api/models/model.md +++ b/docs/api/models/model.md @@ -31,6 +31,7 @@ export default Component.extend({ model: model().owner('path').inline({ prepare({ path }) { + this.setProperties(arguments[0]); // default if prepare() is not provided } }).mapping(owner => ({ @@ -71,3 +72,15 @@ export default Component.extend({ }); ``` + +``` javascript +// models/document/book.ks + +export default EmberObject.extend({ + + prepare({ path }) { + this.setProperties(arguments[0]); // default if prepare() is not provided + } + +}) +``` diff --git a/docs/api/models/models.md b/docs/api/models/models.md index 36e9a078..6110e492 100644 --- a/docs/api/models/models.md +++ b/docs/api/models/models.md @@ -43,9 +43,11 @@ export default Component.extend({ ``` javascript // models/book.js export default EmberObject.extend({ + prepare(doc, owner) { this.setProperties({ doc }); } + }) ``` @@ -66,9 +68,11 @@ export default Component.extend({ ``` javascript // models/book.js export default EmberObject.extend({ + prepare({ doc, owner }) { - this.setProperties({ doc }); + this.setProperties(arguments[0]); // default if prepare() is not provided } + }) ``` @@ -90,9 +94,11 @@ export default Component.extend({ ``` javascript // models/book.js export default EmberObject.extend({ + prepare(doc) { this.setProperties({ doc }); } + }) ``` @@ -120,6 +126,7 @@ export default Component.extend({ // models/book.js export default EmberObject.extend({ prepare({ doc, owner }) { + // this.setProperties(arguments[0]); -- default if prepare() is not provided this.setProperties({ doc }); } }) diff --git a/docs/api/models/route.md b/docs/api/models/route.md index 9874711d..c8720dc2 100644 --- a/docs/api/models/route.md +++ b/docs/api/models/route.md @@ -16,6 +16,9 @@ export default Route.extend({ model: route().inline({ prepare(route, params) { + }, + + async load() { } }) @@ -31,6 +34,10 @@ export default Route.extend({ model: route().inline({ prepare({ sources, id }) { + this.setProperties(arguments[0]); // default if prepare() is not provided + }, + + async load() { } }).mapping((route, params) => ({ @@ -44,6 +51,7 @@ export default Route.extend({ ## Looked up from route name ``` javascript +// route/sources/source.js export default Route.extend({ model: model().mapping((route, params) => ({ @@ -54,6 +62,20 @@ export default Route.extend({ }); ``` +``` javascript +// models/route/sources/source.js +export default EmberObject.extend({ + + prepare({ sources, id }) { + this.setProperties(arguments[0]); // default if prepare() is not provided + }, + + async load() { + } + +}); +``` + ## Provided name ``` javascript @@ -66,3 +88,17 @@ export default Route.extend({ }); ``` + +``` javascript +// models/route/sources.js +export default EmberObject.extend({ + + prepare({ sources, id }) { + this.setProperties(arguments[0]); // default if prepare() is not provided + }, + + async load() { + } + +}); +```