diff --git a/packages/ember-glimmer/lib/component-managers/curly.ts b/packages/ember-glimmer/lib/component-managers/curly.ts index fcfbe3bd87b..154d8b120e4 100644 --- a/packages/ember-glimmer/lib/component-managers/curly.ts +++ b/packages/ember-glimmer/lib/component-managers/curly.ts @@ -23,7 +23,7 @@ import { WithDynamicTagName, WithStaticLayout, } from '@glimmer/runtime'; -import { Destroyable, EMPTY_ARRAY, Opaque } from '@glimmer/util'; +import { Destroyable, EMPTY_ARRAY } from '@glimmer/util'; import { privatize as P } from 'container'; import { assert, @@ -206,7 +206,7 @@ export default class CurlyComponentManager extends AbstractManager, hasBlock: boolean): ComponentStateBucket { + create(environment: Environment, state: DefinitionState, args: Arguments, dynamicScope: DynamicScope, callerSelfRef: VersionedPathReference, hasBlock: boolean): ComponentStateBucket { if (DEBUG) { this._pushToDebugStack(`component:${state.name}`, environment); } @@ -298,7 +298,7 @@ export default class CurlyComponentManager extends AbstractManager { + getSelf({ component }: ComponentStateBucket): VersionedPathReference { return component[ROOT_REF]; } diff --git a/packages/ember-glimmer/lib/environment.ts b/packages/ember-glimmer/lib/environment.ts index edbbafb67c2..483a69a24b6 100644 --- a/packages/ember-glimmer/lib/environment.ts +++ b/packages/ember-glimmer/lib/environment.ts @@ -1,10 +1,9 @@ import { - Reference, + OpaqueIterable, VersionedReference, } from '@glimmer/reference'; import { ElementBuilder, Environment as GlimmerEnvironment, - PrimitiveReference, SimpleDynamicAttribute, } from '@glimmer/runtime'; import { @@ -21,7 +20,6 @@ import DebugStack from './utils/debug-stack'; import createIterable from './utils/iterable'; import { ConditionalReference, - RootPropertyReference, UpdatableReference, } from './utils/references'; import { isHTMLSafe } from './utils/string'; @@ -45,7 +43,7 @@ export default class Environment extends GlimmerEnvironment { public destroyedComponents: Destroyable[]; public debugStack: typeof DebugStack; - public inTransaction: boolean; + public inTransaction = false; constructor(injections: any) { super(injections); @@ -72,11 +70,11 @@ export default class Environment extends GlimmerEnvironment { return lookupComponent(meta.owner, name, meta); } - toConditionalReference(reference: UpdatableReference): ConditionalReference | RootPropertyReference | PrimitiveReference { + toConditionalReference(reference: UpdatableReference): VersionedReference { return ConditionalReference.create(reference); } - iterableFor(ref: Reference, key: string) { + iterableFor(ref: VersionedReference, key: string): OpaqueIterable { return createIterable(ref, key); } @@ -86,23 +84,23 @@ export default class Environment extends GlimmerEnvironment { } } - scheduleUpdateModifier(modifier: any, manager: any) { + scheduleUpdateModifier(modifier: any, manager: any): void { if (this.isInteractive) { super.scheduleUpdateModifier(modifier, manager); } } - didDestroy(destroyable: Destroyable) { + didDestroy(destroyable: Destroyable): void { destroyable.destroy(); } - begin() { + begin(): void { this.inTransaction = true; super.begin(); } - commit() { + commit(): void { let destroyedComponents = this.destroyedComponents; this.destroyedComponents = []; // components queued for destruction must be destroyed before firing diff --git a/packages/ember-glimmer/lib/helpers/each-in.ts b/packages/ember-glimmer/lib/helpers/each-in.ts index 5f55124ab7c..78f866554b7 100644 --- a/packages/ember-glimmer/lib/helpers/each-in.ts +++ b/packages/ember-glimmer/lib/helpers/each-in.ts @@ -1,10 +1,12 @@ /** @module ember */ +import { Tag, VersionedPathReference } from '@glimmer/reference'; import { Arguments, VM } from '@glimmer/runtime'; +import { Opaque } from '@glimmer/util'; import { symbol } from 'ember-utils'; /** @@ -115,12 +117,27 @@ import { symbol } from 'ember-utils'; */ const EACH_IN_REFERENCE = symbol('EACH_IN'); -export function isEachIn(ref: any): boolean { - return ref && ref[EACH_IN_REFERENCE]; +class EachInReference implements VersionedPathReference { + public tag: Tag; + + constructor(private inner: VersionedPathReference) { + this.tag = inner.tag; + this[EACH_IN_REFERENCE] = true; + } + + value(): Opaque { + return this.inner.value(); + } + + get(key: string): VersionedPathReference { + return this.inner.get(key); + } +} + +export function isEachIn(ref: Opaque): ref is VersionedPathReference { + return ref !== null && typeof ref === 'object' && ref[EACH_IN_REFERENCE]; } export default function(_vm: VM, args: Arguments) { - let ref = Object.create(args.positional.at(0)); - ref[EACH_IN_REFERENCE] = true; - return ref; + return new EachInReference(args.positional.at(0)); } diff --git a/packages/ember-glimmer/lib/utils/iterable.ts b/packages/ember-glimmer/lib/utils/iterable.ts index 29cf785a3f4..f9d3deb72e7 100644 --- a/packages/ember-glimmer/lib/utils/iterable.ts +++ b/packages/ember-glimmer/lib/utils/iterable.ts @@ -1,209 +1,258 @@ import { + AbstractIterable, combine, CONSTANT_TAG, IterationItem, - TagWrapper, + OpaqueIterator, + Tag, UpdatableTag, + VersionedReference } from '@glimmer/reference'; -import { Opaque } from '@glimmer/util'; -import { - get, - isProxy, - objectAt, - tagFor, - tagForProperty -} from 'ember-metal'; -import { - _contentFor, - isEmberArray -} from 'ember-runtime'; -import { guidFor } from 'ember-utils'; +import { Opaque, Option } from '@glimmer/util'; +import { assert } from 'ember-debug'; +import { get, isProxy, objectAt, tagFor, tagForProperty } from 'ember-metal'; +import { _contentFor, isEmberArray } from 'ember-runtime'; +import { guidFor, HAS_NATIVE_SYMBOL } from 'ember-utils'; import { isEachIn } from '../helpers/each-in'; -import { - UpdatablePrimitiveReference, - UpdatableReference, -} from './references'; +import { UpdatableReference } from './references'; const ITERATOR_KEY_GUID = 'be277757-bbbe-4620-9fcb-213ef433cca2'; -type KeyFor = (value: any, memo: any) => any; +// FIXME: export this from Glimmer +type OpaqueIterationItem = IterationItem; +type EmberIterable = AbstractIterable; -export default function iterableFor(ref: any, keyPath: string) { +export default function iterableFor(ref: VersionedReference, keyPath: string | null | undefined): EmberIterable { if (isEachIn(ref)) { - return new EachInIterable(ref, keyForEachIn(keyPath)); + return new EachInIterable(ref, keyPath || '@key'); } else { - return new ArrayIterable(ref, keyForArray(keyPath)); + return new EachIterable(ref, keyPath || '@identity'); } } -function keyForEachIn(keyPath: string | undefined | null) { - switch (keyPath) { - case '@index': - case undefined: - case null: - return index; - case '@identity': - return identity; - default: - return (item: any) => get(item, keyPath); - } -} +abstract class BoundedIterator implements OpaqueIterator { + private position = 0; -function keyForArray(keyPath: string | undefined | null) { - switch (keyPath) { - case '@index': - return index; - case '@identity': - case undefined: - case null: - return identity; - default: - return (item: any) => get(item, keyPath); + constructor(private length: number, private keyFor: KeyFor) {} + + isEmpty(): false { + return false; } -} -function index(_item: any, i: any): string { - return String(i); -} + abstract valueFor(position: number): Opaque; -function identity(item: any) { - switch (typeof item) { - case 'string': - case 'number': - return String(item); - default: - return guidFor(item); + memoFor(position: number): Opaque { + return position; } -} -function ensureUniqueKey(seen: any, key: string) { - let seenCount = seen[key]; + next(): Option { + let { length, keyFor, position } = this; - if (seenCount > 0) { - seen[key]++; - return `${key}${ITERATOR_KEY_GUID}${seenCount}`; - } else { - seen[key] = 1; - } + if (position >= length) { return null; } - return key; -} + let value = this.valueFor(position); + let memo = this.memoFor(position); + let key = keyFor(value, memo, position); + + this.position++; -interface Iterator { - isEmpty(): boolean; - next(): any; + return { key, value, memo }; + } } -class ArrayIterator implements Iterator { - static from(array: any[], keyFor: KeyFor): Iterator { +class ArrayIterator extends BoundedIterator { + static from(array: Opaque[], keyFor: KeyFor): OpaqueIterator { let { length } = array; - if (length > 0) { - return new this(array, length, keyFor); - } else { + if (length === 0) { return EMPTY_ITERATOR; + } else { + return new this(array, length, keyFor); } } - public position = 0; - public seen: any = Object.create(null); + static fromForEachable(object: ForEachable, keyFor: KeyFor): OpaqueIterator { + let array: Opaque[] = []; + object.forEach(item => array.push(item)); + return this.from(array, keyFor); + } - constructor(public array: any[], public length: number, public keyFor: KeyFor) { + constructor(private array: Opaque[], length: number, keyFor: KeyFor) { + super(length, keyFor); } - isEmpty() { - return false; + valueFor(position: number): Opaque { + return this.array[position]; } +} - getMemo(position: number) { - return position; +class EmberArrayIterator extends BoundedIterator { + static from(array: Opaque, keyFor: KeyFor): OpaqueIterator { + let length = get(array, 'length'); + + if (length === 0) { + return EMPTY_ITERATOR; + } else { + return new this(array, length, keyFor); + } } - getValue(position: number) { - return this.array[position]; + constructor(private array: Opaque, length: number, keyFor: KeyFor) { + super(length, keyFor); } - next() { - let { length, keyFor, position, seen } = this; + valueFor(position: number): Opaque { + return objectAt(this.array, position); + } +} - if (position >= length) { return null; } +class ObjectIterator extends BoundedIterator { + static fromIndexable(obj: Indexable, keyFor: KeyFor): OpaqueIterator { + let keys = Object.keys(obj); + let values: Opaque[] = []; - let value = this.getValue(position); - let memo = this.getMemo(position); - let key = ensureUniqueKey(seen, keyFor(value, memo)); + let { length } = keys; - this.position++; + for (let i=0; i 0) { - return new this(array, length, keyFor); - } else { + let isMapLike = false; + + obj.forEach((value: Opaque, key: Opaque) => { + isMapLike = isMapLike || arguments.length >= 2; + + if (isMapLike) { + keys.push(key); + values.push(value); + } else { + values.push(value); + } + + length++; + }); + + if (length === 0) { return EMPTY_ITERATOR; + } else if (isMapLike) { + return new this(keys, values, length, keyFor); + } else { + return new ArrayIterator(values, length, keyFor); } } - getValue(position: number) { - return objectAt(this.array, position); + constructor(private keys: Opaque[], private values: Opaque[], length: number, keyFor: KeyFor) { + super(length, keyFor); + } + + valueFor(position: number): Opaque { + return this.values[position]; + } + + memoFor(position: number): Opaque { + return this.keys[position]; } } -class ObjectKeysIterator extends ArrayIterator { - static from(obj: object, keyFor: KeyFor): Iterator { - let keys = Object.keys(obj); - let { length } = keys; +interface NativeIteratorConstructor { + new(iterable: Iterator, result: IteratorResult, keyFor: KeyFor): NativeIterator; +} - if (length > 0) { - return new this(keys, keys.map((key) => obj[key]), length, keyFor); - } else{ +abstract class NativeIterator implements OpaqueIterator { + static from(this: NativeIteratorConstructor, iterable: Iterable, keyFor: KeyFor): OpaqueIterator { + let iterator = iterable[Symbol.iterator](); + let result = iterator.next(); + let { value, done } = result; + + if (done) { return EMPTY_ITERATOR; + } else if (Array.isArray(value) && value.length === 2) { + return new this(iterator, result, keyFor); + } else { + return new ArrayLikeNativeIterator(iterator, result, keyFor); } } - constructor(public keys: any[], values: any[], length: number, keyFor: KeyFor) { - super(values, length, keyFor); + private position = 0; + + constructor(private iterable: Iterator, private result: IteratorResult, private keyFor: KeyFor) {} + + isEmpty(): false { + return false; } - getMemo(position: number) { - return this.keys[position]; + abstract valueFor(result: IteratorResult, position: number): Opaque; + abstract memoFor(result: IteratorResult, position: number): Opaque; + + next(): Option { + let { iterable, result, position, keyFor } = this; + + if (result.done) { return null; } + + let value = this.valueFor(result, position); + let memo = this.memoFor(result, position); + let key = keyFor(value, memo, position); + + this.position++; + this.result = iterable.next(); + + return { key, value, memo }; } } -class EmptyIterator implements Iterator { - isEmpty() { - return true; +class ArrayLikeNativeIterator extends NativeIterator { + valueFor(result: IteratorResult): Opaque { + return result.value; } - next(): IterationItem { - throw new Error('Cannot call next() on an empty iterator'); + memoFor(_result: IteratorResult, position: number): Opaque { + return position; } } -const EMPTY_ITERATOR = new EmptyIterator(); +class MapLikeNativeIterator extends NativeIterator<[Opaque, Opaque]> { + valueFor(result: IteratorResult<[Opaque, Opaque]>): Opaque { + return result.value[1]; + } -class EachInIterable { - public ref: any; - public keyFor: ((iterable: any) => any) | ((item: any, i: any) => string); - public valueTag: TagWrapper; - public tag: any; + memoFor(result: IteratorResult<[Opaque, Opaque]>): Opaque { + return result.value[0]; + } +} - constructor(ref: any, keyFor: ((iterable: any) => any) | ((item: any, i: any) => string)) { - this.ref = ref; - this.keyFor = keyFor; +const EMPTY_ITERATOR: OpaqueIterator = { + isEmpty(): true { + return true; + }, - let valueTag = this.valueTag = UpdatableTag.create(CONSTANT_TAG); + next(): null { + assert('Cannot call next() on an empty iterator'); + return null; + } +}; + +class EachInIterable implements EmberIterable { + public tag: Tag; + private valueTag = UpdatableTag.create(CONSTANT_TAG); - this.tag = combine([ref.tag, valueTag]); + constructor(private ref: VersionedReference, private keyPath: string) { + this.tag = combine([ref.tag, this.valueTag]); } - iterate() { - let { ref, keyFor, valueTag } = this; + iterate(): OpaqueIterator { + let { ref, valueTag } = this; let iterable = ref.value(); let tag = tagFor(iterable); @@ -216,52 +265,64 @@ class EachInIterable { valueTag.inner.update(tag); - let typeofIterable = typeof iterable; + if (!isIndexable(iterable)) { + return EMPTY_ITERATOR; + } - if (iterable !== null && (typeofIterable === 'object' || typeofIterable === 'function')) { - return ObjectKeysIterator.from(iterable, keyFor); + if (Array.isArray(iterable) || isEmberArray(iterable)) { + return ObjectIterator.fromIndexable(iterable, this.keyFor(true)); + } else if (HAS_NATIVE_SYMBOL && isNativeIterable<[Opaque, Opaque]>(iterable)) { + return MapLikeNativeIterator.from(iterable, this.keyFor()); + } else if (hasForEach(iterable)) { + return ObjectIterator.fromForEachable(iterable, this.keyFor()); } else { - return EMPTY_ITERATOR; + return ObjectIterator.fromIndexable(iterable, this.keyFor(true)); } } - // {{each-in}} yields |key value| instead of |value key|, so the memo and - // value are flipped + valueReferenceFor(item: OpaqueIterationItem): UpdatableReference { + return new UpdatableReference(item.value); + } - valueReferenceFor(item: any): UpdatablePrimitiveReference { - return new UpdatablePrimitiveReference(item.memo); + updateValueReference(ref: UpdatableReference, item: OpaqueIterationItem): void { + ref.update(item.value); } - updateValueReference(reference: UpdatableReference, item: any) { - reference.update(item.memo); + memoReferenceFor(item: OpaqueIterationItem): UpdatableReference { + return new UpdatableReference(item.memo); } - memoReferenceFor(item: any): UpdatableReference { - return new UpdatableReference(item.value); + updateMemoReference(ref: UpdatableReference, item: OpaqueIterationItem): void { + ref.update(item.memo); } - updateMemoReference(reference: UpdatableReference, item: any) { - reference.update(item.value); + private keyFor(hasUniqueKeys = false): KeyFor { + let { keyPath } = this; + + switch (keyPath) { + case '@key': + return hasUniqueKeys ? ObjectKey : Unique(MapKey); + case '@index': + return Index; + case '@identity': + return Unique(Identity); + default: + assert(`Invalid key: ${keyPath}`, keyPath[0] !== '@'); + return Unique(KeyPath(keyPath)); + } } } -class ArrayIterable { - public ref: UpdatableReference; - public keyFor: KeyFor; - public valueTag: TagWrapper; - public tag: any; - - constructor(ref: UpdatableReference, keyFor: KeyFor) { - this.ref = ref; - this.keyFor = keyFor; +class EachIterable implements EmberIterable { + public tag: Tag; + private valueTag = UpdatableTag.create(CONSTANT_TAG); - let valueTag = this.valueTag = UpdatableTag.create(CONSTANT_TAG); - - this.tag = combine([ref.tag, valueTag]); + constructor(private ref: VersionedReference, private keyPath: string) { + this.tag = combine([ref.tag, this.valueTag]); } - iterate(): Iterator { - let { ref, keyFor, valueTag } = this; + iterate(): OpaqueIterator { + let { ref, valueTag } = this; let iterable = ref.value(); @@ -271,32 +332,117 @@ class ArrayIterable { return EMPTY_ITERATOR; } + let keyFor = this.keyFor(); + if (Array.isArray(iterable)) { return ArrayIterator.from(iterable, keyFor); } else if (isEmberArray(iterable)) { return EmberArrayIterator.from(iterable, keyFor); - } else if (typeof iterable.forEach === 'function') { - let array: any[] = []; - iterable.forEach((item: any) => array.push(item)); - return ArrayIterator.from(array, keyFor); + } else if (HAS_NATIVE_SYMBOL && isNativeIterable(iterable)) { + return ArrayLikeNativeIterator.from(iterable, keyFor); + } else if (hasForEach(iterable)) { + return ArrayIterator.fromForEachable(iterable, keyFor); } else { return EMPTY_ITERATOR; } } - valueReferenceFor(item: any): UpdatableReference { + valueReferenceFor(item: OpaqueIterationItem): UpdatableReference { return new UpdatableReference(item.value); } - updateValueReference(reference: UpdatableReference, item: any) { - reference.update(item.value); + updateValueReference(ref: UpdatableReference, item: OpaqueIterationItem): void { + ref.update(item.value); } - memoReferenceFor(item: any): UpdatablePrimitiveReference { - return new UpdatablePrimitiveReference(item.memo); + memoReferenceFor(item: OpaqueIterationItem): UpdatableReference { + return new UpdatableReference(item.memo as number); } - updateMemoReference(reference: UpdatablePrimitiveReference, item: any) { - reference.update(item.memo); + updateMemoReference(ref: UpdatableReference, item: OpaqueIterationItem): void { + ref.update(item.memo); } + + private keyFor(): KeyFor { + let { keyPath } = this; + + switch (keyPath) { + case '@index': + return Index; + case '@identity': + return Unique(Identity); + default: + assert(`Invalid key: ${keyPath}`, keyPath[0] !== '@'); + return Unique(KeyPath(keyPath)); + } + } +} + +interface ForEachable { + forEach(callback: (item: Opaque, key: Opaque) => void): void; +} + +function hasForEach(value: object): value is ForEachable { + return typeof value['forEach'] === 'function'; +} + +function isNativeIterable(value: object): value is Iterable { + return typeof value[Symbol.iterator] === 'function'; +} + +interface Indexable { + readonly [key: string]: Opaque; +} + +function isIndexable(value: Opaque): value is Indexable { + return value !== null && (typeof value === 'object' || typeof value === 'function'); +} + +type KeyFor = (value: Opaque, memo: Opaque, position: number) => string; + +// Position in an array is guarenteed to be unique +function Index(_value: Opaque, _memo: Opaque, position: number): string { + return String(position); +} + +// Object.keys(...) is guarenteed to be strings and unique +function ObjectKey(_value: Opaque, memo: Opaque): string { + return memo as string; +} + +// Map keys can be any objects +function MapKey(_value: Opaque, memo: Opaque): string { + return Identity(memo); +} + +function Identity(value: Opaque): string { + switch (typeof value) { + case 'string': + return value as string; + case 'number': + return String(value); + default: + return guidFor(value); + } +} + +function KeyPath(keyPath: string): KeyFor { + return (value: Opaque) => String(get(value, keyPath)); +} + +function Unique(func: KeyFor): KeyFor { + let seen = new Set(); + + return (value: Opaque, memo: Opaque, position: number) => { + let key = func(value, memo, position); + let count = seen[key]; + + if (count === undefined) { + seen[key] = 0; + return key; + } else { + seen[key] = ++count; + return `${key}${ITERATOR_KEY_GUID}${count}`; + } + }; } diff --git a/packages/ember-glimmer/lib/utils/references.ts b/packages/ember-glimmer/lib/utils/references.ts index f01ee5b35bb..697dc8447f1 100644 --- a/packages/ember-glimmer/lib/utils/references.ts +++ b/packages/ember-glimmer/lib/utils/references.ts @@ -5,17 +5,20 @@ import { ConstReference, DirtyableTag, isConst, + Revision, RevisionTag, Tag, TagWrapper, UpdatableTag, VersionedPathReference, + VersionedReference, } from '@glimmer/reference'; import { CapturedArguments, ConditionalReference as GlimmerConditionalReference, PrimitiveReference, } from '@glimmer/runtime'; +import { Option } from '@glimmer/util'; import { DEBUG } from 'ember-env-flags'; import { didRender, @@ -60,12 +63,8 @@ if (DEBUG) { }; } -// @abstract -// @implements PathReference abstract class EmberPathReference implements VersionedPathReference { - // @abstract get tag() - // @abstract value() - public tag: Tag; + abstract tag: Tag; get(key: string): any { return PropertyReference.create(this, key); @@ -74,11 +73,10 @@ abstract class EmberPathReference implements VersionedPathReference { abstract value(): Opaque; } -// @abstract -export class CachedReference extends EmberPathReference { - private _lastRevision: any; - private _lastValue: any; - public tag: Tag; +export abstract class CachedReference extends EmberPathReference { + abstract tag: Tag; + private _lastRevision: Option; + private _lastValue: Opaque; constructor() { super(); @@ -86,7 +84,7 @@ export class CachedReference extends EmberPathReference { this._lastValue = null; } - compute() { /* NOOP */ } + abstract compute(): Opaque; value() { let { tag, _lastRevision, _lastValue } = this; @@ -98,11 +96,8 @@ export class CachedReference extends EmberPathReference { return _lastValue; } - - // @abstract compute() } -// @implements PathReference export class RootReference extends ConstReference { public children: any; @@ -172,7 +167,9 @@ if (EMBER_GLIMMER_DETECT_BACKTRACKING_RERENDER) { }; } -export class PropertyReference extends CachedReference { +export abstract class PropertyReference extends CachedReference { + abstract tag: Tag; + static create(parentReference: VersionedPathReference, propertyKey: string) { if (isConst(parentReference)) { return new RootPropertyReference(parentReference.value(), propertyKey); @@ -187,6 +184,7 @@ export class PropertyReference extends CachedReference { } export class RootPropertyReference extends PropertyReference implements VersionedPathReference { + public tag: Tag; private _parentValue: any; private _propertyKey: string; @@ -223,6 +221,7 @@ export class RootPropertyReference extends PropertyReference implements Versione } export class NestedPropertyReference extends PropertyReference { + public tag: Tag; private _parentReference: any; private _parentObjectTag: TagWrapper; private _propertyKey: string; @@ -304,17 +303,14 @@ export class UpdatableReference extends EmberPathReference { } } -export class UpdatablePrimitiveReference extends UpdatableReference { -} - -export class ConditionalReference extends GlimmerConditionalReference { +export class ConditionalReference extends GlimmerConditionalReference implements VersionedReference { public objectTag: TagWrapper; - static create(reference: UpdatableReference) { + static create(reference: VersionedReference): VersionedReference { if (isConst(reference)) { let value = reference.value(); if (isProxy(value)) { - return new RootPropertyReference(value, 'isTruthy'); + return new RootPropertyReference(value, 'isTruthy') as VersionedReference; } else { return PrimitiveReference.create(emberToBool(value)); } @@ -323,14 +319,13 @@ export class ConditionalReference extends GlimmerConditionalReference { return new ConditionalReference(reference); } - constructor(reference: UpdatableReference) { + constructor(reference: VersionedReference) { super(reference); - this.objectTag = UpdatableTag.create(CONSTANT_TAG); this.tag = combine([reference.tag, this.objectTag]); } - toBool(predicate: any) { + toBool(predicate: Opaque) { if (isProxy(predicate)) { this.objectTag.inner.update(tagForProperty(predicate, 'isTruthy')); return get(predicate, 'isTruthy'); @@ -342,6 +337,7 @@ export class ConditionalReference extends GlimmerConditionalReference { } export class SimpleHelperReference extends CachedReference { + public tag: Tag; public helper: HelperFunction; public args: CapturedArguments; @@ -388,6 +384,7 @@ export class SimpleHelperReference extends CachedReference { } export class ClassBasedHelperReference extends CachedReference { + public tag: Tag; public instance: HelperInstance; public args: CapturedArguments; @@ -419,6 +416,7 @@ export class ClassBasedHelperReference extends CachedReference { } export class InternalHelperReference extends CachedReference { + public tag: Tag; public helper: (args: CapturedArguments) => CapturedArguments; public args: any; @@ -436,7 +434,6 @@ export class InternalHelperReference extends CachedReference { } } -// @implements PathReference export class UnboundReference extends ConstReference { static create(value: T): VersionedPathReference { return valueToRef(value, false); @@ -450,7 +447,7 @@ export class UnboundReference extends ConstReference { export class ReadonlyReference extends CachedReference { constructor(private inner: VersionedPathReference) { super(); - } +} get tag() { return this.inner.tag; @@ -460,7 +457,7 @@ export class ReadonlyReference extends CachedReference { return this.inner[INVOKE]; } - value() { + compute() { return this.inner.value(); } @@ -479,7 +476,7 @@ export function referenceFromParts(root: VersionedPathReference, parts: return reference; } -export function valueToRef(value: any | null | undefined, bound = true): VersionedPathReference { +export function valueToRef(value: T, bound = true): VersionedPathReference { if (value !== null && typeof value === 'object') { // root of interop with ember objects return bound ? new RootReference(value) : new UnboundReference(value); diff --git a/packages/ember-glimmer/tests/integration/syntax/each-test.js b/packages/ember-glimmer/tests/integration/syntax/each-test.js index ccc28ddc215..42b1c2857e0 100644 --- a/packages/ember-glimmer/tests/integration/syntax/each-test.js +++ b/packages/ember-glimmer/tests/integration/syntax/each-test.js @@ -3,6 +3,7 @@ import { applyMixins, strip } from '../../utils/abstract-test-case'; import { moduleFor, RenderingTest } from '../../utils/test-case'; import { A as emberA, ArrayProxy, RSVP } from 'ember-runtime'; import { Component } from '../../utils/helpers'; +import { HAS_NATIVE_SYMBOL } from 'ember-utils'; import { TogglingSyntaxConditionalsTest, @@ -16,16 +17,12 @@ class ArrayLike { this._array = content; } - get length() { - return this._array.length; - } + // The following methods are APIs used by the tests - forEach(callback) { - this._array.forEach(callback); + toArray() { + return this._array.slice(); } - // The following methods are APIs used by the tests - objectAt(idx) { return this._array[idx]; } @@ -86,7 +83,26 @@ class ArrayLike { notifyPropertyChange(this, '[]'); notifyPropertyChange(this, 'length'); } +} +class ForEachable extends ArrayLike { + get length() { + return this._array.length; + } + + forEach(callback) { + this._array.forEach(callback); + } +} + +let ArrayIterable; + +if (HAS_NATIVE_SYMBOL) { + ArrayIterable = class extends ArrayLike { + [Symbol.iterator]() { + return this._array[Symbol.iterator](); + } + }; } class TogglingEachTest extends TogglingSyntaxConditionalsTest { @@ -98,27 +114,36 @@ class TogglingEachTest extends TogglingSyntaxConditionalsTest { class BasicEachTest extends TogglingEachTest {} -applyMixins(BasicEachTest, - - new TruthyGenerator([ - ['hello'], - emberA(['hello']), - new ArrayLike(['hello']), - ArrayProxy.create({ content: ['hello'] }), - ArrayProxy.create({ content: emberA(['hello']) }) - ]), - - new FalsyGenerator([ - null, - undefined, - false, - '', - 0, - [] - ]), +const TRUTHY_CASES = [ + ['hello'], + emberA(['hello']), + new ForEachable(['hello']), + ArrayProxy.create({ content: ['hello'] }), + ArrayProxy.create({ content: emberA(['hello']) }) +]; + +const FALSY_CASES = [ + null, + undefined, + false, + '', + 0, + [], + emberA([]), + new ForEachable([]), + ArrayProxy.create({ content: [] }), + ArrayProxy.create({ content: emberA([]) }) +]; + +if (HAS_NATIVE_SYMBOL) { + TRUTHY_CASES.push(new ArrayIterable(['hello'])); + FALSY_CASES.push(new ArrayIterable([])); +} +applyMixins(BasicEachTest, + new TruthyGenerator(TRUTHY_CASES), + new FalsyGenerator(FALSY_CASES), ArrayTestCases - ); moduleFor('Syntax test: toggling {{#each}}', class extends BasicEachTest { @@ -185,7 +210,7 @@ class AbstractEachTest extends RenderingTest { } forEach(callback) { - return this.delegate.forEach(callback); + return this.delegate.toArray().forEach(callback); } objectAt(idx) { @@ -752,11 +777,21 @@ moduleFor('Syntax test: {{#each}} with arrays', class extends SingleEachTest { moduleFor('Syntax test: {{#each}} with array-like objects', class extends SingleEachTest { makeList(list) { - return this.list = this.delegate = new ArrayLike(list); + return this.list = this.delegate = new ForEachable(list); } }); +if (HAS_NATIVE_SYMBOL) { + moduleFor('Syntax test: {{#each}} with native iterables', class extends SingleEachTest { + + makeList(list) { + return this.list = this.delegate = new ArrayIterable(list); + } + + }); +} + moduleFor('Syntax test: {{#each}} with array proxies, modifying itself', class extends SingleEachTest { makeList(list) { diff --git a/packages/ember-glimmer/tests/unit/utils/iterable-test.js b/packages/ember-glimmer/tests/unit/utils/iterable-test.js deleted file mode 100644 index 1f2e6b6816d..00000000000 --- a/packages/ember-glimmer/tests/unit/utils/iterable-test.js +++ /dev/null @@ -1,112 +0,0 @@ -import { moduleFor, TestCase } from 'ember-glimmer/tests/utils/test-case'; -import { iterableFor, UpdatableReference } from 'ember-glimmer'; -import { A } from 'ember-runtime'; - -const ITERATOR_KEY_GUID = 'be277757-bbbe-4620-9fcb-213ef433cca2'; - -moduleFor('Iterable', class extends TestCase { - ['@test iterates over an array']() { - let iterator = iteratorForArray(['foo', 'bar']); - - this.assert.deepEqual(iterator.next(), { key: 'foo', memo: 0, value: 'foo' }); - this.assert.deepEqual(iterator.next(), { key: 'bar', memo: 1, value: 'bar' }); - } - - ['@test iterates over an `Ember.A`']() { - let iterator = iteratorForArray(A(['foo', 'bar'])); - - this.assert.deepEqual(iterator.next(), { key: 'foo', memo: 0, value: 'foo' }); - this.assert.deepEqual(iterator.next(), { key: 'bar', memo: 1, value: 'bar' }); - } - - ['@test returns `null` when out of items']() { - let iterator = iteratorForArray(['foo']); - - this.assert.deepEqual(iterator.next(), { key: 'foo', memo: 0, value: 'foo' }); - this.assert.deepEqual(iterator.next(), null); - } - - ['@test iterates over an array with indices as keys']() { - let iterator = iteratorForArray(['foo', 'bar'], '@index'); - - this.assert.deepEqual(iterator.next(), { key: '0', memo: 0, value: 'foo' }); - this.assert.deepEqual(iterator.next(), { key: '1', memo: 1, value: 'bar' }); - } - - ['@test iterates over an array with identities as keys']() { - let iterator = iteratorForArray(['foo', 'bar'], '@identity'); - - this.assert.deepEqual(iterator.next(), { key: 'foo', memo: 0, value: 'foo' }); - this.assert.deepEqual(iterator.next(), { key: 'bar', memo: 1, value: 'bar' }); - } - - ['@test iterates over an array with arbitrary properties as keys']() { - let iterator = iteratorForArray([{ k: 'first', v: 'foo' }, { k: 'second', v: 'bar' }], 'k'); - - this.assert.deepEqual(iterator.next(), { key: 'first', memo: 0, value: { k: 'first', v: 'foo' } }); - this.assert.deepEqual(iterator.next(), { key: 'second', memo: 1, value: { k: 'second', v: 'bar' } }); - } - - ['@test errors on `#next` with an undefined ref']() { - let iterator = iteratorForArray(undefined); - - this.assert.expect(1); - - try { - iterator.next(); - } catch({ message }) { - this.assert.equal(message, 'Cannot call next() on an empty iterator'); - } - } - - ['@test errors on `#next` with a null ref']() { - let iterator = iteratorForArray(null); - - this.assert.expect(1); - - try { - iterator.next(); - } catch({ message }) { - this.assert.equal(message, 'Cannot call next() on an empty iterator'); - } - } - - ['@test errors on `#next` with an invalid ref type']() { - let iterator = iteratorForArray('string'); - - this.assert.expect(1); - - try { - iterator.next(); - } catch({ message }) { - this.assert.equal(message, 'Cannot call next() on an empty iterator'); - } - } - - ['@test errors on `#next` with an empty array']() { - let iterator = iteratorForArray([]); - - this.assert.expect(1); - - try { - iterator.next(); - } catch({ message }) { - this.assert.equal(message, 'Cannot call next() on an empty iterator'); - } - } - - ['@test ensures keys are unique']() { - let iterator = iteratorForArray([{ k: 'qux', v: 'foo' }, { k: 'qux', v: 'bar' }, { k: 'qux', v: 'baz' }], 'k'); - - this.assert.deepEqual(iterator.next(), { key: 'qux', memo: 0, value: { k: 'qux', v: 'foo' } }); - this.assert.deepEqual(iterator.next(), { key: `qux${ITERATOR_KEY_GUID}1`, memo: 1, value: { k: 'qux', v: 'bar' } }); - this.assert.deepEqual(iterator.next(), { key: `qux${ITERATOR_KEY_GUID}2`, memo: 2, value: { k: 'qux', v: 'baz' } }); - } -}); - -function iteratorForArray(arr, keyPath) { - let ref = new UpdatableReference(arr); - let iterable = iterableFor(ref, keyPath); - - return iterable.iterate(); -} diff --git a/packages/ember-metal/lib/index.d.ts b/packages/ember-metal/lib/index.d.ts index aeb43792244..3d60c1f751f 100644 --- a/packages/ember-metal/lib/index.d.ts +++ b/packages/ember-metal/lib/index.d.ts @@ -55,3 +55,5 @@ export class Cache { constructor(limit: number, func: (obj: T) => V, key?: (obj: T) => string, store?: any) get(obj: T): V } + +export const WeakSet: WeakSetConstructor; diff --git a/packages/ember-metal/lib/index.js b/packages/ember-metal/lib/index.js index 36a0a9f8e2a..55180fd318f 100644 --- a/packages/ember-metal/lib/index.js +++ b/packages/ember-metal/lib/index.js @@ -137,6 +137,7 @@ export { didRender, assertNotRendered } from './transaction'; +export { default as WeakSet } from './weak_set'; export { isProxy, setProxy diff --git a/packages/ember-metal/lib/properties.js b/packages/ember-metal/lib/properties.js index b4357dc34bf..a336a9193ad 100644 --- a/packages/ember-metal/lib/properties.js +++ b/packages/ember-metal/lib/properties.js @@ -3,7 +3,7 @@ */ import { assert } from 'ember-debug'; -import { HAS_NATIVE_PROXY } from 'ember-utils'; +import { HAS_NATIVE_PROXY, HAS_NATIVE_SYMBOL } from 'ember-utils'; import { descriptorFor, meta as metaFor, peekMeta, DESCRIPTOR, UNDEFINED } from './meta'; import { overrideChains } from './property_events'; import { DESCRIPTOR_TRAP, EMBER_METAL_ES5_GETTERS, MANDATORY_SETTER } from 'ember/features'; @@ -127,8 +127,8 @@ if (EMBER_METAL_ES5_GETTERS) { property === 'valueOf' || property === 'inspect' || property === 'toJSON' || - Symbol && property === Symbol.toPrimitive || - Symbol && property === Symbol.toStringTag + HAS_NATIVE_SYMBOL && property === Symbol.toPrimitive || + HAS_NATIVE_SYMBOL && property === Symbol.toStringTag ) { return () => '[COMPUTED PROPERTY]'; } diff --git a/packages/ember-template-compiler/lib/plugins/transform-each-in-into-each.js b/packages/ember-template-compiler/lib/plugins/transform-each-in-into-each.js index 91426126921..50589934398 100644 --- a/packages/ember-template-compiler/lib/plugins/transform-each-in-into-each.js +++ b/packages/ember-template-compiler/lib/plugins/transform-each-in-into-each.js @@ -12,7 +12,7 @@ with ```handlebars - {{#each (-each-in iterableThing) as |key value|}} + {{#each (-each-in iterableThing) as |value key|}} ``` @private @@ -28,6 +28,23 @@ export default function transformEachInIntoEach(env) { BlockStatement(node) { if (node.path.original === 'each-in') { node.params[0] = b.sexpr(b.path('-each-in'), [node.params[0]]); + + let blockParams = node.program.blockParams; + + if (!blockParams || blockParams.length === 0) { + // who uses {{#each-in}} without block params?! + } else if (blockParams.length === 1) { + // insert a dummy variable for the first slot + // pick a name that won't parse so it won't shadow any real variables + blockParams = ['( unused value )', blockParams[0]]; + } else { + let key = blockParams.shift(); + let value = blockParams.shift(); + blockParams = [value, key, ...blockParams]; + } + + node.program.blockParams = blockParams; + return b.block(b.path('each'), node.params, node.hash, node.program, node.inverse, node.loc); } } diff --git a/packages/ember-utils/lib/index.d.ts b/packages/ember-utils/lib/index.d.ts index a1bbf890848..86be2c13d54 100644 --- a/packages/ember-utils/lib/index.d.ts +++ b/packages/ember-utils/lib/index.d.ts @@ -31,3 +31,6 @@ export const OWNER: string; export function generateGuid(obj: Opaque, prefix?: string): string; export function guidFor(obj: Opaque): string; export function uuid(): number; + +export const HAS_NATIVE_SYMBOL: boolean; +export const HAS_NATIVE_PROXY: boolean; diff --git a/packages/ember-utils/lib/index.js b/packages/ember-utils/lib/index.js index d5da88c3b6a..264ca8db4e4 100644 --- a/packages/ember-utils/lib/index.js +++ b/packages/ember-utils/lib/index.js @@ -29,4 +29,5 @@ export { canInvoke, tryInvoke } from './invoke'; export { default as makeArray } from './make-array'; export { default as NAME_KEY } from './name'; export { default as toString } from './to-string'; +export { HAS_NATIVE_SYMBOL } from './symbol-utils'; export { HAS_NATIVE_PROXY } from './proxy-utils'; diff --git a/packages/ember-utils/lib/symbol-utils.js b/packages/ember-utils/lib/symbol-utils.js new file mode 100644 index 00000000000..2ef59bca6a5 --- /dev/null +++ b/packages/ember-utils/lib/symbol-utils.js @@ -0,0 +1,9 @@ +export const HAS_NATIVE_SYMBOL = (function() { + if (typeof Symbol !== 'function') { + return false; + } + + // use `Object`'s `.toString` directly to prevent us from detecting + // polyfills as native + return Object.prototype.toString.call(Symbol()) === '[object Symbol]'; +})(); diff --git a/packages/ember-utils/tests/inspect_test.js b/packages/ember-utils/tests/inspect_test.js index 6f2b4390a25..628399a7217 100644 --- a/packages/ember-utils/tests/inspect_test.js +++ b/packages/ember-utils/tests/inspect_test.js @@ -1,13 +1,9 @@ -import { inspect } from '..'; +import { HAS_NATIVE_SYMBOL, inspect } from '..'; import { moduleFor, AbstractTestCase as TestCase } from 'internal-test-helpers'; -// Symbol is not defined on pre-ES2015 runtimes, so this let's us safely test -// for it's existence (where a simple `if (Symbol)` would ReferenceError) -const HAS_NATIVE_SYMBOL = typeof Symbol === 'function'; - moduleFor('Ember.inspect', class extends TestCase { ['@test strings'](assert) { assert.equal(inspect('foo'), 'foo');