diff --git a/packages/@ember/-internals/glimmer/lib/component-managers/mount.ts b/packages/@ember/-internals/glimmer/lib/component-managers/mount.ts index 35a35432b07..cd294e3148e 100644 --- a/packages/@ember/-internals/glimmer/lib/component-managers/mount.ts +++ b/packages/@ember/-internals/glimmer/lib/component-managers/mount.ts @@ -1,12 +1,15 @@ import { DEBUG } from '@glimmer/env'; import { ComponentCapabilities } from '@glimmer/interfaces'; -import { CONSTANT_TAG, Tag, validate, value, VersionedPathReference } from '@glimmer/reference'; -import { ComponentDefinition, Invocation, WithDynamicLayout } from '@glimmer/runtime'; +import { CONSTANT_TAG, Tag, VersionedPathReference } from '@glimmer/reference'; +import { Arguments, ComponentDefinition, Invocation, WithDynamicLayout } from '@glimmer/runtime'; import { Destroyable, Opaque, Option } from '@glimmer/util'; import { Owner } from '@ember/-internals/owner'; import { generateControllerFactory } from '@ember/-internals/routing'; import { OwnedTemplateMeta } from '@ember/-internals/views'; +import { EMBER_ROUTING_MODEL_ARG } from '@ember/canary-features'; +import { assert } from '@ember/debug'; + import { TemplateFactory } from '../..'; import Environment from '../environment'; import RuntimeResolver from '../resolver'; @@ -23,24 +26,18 @@ interface EngineState { engine: EngineInstance; controller: any; self: RootReference; - tag: Tag; -} - -interface EngineWithModelState extends EngineState { - modelRef: VersionedPathReference; - modelRev: number; + modelRef?: VersionedPathReference; } interface EngineDefinitionState { name: string; - modelRef: VersionedPathReference | undefined; } const CAPABILITIES = { dynamicLayout: true, dynamicTag: false, prepareArgs: false, - createArgs: false, + createArgs: true, attributeHook: false, elementHook: false, createCaller: true, @@ -49,10 +46,13 @@ const CAPABILITIES = { createInstance: true, }; -class MountManager - extends AbstractManager - implements - WithDynamicLayout { +// TODO +// This "disables" the "@model" feature by making the arg untypable syntatically +// Delete this when EMBER_ROUTING_MODEL_ARG has shipped +export const MODEL_ARG_NAME = EMBER_ROUTING_MODEL_ARG || !DEBUG ? 'model' : ' untypable model arg '; + +class MountManager extends AbstractManager + implements WithDynamicLayout { getDynamicLayout(state: EngineState, _: RuntimeResolver): Invocation { let templateFactory = state.engine.lookup('template:application') as TemplateFactory; let template = templateFactory(state.engine); @@ -68,9 +68,9 @@ class MountManager return CAPABILITIES; } - create(environment: Environment, state: EngineDefinitionState) { + create(environment: Environment, { name }: EngineDefinitionState, args: Arguments) { if (DEBUG) { - this._pushEngineToDebugStack(`engine:${state.name}`, environment); + this._pushEngineToDebugStack(`engine:${name}`, environment); } // TODO @@ -78,7 +78,7 @@ class MountManager // we should resolve the engine app template in the helper // it also should use the owner that looked up the mount helper. - let engine = environment.owner.buildChildEngineInstance(state.name); + let engine = environment.owner.buildChildEngineInstance(name); engine.boot(); @@ -86,21 +86,22 @@ class MountManager let controllerFactory = applicationFactory || generateControllerFactory(engine, 'application'); let controller: any; let self: RootReference; - let bucket: EngineState | EngineWithModelState; - let tag: Tag; - let modelRef = state.modelRef; + let bucket: EngineState; + let modelRef; + + if (args.named.has(MODEL_ARG_NAME)) { + modelRef = args.named.get(MODEL_ARG_NAME); + } + if (modelRef === undefined) { controller = controllerFactory.create(); self = new RootReference(controller); - tag = CONSTANT_TAG; - bucket = { engine, controller, self, tag }; + bucket = { engine, controller, self }; } else { let model = modelRef.value(); - let modelRev = value(modelRef.tag); controller = controllerFactory.create({ model }); self = new RootReference(controller); - tag = modelRef.tag; - bucket = { engine, controller, self, tag, modelRef, modelRev }; + bucket = { engine, controller, self, modelRef }; } return bucket; @@ -110,8 +111,12 @@ class MountManager return self; } - getTag(state: EngineState | EngineWithModelState): Tag { - return state.tag; + getTag(state: EngineState): Tag { + if (state.modelRef) { + return state.modelRef.tag; + } else { + return CONSTANT_TAG; + } } getDestructor({ engine }: EngineState): Option { @@ -124,13 +129,9 @@ class MountManager } } - update(bucket: EngineWithModelState): void { - let { controller, modelRef, modelRev } = bucket; - if (!validate(modelRef.tag, modelRev!)) { - let model = modelRef.value(); - bucket.modelRev = value(modelRef.tag); - controller.set('model', model); - } + update({ controller, modelRef }: EngineState): void { + assert('[BUG] `update` should only be called when modelRef is present', modelRef !== undefined); + controller.set('model', modelRef!.value()); } } @@ -140,7 +141,7 @@ export class MountDefinition implements ComponentDefinition { public state: EngineDefinitionState; public manager = MOUNT_MANAGER; - constructor(name: string, modelRef: VersionedPathReference | undefined) { - this.state = { name, modelRef }; + constructor(name: string) { + this.state = { name }; } } diff --git a/packages/@ember/-internals/glimmer/lib/component-managers/outlet.ts b/packages/@ember/-internals/glimmer/lib/component-managers/outlet.ts index 34ca5d2ff92..b9ed4d57ef6 100644 --- a/packages/@ember/-internals/glimmer/lib/component-managers/outlet.ts +++ b/packages/@ember/-internals/glimmer/lib/component-managers/outlet.ts @@ -12,7 +12,6 @@ import { ElementOperations, Environment, Invocation, - UNDEFINED_REFERENCE, WithDynamicTagName, WithStaticLayout, } from '@glimmer/runtime'; @@ -39,7 +38,8 @@ export interface OutletDefinitionState { name: string; outlet: string; template: OwnedTemplate; - controller: any | undefined; + controller: unknown; + model: unknown; } const CAPABILITIES: ComponentCapabilities = { @@ -74,10 +74,8 @@ class OutletComponentManager extends AbstractManager { let env = vm.env as Environment; let nameRef = args.positional.at(0); - let modelRef = args.named.has('model') ? args.named.get('model') : undefined; - return new DynamicEngineReference(nameRef, env, modelRef); + let captured: Option = null; + + // TODO: the functionailty to create a proper CapturedArgument should be + // exported by glimmer, or that it should provide an overload for `curry` + // that takes `PreparedArguments` + if (args.named.has('model')) { + assert('[BUG] this should already be checked by the macro', args.named.length === 1); + + let named = args.named.capture(); + let { tag } = named; + + // TODO delete me after EMBER_ROUTING_MODEL_ARG has shipped + if (DEBUG && MODEL_ARG_NAME !== 'model') { + assert('[BUG] named._map is not null', named['_map'] === null); + named.names = [MODEL_ARG_NAME]; + } + + captured = { + tag, + positional: EMPTY_ARGS.positional, + named, + length: 1, + value() { + return { + named: this.named.value(), + positional: this.positional.value(), + }; + }, + }; + } + + return new DynamicEngineReference(nameRef, env, captured); } /** @@ -78,33 +111,38 @@ export function mountMacro( params!.length === 1 ); + if (DEBUG && hash) { + let keys = hash[0]; + let extra = keys.filter(k => k !== 'model'); + + assert( + 'You can only pass a `model` argument to the {{mount}} helper, ' + + 'e.g. {{mount "profile-engine" model=this.profile}}. ' + + `You passed ${extra.join(',')}.`, + extra.length === 0 + ); + } + let expr: WireFormat.Expressions.Helper = [WireFormat.Ops.Helper, '-mount', params || [], hash]; builder.dynamicComponent(expr, null, [], null, false, null, null); return true; } -class DynamicEngineReference { +class DynamicEngineReference implements VersionedPathReference> { public tag: Tag; - public nameRef: VersionedPathReference; - public modelRef: VersionedPathReference | undefined; - public env: Environment; - private _lastName: string | null; - private _lastDef: CurriedComponentDefinition | null; + private _lastName: Option = null; + private _lastDef: Option = null; + constructor( - nameRef: VersionedPathReference, - env: Environment, - modelRef: VersionedPathReference | undefined + public nameRef: VersionedPathReference, + public env: Environment, + public args: Option ) { this.tag = nameRef.tag; - this.nameRef = nameRef; - this.modelRef = modelRef; - this.env = env; - this._lastName = null; - this._lastDef = null; } - value() { - let { env, nameRef, modelRef } = this; + value(): Option { + let { env, nameRef, args } = this; let name = nameRef.value(); if (typeof name === 'string') { @@ -122,7 +160,7 @@ class DynamicEngineReference { } this._lastName = name; - this._lastDef = curry(new MountDefinition(name, modelRef)); + this._lastDef = curry(new MountDefinition(name), args); return this._lastDef; } else { diff --git a/packages/@ember/-internals/glimmer/lib/syntax/outlet.ts b/packages/@ember/-internals/glimmer/lib/syntax/outlet.ts index 1356c70c71f..09be6a326bf 100644 --- a/packages/@ember/-internals/glimmer/lib/syntax/outlet.ts +++ b/packages/@ember/-internals/glimmer/lib/syntax/outlet.ts @@ -1,19 +1,25 @@ import { OwnedTemplateMeta } from '@ember/-internals/views'; -import { Option } from '@glimmer/interfaces'; +import { EMBER_ROUTING_MODEL_ARG } from '@ember/canary-features'; +import { DEBUG } from '@glimmer/env'; +import { Option, unsafe } from '@glimmer/interfaces'; import { OpcodeBuilder } from '@glimmer/opcode-compiler'; import { ConstReference, Reference, Tag, VersionedPathReference } from '@glimmer/reference'; import { Arguments, + CapturedArguments, CurriedComponentDefinition, curry, + EMPTY_ARGS, UNDEFINED_REFERENCE, VM, } from '@glimmer/runtime'; +import { Dict, dict, Opaque } from '@glimmer/util'; import * as WireFormat from '@glimmer/wire-format'; import { OutletComponentDefinition, OutletDefinitionState } from '../component-managers/outlet'; import { DynamicScope } from '../renderer'; import { isTemplateFactory } from '../template'; import { OutletReference, OutletState } from '../utils/outlet'; +import { NestedPropertyReference, PropertyReference } from '../utils/references'; /** The `{{outlet}}` helper lets you specify where a child route will render in @@ -85,17 +91,93 @@ export function outletMacro( return true; } +class OutletModelReference implements VersionedPathReference { + public tag: Tag; + + constructor(private parent: VersionedPathReference) { + this.tag = parent.tag; + } + + value(): Opaque { + let state = this.parent.value(); + + if (state === undefined) { + return undefined; + } + + let { render } = state; + + if (render === undefined) { + return undefined; + } + + return render.model as Opaque; + } + + get(property: string): VersionedPathReference { + if (DEBUG) { + // This guarentees that we preserve the `debug()` output below + return new NestedPropertyReference(this, property); + } else { + return PropertyReference.create(this, property); + } + } +} + +if (DEBUG) { + OutletModelReference.prototype['debug'] = function debug(): string { + return '@model'; + }; +} + class OutletComponentReference implements VersionedPathReference { public tag: Tag; - private definition: CurriedComponentDefinition | null; - private lastState: OutletDefinitionState | null; + private args: Option = null; + private definition: Option = null; + private lastState: Option = null; constructor(private outletRef: VersionedPathReference) { - this.definition = null; - this.lastState = null; // The router always dirties the root state. - this.tag = outletRef.tag; + let tag = (this.tag = outletRef.tag); + + if (EMBER_ROUTING_MODEL_ARG) { + let modelRef = new OutletModelReference(outletRef); + let map = dict(); + map.model = modelRef; + + // TODO: the functionailty to create a proper CapturedArgument should be + // exported by glimmer, or that it should provide an overload for `curry` + // that takes `PreparedArguments` + this.args = { + tag, + positional: EMPTY_ARGS.positional, + named: { + tag, + map, + names: ['model'], + references: [modelRef], + length: 1, + has(key: string): boolean { + return key === 'model'; + }, + get(key: string): T { + return (key === 'model' ? modelRef : UNDEFINED_REFERENCE) as unsafe; + }, + value(): Dict { + let model = modelRef.value(); + return { model }; + }, + }, + length: 1, + value() { + return { + named: this.named.value(), + positional: this.positional.value(), + }; + }, + }; + } } value(): CurriedComponentDefinition | null { @@ -106,7 +188,7 @@ class OutletComponentReference this.lastState = state; let definition = null; if (state !== null) { - definition = curry(new OutletComponentDefinition(state)); + definition = curry(new OutletComponentDefinition(state), this.args); } return (this.definition = definition); } @@ -138,6 +220,7 @@ function stateFor( outlet: render.outlet, template, controller: render.controller, + model: render.model, }; } diff --git a/packages/@ember/-internals/glimmer/lib/utils/outlet.ts b/packages/@ember/-internals/glimmer/lib/utils/outlet.ts index 6c41134c855..a415c5cf1cb 100644 --- a/packages/@ember/-internals/glimmer/lib/utils/outlet.ts +++ b/packages/@ember/-internals/glimmer/lib/utils/outlet.ts @@ -36,7 +36,12 @@ export interface RenderState { /** * The controller (the self of the outlet component) */ - controller: any | undefined; + controller: unknown; + + /** + * The model (the resolved value of the model hook) + */ + model: unknown; /** * template (the layout of the outlet component) diff --git a/packages/@ember/-internals/glimmer/lib/utils/references.ts b/packages/@ember/-internals/glimmer/lib/utils/references.ts index 3388de34a8f..87d91364960 100644 --- a/packages/@ember/-internals/glimmer/lib/utils/references.ts +++ b/packages/@ember/-internals/glimmer/lib/utils/references.ts @@ -10,7 +10,7 @@ import { } from '@ember/-internals/metal'; import { isProxy, symbol } from '@ember/-internals/utils'; import { EMBER_METAL_TRACKED_PROPERTIES } from '@ember/canary-features'; -import { debugFreeze } from '@ember/debug'; +import { assert, debugFreeze } from '@ember/debug'; import { DEBUG } from '@glimmer/env'; import { Dict, Opaque } from '@glimmer/interfaces'; import { @@ -200,6 +200,12 @@ export class RootPropertyReference extends PropertyReference } } +if (DEBUG) { + RootPropertyReference.prototype['debug'] = function debug(): string { + return `this.${this['propertyKey']}`; + }; +} + export class NestedPropertyReference extends PropertyReference { public tag: Tag; private propertyTag: UpdatableTag; @@ -272,6 +278,20 @@ export class NestedPropertyReference extends PropertyReference { } } +if (DEBUG) { + NestedPropertyReference.prototype['debug'] = function debug(): string { + let parent = this['parentReference']; + let parentKey = 'unknownObject'; + let selfKey = this['propertyKey']; + + if (typeof parent['debug'] === 'function') { + parentKey = parent['debug'](); + } + + return `${parentKey}.${selfKey}`; + }; +} + export class UpdatableReference extends EmberPathReference { public tag: DirtyableTag; private _value: Opaque; @@ -487,30 +507,38 @@ export function referenceFromParts( type Primitive = undefined | null | boolean | number | string; -function isObject(value: Opaque): value is object { +function isObject(value: unknown): value is object { return value !== null && typeof value === 'object'; } -function isFunction(value: Opaque): value is Function { +function isFunction(value: unknown): value is Function { return typeof value === 'function'; } -function isPrimitive(value: Opaque): value is Primitive { +function isPrimitive(value: unknown): value is Primitive { if (DEBUG) { - let type = typeof value; - return ( + let label; + + try { + label = ` (was \`${String(value)}\`)`; + } catch (e) { + label = null; + } + + assert( + `This is a fall-through check for typing purposes only! \`value\` must already be a primitive at this point.${label})`, value === undefined || - value === null || - type === 'boolean' || - type === 'number' || - type === 'string' + value === null || + typeof value === 'boolean' || + typeof value === 'number' || + typeof value === 'string' ); - } else { - return true; } + + return true; } -function valueToRef(value: T, bound = true): VersionedPathReference { +function valueToRef(value: T, bound = true): VersionedPathReference { if (isObject(value)) { // root of interop with ember objects return bound ? new RootReference(value) : new UnboundReference(value); @@ -539,7 +567,7 @@ function valueToRef(value: T, bound = true): VersionedPathReference< } } -function valueKeyToRef(value: Opaque, key: string): VersionedPathReference { +function valueKeyToRef(value: unknown, key: string): VersionedPathReference { if (isObject(value)) { // root of interop with ember objects return new RootPropertyReference(value, key); diff --git a/packages/@ember/-internals/glimmer/lib/views/outlet.ts b/packages/@ember/-internals/glimmer/lib/views/outlet.ts index 9af41ceaa54..ae4eaf7226b 100644 --- a/packages/@ember/-internals/glimmer/lib/views/outlet.ts +++ b/packages/@ember/-internals/glimmer/lib/views/outlet.ts @@ -57,6 +57,7 @@ export default class OutletView { outlet: TOP_LEVEL_OUTLET, name: TOP_LEVEL_NAME, controller: undefined, + model: undefined, template, }, })); @@ -66,6 +67,7 @@ export default class OutletView { outlet: TOP_LEVEL_OUTLET, template, controller: undefined, + model: undefined, }; } diff --git a/packages/@ember/-internals/glimmer/tests/integration/application/engine-test.js b/packages/@ember/-internals/glimmer/tests/integration/application/engine-test.js index 953060821c8..297e955ca02 100644 --- a/packages/@ember/-internals/glimmer/tests/integration/application/engine-test.js +++ b/packages/@ember/-internals/glimmer/tests/integration/application/engine-test.js @@ -1,10 +1,11 @@ import { moduleFor, ApplicationTestCase, strip, runTaskNext } from 'internal-test-helpers'; -import Controller from '@ember/controller'; -import { RSVP } from '@ember/-internals/runtime'; import { Component } from '@ember/-internals/glimmer'; -import Engine from '@ember/engine'; import { Route } from '@ember/-internals/routing'; +import { RSVP } from '@ember/-internals/runtime'; +import { EMBER_ROUTING_MODEL_ARG } from '@ember/canary-features'; +import Controller from '@ember/controller'; +import Engine from '@ember/engine'; import { next } from '@ember/runloop'; import { compile } from '../../utils/helpers'; @@ -517,7 +518,12 @@ moduleFor( }, }) ); - this.register('template:application_error', compile('Error! {{model.message}}')); + this.register( + 'template:application_error', + compile( + EMBER_ROUTING_MODEL_ARG ? 'Error! {{@model.message}}' : 'Error! {{this.model.message}}' + ) + ); this.register( 'route:post', Route.extend({ @@ -556,7 +562,12 @@ moduleFor( }, }) ); - this.register('template:error', compile('Error! {{model.message}}')); + this.register( + 'template:error', + compile( + EMBER_ROUTING_MODEL_ARG ? 'Error! {{@model.message}}' : 'Error! {{this.model.message}}' + ) + ); this.register( 'route:post', Route.extend({ @@ -595,7 +606,12 @@ moduleFor( }, }) ); - this.register('template:post_error', compile('Error! {{model.message}}')); + this.register( + 'template:post_error', + compile( + EMBER_ROUTING_MODEL_ARG ? 'Error! {{@model.message}}' : 'Error! {{this.model.message}}' + ) + ); this.register( 'route:post', Route.extend({ @@ -634,7 +650,12 @@ moduleFor( }, }) ); - this.register('template:post.error', compile('Error! {{model.message}}')); + this.register( + 'template:post.error', + compile( + EMBER_ROUTING_MODEL_ARG ? 'Error! {{@model.message}}' : 'Error! {{this.model.message}}' + ) + ); this.register( 'route:post.comments', Route.extend({ diff --git a/packages/@ember/-internals/glimmer/tests/integration/application/rendering-test.js b/packages/@ember/-internals/glimmer/tests/integration/application/rendering-test.js index a69480e5b6f..314de33642e 100644 --- a/packages/@ember/-internals/glimmer/tests/integration/application/rendering-test.js +++ b/packages/@ember/-internals/glimmer/tests/integration/application/rendering-test.js @@ -4,6 +4,8 @@ import { ENV } from '@ember/-internals/environment'; import Controller from '@ember/controller'; import { Route } from '@ember/-internals/routing'; import { Component } from '@ember/-internals/glimmer'; +import { set, tracked } from '@ember/-internals/metal'; +import { runTask } from '../../../../../../internal-test-helpers/lib/run'; moduleFor( 'Application test: rendering', @@ -38,7 +40,7 @@ moduleFor( }); } - ['@test it can access the model provided by the route']() { + ['@feature(EMBER_ROUTING_MODEL_ARG) it can access the model provided by the route via @model']() { this.add( 'route:application', Route.extend({ @@ -51,12 +53,102 @@ moduleFor( this.addTemplate( 'application', strip` -
    - {{#each model as |item|}} -
  • {{item}}
  • - {{/each}} -
- ` +
    + {{#each @model as |item|}} +
  • {{item}}
  • + {{/each}} +
+ ` + ); + + return this.visit('/').then(() => { + this.assertInnerHTML(strip` +
    +
  • red
  • +
  • yellow
  • +
  • blue
  • +
+ `); + }); + } + + ['@feature(!EMBER_ROUTING_MODEL_ARG) it cannot access the model provided by the route via @model']() { + this.add( + 'route:application', + Route.extend({ + model() { + return ['red', 'yellow', 'blue']; + }, + }) + ); + + this.addTemplate( + 'application', + strip` +
    + {{#each @model as |item|}} +
  • {{item}}
  • + {{/each}} +
+ ` + ); + + return this.visit('/').then(() => { + this.assertInnerHTML('
'); + }); + } + + ['@test it can access the model provided by the route via this.model']() { + this.add( + 'route:application', + Route.extend({ + model() { + return ['red', 'yellow', 'blue']; + }, + }) + ); + + this.addTemplate( + 'application', + strip` +
    + {{#each this.model as |item|}} +
  • {{item}}
  • + {{/each}} +
+ ` + ); + + return this.visit('/').then(() => { + this.assertInnerHTML(strip` +
    +
  • red
  • +
  • yellow
  • +
  • blue
  • +
+ `); + }); + } + + ['@test it can access the model provided by the route via implicit this fallback']() { + this.add( + 'route:application', + Route.extend({ + model() { + return ['red', 'yellow', 'blue']; + }, + }) + ); + + this.addTemplate( + 'application', + strip` +
    + {{#each model as |item|}} +
  • {{item}}
  • + {{/each}} +
+ ` ); return this.visit('/').then(() => { @@ -70,7 +162,252 @@ moduleFor( }); } - ['@test it can render a nested route']() { + async ['@feature(EMBER_ROUTING_MODEL_ARG) interior mutations on the model with set'](assert) { + this.router.map(function() { + this.route('color', { path: '/:color' }); + }); + + this.add( + 'route:color', + Route.extend({ + model({ color }) { + return { color }; + }, + }) + ); + + this.addTemplate( + 'color', + strip` + [@model: {{@model.color}}] + [this.model: {{this.model.color}}] + [model: {{model.color}}] + ` + ); + + await this.visit('/red'); + + assert.equal(this.currentURL, '/red'); + + this.assertInnerHTML(strip` + [@model: red] + [this.model: red] + [model: red] + `); + + await this.visit('/yellow'); + + assert.equal(this.currentURL, '/yellow'); + + this.assertInnerHTML(strip` + [@model: yellow] + [this.model: yellow] + [model: yellow] + `); + + runTask(() => { + let { model } = this.controllerFor('color'); + set(model, 'color', 'blue'); + }); + + assert.equal(this.currentURL, '/yellow'); + + this.assertInnerHTML(strip` + [@model: blue] + [this.model: blue] + [model: blue] + `); + } + + async ['@feature(EMBER_ROUTING_MODEL_ARG, EMBER_METAL_TRACKED_PROPERTIES) interior mutations on the model with tracked properties']( + assert + ) { + class Model { + @tracked color; + + constructor(color) { + this.color = color; + } + } + + this.router.map(function() { + this.route('color', { path: '/:color' }); + }); + + this.add( + 'route:color', + Route.extend({ + model({ color }) { + return new Model(color); + }, + }) + ); + + this.addTemplate( + 'color', + strip` + [@model: {{@model.color}}] + [this.model: {{this.model.color}}] + [model: {{model.color}}] + ` + ); + + await this.visit('/red'); + + assert.equal(this.currentURL, '/red'); + + this.assertInnerHTML(strip` + [@model: red] + [this.model: red] + [model: red] + `); + + await this.visit('/yellow'); + + assert.equal(this.currentURL, '/yellow'); + + this.assertInnerHTML(strip` + [@model: yellow] + [this.model: yellow] + [model: yellow] + `); + + runTask(() => { + this.controllerFor('color').model.color = 'blue'; + }); + + assert.equal(this.currentURL, '/yellow'); + + this.assertInnerHTML(strip` + [@model: blue] + [this.model: blue] + [model: blue] + `); + } + + async ['@feature(EMBER_ROUTING_MODEL_ARG) exterior mutations on the model with set'](assert) { + this.router.map(function() { + this.route('color', { path: '/:color' }); + }); + + this.add( + 'route:color', + Route.extend({ + model({ color }) { + return color; + }, + }) + ); + + this.addTemplate( + 'color', + strip` + [@model: {{@model}}] + [this.model: {{this.model}}] + [model: {{model}}] + ` + ); + + await this.visit('/red'); + + assert.equal(this.currentURL, '/red'); + + this.assertInnerHTML(strip` + [@model: red] + [this.model: red] + [model: red] + `); + + await this.visit('/yellow'); + + assert.equal(this.currentURL, '/yellow'); + + this.assertInnerHTML(strip` + [@model: yellow] + [this.model: yellow] + [model: yellow] + `); + + runTask(() => { + let controller = this.controllerFor('color'); + set(controller, 'model', 'blue'); + }); + + assert.equal(this.currentURL, '/yellow'); + + this.assertInnerHTML(strip` + [@model: yellow] + [this.model: blue] + [model: blue] + `); + } + + async ['@feature(EMBER_ROUTING_MODEL_ARG, EMBER_METAL_TRACKED_PROPERTIES) exterior mutations on the model with tracked properties']( + assert + ) { + this.router.map(function() { + this.route('color', { path: '/:color' }); + }); + + this.add( + 'route:color', + Route.extend({ + model({ color }) { + return color; + }, + }) + ); + + this.add( + 'controller:color', + class ColorController extends Controller { + @tracked model; + } + ); + + this.addTemplate( + 'color', + strip` + [@model: {{@model}}] + [this.model: {{this.model}}] + [model: {{model}}] + ` + ); + + await this.visit('/red'); + + assert.equal(this.currentURL, '/red'); + + this.assertInnerHTML(strip` + [@model: red] + [this.model: red] + [model: red] + `); + + await this.visit('/yellow'); + + assert.equal(this.currentURL, '/yellow'); + + this.assertInnerHTML(strip` + [@model: yellow] + [this.model: yellow] + [model: yellow] + `); + + runTask(() => { + this.controllerFor('color').model = 'blue'; + }); + + assert.equal(this.currentURL, '/yellow'); + + this.assertInnerHTML(strip` + [@model: yellow] + [this.model: blue] + [model: blue] + `); + } + + ['@feature(!EMBER_ROUTING_MODEL_ARG) it can render a nested route']() { this.router.map(function() { this.route('lists', function() { this.route('colors', function() { @@ -92,26 +429,67 @@ moduleFor( this.addTemplate( 'lists.colors.favorite', strip` -
    - {{#each model as |item|}} -
  • {{item}}
  • - {{/each}} -
- ` +
    + {{#each this.model as |item|}} +
  • {{item}}
  • + {{/each}} +
+ ` ); return this.visit('/lists/colors/favorite').then(() => { this.assertInnerHTML(strip` +
    +
  • red
  • +
  • yellow
  • +
  • blue
  • +
+ `); + }); + } + + ['@feature(EMBER_ROUTING_MODEL_ARG) it can render a nested route']() { + this.router.map(function() { + this.route('lists', function() { + this.route('colors', function() { + this.route('favorite'); + }); + }); + }); + + // The "favorite" route will inherit the model + this.add( + 'route:lists.colors', + Route.extend({ + model() { + return ['red', 'yellow', 'blue']; + }, + }) + ); + + this.addTemplate( + 'lists.colors.favorite', + strip`
    -
  • red
  • -
  • yellow
  • -
  • blue
  • + {{#each @model as |item|}} +
  • {{item}}
  • + {{/each}}
- `); + ` + ); + + return this.visit('/lists/colors/favorite').then(() => { + this.assertInnerHTML(strip` +
    +
  • red
  • +
  • yellow
  • +
  • blue
  • +
+ `); }); } - ['@test it can render into named outlets']() { + ['@feature(!EMBER_ROUTING_MODEL_ARG) it can render into named outlets']() { this.router.map(function() { this.route('colors'); }); @@ -119,16 +497,16 @@ moduleFor( this.addTemplate( 'application', strip` - -
{{outlet}}
- ` + +
{{outlet}}
+ ` ); this.addTemplate( 'nav', strip` - Ember - ` + Ember + ` ); this.add( @@ -156,31 +534,31 @@ moduleFor( this.addTemplate( 'colors', strip` -
    - {{#each model as |item|}} -
  • {{item}}
  • - {{/each}} -
- ` +
    + {{#each this.model as |item|}} +
  • {{item}}
  • + {{/each}} +
+ ` ); return this.visit('/colors').then(() => { this.assertInnerHTML(strip` - -
-
    -
  • red
  • -
  • yellow
  • -
  • blue
  • -
-
- `); + +
+
    +
  • red
  • +
  • yellow
  • +
  • blue
  • +
+
+ `); }); } - ['@test it can render into named outlets']() { + ['@feature(EMBER_ROUTING_MODEL_ARG) it can render into named outlets']() { this.router.map(function() { this.route('colors'); }); @@ -188,16 +566,16 @@ moduleFor( this.addTemplate( 'application', strip` - -
{{outlet}}
- ` + +
{{outlet}}
+ ` ); this.addTemplate( 'nav', strip` - Ember - ` + Ember + ` ); this.add( @@ -225,27 +603,27 @@ moduleFor( this.addTemplate( 'colors', strip` -
    - {{#each model as |item|}} -
  • {{item}}
  • - {{/each}} -
- ` +
    + {{#each @model as |item|}} +
  • {{item}}
  • + {{/each}} +
+ ` ); return this.visit('/colors').then(() => { this.assertInnerHTML(strip` - -
-
    -
  • red
  • -
  • yellow
  • -
  • blue
  • -
-
- `); + +
+
    +
  • red
  • +
  • yellow
  • +
  • blue
  • +
+
+ `); }); } @@ -280,7 +658,7 @@ moduleFor( }); } - ['@test it should produce a stable DOM when the model changes']() { + ['@feature(!EMBER_ROUTING_MODEL_ARG) it should produce a stable DOM when the model changes']() { this.router.map(function() { this.route('color', { path: '/colors/:color' }); }); @@ -294,7 +672,35 @@ moduleFor( }) ); - this.addTemplate('color', 'color: {{model}}'); + this.addTemplate('color', 'color: {{this.model}}'); + + return this.visit('/colors/red') + .then(() => { + this.assertInnerHTML('color: red'); + this.takeSnapshot(); + return this.visit('/colors/green'); + }) + .then(() => { + this.assertInnerHTML('color: green'); + this.assertInvariants(); + }); + } + + ['@feature(EMBER_ROUTING_MODEL_ARG) it should produce a stable DOM when the model changes']() { + this.router.map(function() { + this.route('color', { path: '/colors/:color' }); + }); + + this.add( + 'route:color', + Route.extend({ + model(params) { + return params.color; + }, + }) + ); + + this.addTemplate('color', 'color: {{@model}}'); return this.visit('/colors/red') .then(() => { @@ -339,7 +745,51 @@ moduleFor( .then(() => this.assertText('b')); } - ['@test it should update correctly when the controller changes']() { + ['@feature(!EMBER_ROUTING_MODEL_ARG) it should update correctly when the controller changes']() { + this.router.map(function() { + this.route('color', { path: '/colors/:color' }); + }); + + this.add( + 'route:color', + Route.extend({ + model(params) { + return { color: params.color }; + }, + + renderTemplate(controller, model) { + this.render({ controller: model.color, model }); + }, + }) + ); + + this.add( + 'controller:red', + Controller.extend({ + color: 'red', + }) + ); + + this.add( + 'controller:green', + Controller.extend({ + color: 'green', + }) + ); + + this.addTemplate('color', 'model color: {{this.model.color}}, controller color: {{color}}'); + + return this.visit('/colors/red') + .then(() => { + this.assertInnerHTML('model color: red, controller color: red'); + return this.visit('/colors/green'); + }) + .then(() => { + this.assertInnerHTML('model color: green, controller color: green'); + }); + } + + ['@feature(EMBER_ROUTING_MODEL_ARG) it should update correctly when the controller changes']() { this.router.map(function() { this.route('color', { path: '/colors/:color' }); }); @@ -371,7 +821,7 @@ moduleFor( }) ); - this.addTemplate('color', 'model color: {{model.color}}, controller color: {{color}}'); + this.addTemplate('color', 'model color: {{@model.color}}, controller color: {{color}}'); return this.visit('/colors/red') .then(() => { @@ -383,7 +833,7 @@ moduleFor( }); } - ['@test it should produce a stable DOM when two routes render the same template']() { + ['@feature(!EMBER_ROUTING_MODEL_ARG) it should produce a stable DOM when two routes render the same template']() { this.router.map(function() { this.route('a'); this.route('b'); @@ -422,7 +872,60 @@ moduleFor( }) ); - this.addTemplate('common', '{{prefix}} {{model}}'); + this.addTemplate('common', '{{prefix}} {{this.model}}'); + + return this.visit('/a') + .then(() => { + this.assertInnerHTML('common A'); + this.takeSnapshot(); + return this.visit('/b'); + }) + .then(() => { + this.assertInnerHTML('common B'); + this.assertInvariants(); + }); + } + + ['@feature(EMBER_ROUTING_MODEL_ARG) it should produce a stable DOM when two routes render the same template']() { + this.router.map(function() { + this.route('a'); + this.route('b'); + }); + + this.add( + 'route:a', + Route.extend({ + model() { + return 'A'; + }, + + renderTemplate(controller, model) { + this.render('common', { controller: 'common', model }); + }, + }) + ); + + this.add( + 'route:b', + Route.extend({ + model() { + return 'B'; + }, + + renderTemplate(controller, model) { + this.render('common', { controller: 'common', model }); + }, + }) + ); + + this.add( + 'controller:common', + Controller.extend({ + prefix: 'common', + }) + ); + + this.addTemplate('common', '{{prefix}} {{@model}}'); return this.visit('/a') .then(() => { @@ -469,36 +972,92 @@ moduleFor( }); } - async ['@test it emits a useful backtracking re-render assertion message'](assert) { + async ['@feature(!EMBER_ROUTING_MODEL_ARG) it emits a useful backtracking re-render assertion message']( + assert + ) { this.router.map(function() { this.route('routeWithError'); }); + this.add( + 'controller:routeWithError', + Controller.extend({ + toString() { + return 'RouteWithErrorController'; + }, + }) + ); + this.add( 'route:routeWithError', Route.extend({ model() { - return { name: 'Alex' }; + return { + name: 'Alex', + toString() { + return `Person (${this.name})`; + }, + }; }, }) ); - this.addTemplate('routeWithError', 'Hi {{model.name}} {{x-foo person=model}}'); + this.addTemplate('routeWithError', 'Hi {{this.model.name}} '); - this.addComponent('x-foo', { + this.addComponent('foo', { ComponentClass: Component.extend({ init() { this._super(...arguments); this.set('person.name', 'Ben'); }, }), - template: 'Hi {{person.name}} from component', + template: 'Hi {{this.person.name}} from component', + }); + + let expectedBacktrackingMessage = /modified `Person \(Ben\)` twice in a single render\. It was first rendered as `this\.model\.name` in "template:my-app\/templates\/routeWithError\.hbs" and then modified later in "component:foo"/; + + await this.visit('/'); + + return assert.rejectsAssertion(this.visit('/routeWithError'), expectedBacktrackingMessage); + } + + async ['@feature(EMBER_ROUTING_MODEL_ARG) it emits a useful backtracking re-render assertion message']( + assert + ) { + this.router.map(function() { + this.route('routeWithError'); }); - let expectedBacktrackingMessage = /modified "model\.name" twice on \[object Object\] in a single render\. It was rendered in "template:my-app\/templates\/routeWithError.hbs" and modified in "component:x-foo"/; + this.add( + 'route:routeWithError', + Route.extend({ + model() { + return { + name: 'Alex', + toString() { + return `Person (${this.name})`; + }, + }; + }, + }) + ); + + this.addTemplate('routeWithError', 'Hi {{@model.name}} '); + + let expectedBacktrackingMessage = /modified `Person \(Ben\)` twice in a single render\. It was first rendered as `@model\.name` in "template:my-app\/templates\/routeWithError\.hbs" and then modified later in "component:foo"/; await this.visit('/'); + this.addComponent('foo', { + ComponentClass: Component.extend({ + init() { + this._super(...arguments); + this.set('person.name', 'Ben'); + }, + }), + template: 'Hi {{this.person.name}} from component', + }); + return assert.rejectsAssertion(this.visit('/routeWithError'), expectedBacktrackingMessage); } diff --git a/packages/@ember/-internals/glimmer/tests/integration/components/angle-bracket-invocation-test.js b/packages/@ember/-internals/glimmer/tests/integration/components/angle-bracket-invocation-test.js index 037f54da93e..40cf897a6c8 100644 --- a/packages/@ember/-internals/glimmer/tests/integration/components/angle-bracket-invocation-test.js +++ b/packages/@ember/-internals/glimmer/tests/integration/components/angle-bracket-invocation-test.js @@ -418,7 +418,7 @@ moduleFor( template: '{{@foo}}', }); - this.render('', { + this.render('', { model: { bar: 'Hola', }, @@ -444,7 +444,7 @@ moduleFor( template: '{{foo}}', }); - this.render('', { + this.render('', { model: { bar: 'Hola', }, diff --git a/packages/@ember/-internals/glimmer/tests/integration/components/attribute-bindings-test.js b/packages/@ember/-internals/glimmer/tests/integration/components/attribute-bindings-test.js index ea7b83dc9f6..238b6e70552 100644 --- a/packages/@ember/-internals/glimmer/tests/integration/components/attribute-bindings-test.js +++ b/packages/@ember/-internals/glimmer/tests/integration/components/attribute-bindings-test.js @@ -66,7 +66,7 @@ moduleFor( template: 'hello', }); - this.render('{{foo-bar foo=model.foo baz=model.baz}}', { + this.render('{{foo-bar foo=this.model.foo baz=this.model.baz}}', { model: { foo: undefined, baz: { bar: 'bar' } }, }); diff --git a/packages/@ember/-internals/glimmer/tests/integration/components/class-bindings-test.js b/packages/@ember/-internals/glimmer/tests/integration/components/class-bindings-test.js index 91878aeed43..80634cec759 100644 --- a/packages/@ember/-internals/glimmer/tests/integration/components/class-bindings-test.js +++ b/packages/@ember/-internals/glimmer/tests/integration/components/class-bindings-test.js @@ -82,7 +82,7 @@ moduleFor( template: 'hello', }); - this.render('{{foo-bar joker=model.wat batman=model.super}}', { + this.render('{{foo-bar joker=this.model.wat batman=this.model.super}}', { model: { wat: false, super: { robin: true } }, }); diff --git a/packages/@ember/-internals/glimmer/tests/integration/components/contextual-components-test.js b/packages/@ember/-internals/glimmer/tests/integration/components/contextual-components-test.js index e6bb9c711f0..e44812e0b85 100644 --- a/packages/@ember/-internals/glimmer/tests/integration/components/contextual-components-test.js +++ b/packages/@ember/-internals/glimmer/tests/integration/components/contextual-components-test.js @@ -51,7 +51,7 @@ moduleFor( template: '{{#each params as |p|}}{{p}}{{/each}}', }); - this.render('{{component (component "-looked-up" model.greeting model.name)}}', { + this.render('{{component (component "-looked-up" this.model.greeting this.model.name)}}', { model: { greeting: 'Gabon ', name: 'Zack', @@ -87,7 +87,7 @@ moduleFor( }); this.render( - '{{component (component "-looked-up" model.greeting model.name) model.name model.greeting}}', + '{{component (component "-looked-up" this.model.greeting this.model.name) this.model.name this.model.greeting}}', { model: { greeting: 'Gabon ', @@ -123,12 +123,15 @@ moduleFor( template: '{{#each params as |p|}}{{p}}{{/each}}', }); - this.render('{{component (component (component "-looked-up" model.greeting model.name))}}', { - model: { - greeting: 'Gabon ', - name: 'Zack', - }, - }); + this.render( + '{{component (component (component "-looked-up" this.model.greeting this.model.name))}}', + { + model: { + greeting: 'Gabon ', + name: 'Zack', + }, + } + ); this.assertText('Gabon Zack'); @@ -158,7 +161,7 @@ moduleFor( }); this.render( - '{{component (component (component "-looked-up" model.greeting model.name) model.name model.greeting)}}', + '{{component (component (component "-looked-up" this.model.greeting this.model.name) this.model.name this.model.greeting)}}', { model: { greeting: 'Gabon ', @@ -214,7 +217,7 @@ moduleFor( template: 'Namaste', }); - this.render('{{component (component model.lookupComponent)}}', { + this.render('{{component (component this.model.lookupComponent)}}', { model: { lookupComponent: '-mandarin', }, @@ -240,7 +243,7 @@ moduleFor( template: '{{greeting}}', }); - this.render(`{{component (component "-looked-up" greeting=model.greeting)}}`, { + this.render(`{{component (component "-looked-up" greeting=this.model.greeting)}}`, { model: { greeting: 'Hodi', }, @@ -268,7 +271,7 @@ moduleFor( this.render( strip` - {{#with (hash comp=(component "-looked-up" greeting=model.greeting)) as |my|}} + {{#with (hash comp=(component "-looked-up" greeting=this.model.greeting)) as |my|}} {{#my.comp}}{{/my.comp}} {{/with}}`, { @@ -338,7 +341,7 @@ moduleFor( strip` {{#with (component "-looked-up" greeting="Hola" name="Dolores" age=33) as |first|}} {{#with (component first greeting="Hej" name="Sigmundur") as |second|}} - {{component second greeting=model.greeting}} + {{component second greeting=this.model.greeting}} {{/with}} {{/with}}`, { @@ -379,7 +382,7 @@ moduleFor( }); this.render( - '{{component "-inner-component" (component "-looked-up" model.outerName model.outerAge)}}', + '{{component "-inner-component" (component "-looked-up" this.model.outerName this.model.outerAge)}}', { model: { outerName: 'Outer', @@ -425,7 +428,7 @@ moduleFor( }); this.render( - '{{component "-inner-component" (component "-looked-up" name=model.outerName age=model.outerAge)}}', + '{{component "-inner-component" (component "-looked-up" name=this.model.outerName age=this.model.outerAge)}}', { model: { outerName: 'Outer', @@ -468,7 +471,7 @@ moduleFor( template: '{{greeting}} {{name}}', }); - this.render('{{component (component "-looked-up" model.name greeting="Hodi")}}', { + this.render('{{component (component "-looked-up" this.model.name greeting="Hodi")}}', { model: { name: 'Hodari', }, @@ -622,7 +625,7 @@ moduleFor( this.render( strip` {{#with (hash lookedup=(component "-looked-up")) as |object|}} - {{object.lookedup expectedText=model.expectedText}} + {{object.lookedup expectedText=this.model.expectedText}} {{/with}}`, { model: { @@ -654,7 +657,7 @@ moduleFor( this.render( strip` - {{#with (hash lookedup=(component "-looked-up" expectedText=model.expectedText)) as |object|}} + {{#with (hash lookedup=(component "-looked-up" expectedText=this.model.expectedText)) as |object|}} {{object.lookedup}} {{/with}}`, { @@ -692,7 +695,7 @@ moduleFor( this.render( strip` {{#with (hash lookedup=(component "-looked-up")) as |object|}} - {{object.lookedup model.expectedText "Hola"}} + {{object.lookedup this.model.expectedText "Hola"}} {{/with}}`, { model: { @@ -774,7 +777,7 @@ moduleFor( `, }); - this.render('{{my-action-component myProp=model.myProp}}', { + this.render('{{my-action-component myProp=this.model.myProp}}', { model: { myProp: 1, }, @@ -839,8 +842,8 @@ moduleFor( this.render( strip` - {{component (component "change-button" model.val2)}} - {{model.val2}}`, + {{component (component "change-button" this.model.val2)}} + {{this.model.val2}}`, { model: { val2: 8, @@ -1363,7 +1366,7 @@ moduleFor( class ContextualComponentMutableParamsTest extends RenderingTestCase { render(templateStr, context = {}) { super.render( - `${templateStr}{{model.val2}}`, + `${templateStr}{{this.model.val2}}`, assign(context, { model: { val2: 8 } }) ); } @@ -1413,7 +1416,7 @@ applyMixins( { title: 'param', setup() { - this.render('{{component (component "change-button" model.val2)}}'); + this.render('{{component (component "change-button" this.model.val2)}}'); }, }, @@ -1427,7 +1430,7 @@ applyMixins( template: '{{component components.comp}}', }); - this.render('{{my-comp (hash comp=(component "change-button" model.val2))}}'); + this.render('{{my-comp (hash comp=(component "change-button" this.model.val2))}}'); }, }, @@ -1438,7 +1441,7 @@ applyMixins( template: '{{component component}}', }); - this.render('{{my-comp component=(component "change-button" val=model.val2)}}'); + this.render('{{my-comp component=(component "change-button" val=this.model.val2)}}'); }, }, @@ -1450,7 +1453,7 @@ applyMixins( }); this.render( - '{{my-comp components=(hash button=(component "change-button" val=model.val2))}}' + '{{my-comp components=(hash button=(component "change-button" val=this.model.val2))}}' ); }, }, diff --git a/packages/@ember/-internals/glimmer/tests/integration/components/curly-components-test.js b/packages/@ember/-internals/glimmer/tests/integration/components/curly-components-test.js index cb0c3dde880..ebcc64dd76f 100644 --- a/packages/@ember/-internals/glimmer/tests/integration/components/curly-components-test.js +++ b/packages/@ember/-internals/glimmer/tests/integration/components/curly-components-test.js @@ -429,7 +429,7 @@ moduleFor( ['@test should apply classes of the dasherized property name when bound property specified is true']() { this.registerComponent('foo-bar', { template: 'hello' }); - this.render('{{foo-bar class=model.someTruth}}', { + this.render('{{foo-bar class=this.model.someTruth}}', { model: { someTruth: true }, }); @@ -688,7 +688,7 @@ moduleFor( template: '{{@foo}}', }); - this.render('{{foo-bar foo=model.bar}}', { + this.render('{{foo-bar foo=this.model.bar}}', { model: { bar: 'Hola', }, @@ -714,7 +714,7 @@ moduleFor( template: '{{foo}}', }); - this.render('{{foo-bar foo=model.bar}}', { + this.render('{{foo-bar foo=this.model.bar}}', { model: { bar: 'Hola', }, @@ -1478,7 +1478,7 @@ moduleFor( `, }); - this.render('{{foo-bar value=model.value items=model.items}}', { + this.render('{{foo-bar value=this.model.value items=this.model.items}}', { model: { value: 'wat', items: [1, 2, 3], @@ -2533,7 +2533,7 @@ moduleFor( template: '
{{value}}
', }); - let expectedBacktrackingMessage = /modified "value" twice on <.+?> in a single render\. It was rendered in "component:x-middle" and modified in "component:x-inner"/; + let expectedBacktrackingMessage = /modified `<.+?>` twice in a single render\. It was first rendered as `this\.value` in "component:x-middle" and then modified later in "component:x-inner"/; expectAssertion(() => { this.render('{{x-outer}}'); @@ -2560,7 +2560,7 @@ moduleFor( template: '
{{wrapper.content}}
', }); - let expectedBacktrackingMessage = /modified "wrapper\.content" twice on <.+?> in a single render\. It was rendered in "component:x-outer" and modified in "component:x-inner"/; + let expectedBacktrackingMessage = /modified `<.+?>` twice in a single render\. It was first rendered as `this\.wrapper\.content` in "component:x-outer" and then modified later in "component:x-inner"/; expectAssertion(() => { this.render('{{x-outer}}'); diff --git a/packages/@ember/-internals/glimmer/tests/integration/components/dynamic-components-test.js b/packages/@ember/-internals/glimmer/tests/integration/components/dynamic-components-test.js index 202554de93a..7b27e5cbe83 100644 --- a/packages/@ember/-internals/glimmer/tests/integration/components/dynamic-components-test.js +++ b/packages/@ember/-internals/glimmer/tests/integration/components/dynamic-components-test.js @@ -761,7 +761,12 @@ moduleFor( ComponentClass: Component.extend({ init() { this._super(...arguments); - this.set('person', { name: 'Alex' }); + this.set('person', { + name: 'Alex', + toString() { + return `Person (${this.name})`; + }, + }); }, }), template: `Hi {{person.name}}! {{component "error-component" person=person}}`, @@ -771,13 +776,13 @@ moduleFor( ComponentClass: Component.extend({ init() { this._super(...arguments); - this.set('person.name', { name: 'Ben' }); + this.set('person.name', 'Ben'); }, }), template: '{{person.name}}', }); - let expectedBacktrackingMessage = /modified "person\.name" twice on \[object Object\] in a single render\. It was rendered in "component:outer-component" and modified in "component:error-component"/; + let expectedBacktrackingMessage = /modified `Person \(Ben\)` twice in a single render\. It was first rendered as `this\.person\.name` in "component:outer-component" and then modified later in "component:error-component"/; expectAssertion(() => { this.render('{{component componentName}}', { diff --git a/packages/@ember/-internals/glimmer/tests/integration/components/life-cycle-test.js b/packages/@ember/-internals/glimmer/tests/integration/components/life-cycle-test.js index b62e9ff3951..295a507c72f 100644 --- a/packages/@ember/-internals/glimmer/tests/integration/components/life-cycle-test.js +++ b/packages/@ember/-internals/glimmer/tests/integration/components/life-cycle-test.js @@ -1465,16 +1465,16 @@ moduleFor( this.render( strip` - {{#each items as |item|}} - {{#parent-component itemId=item.id}}{{item.id}}{{/parent-component}} - {{/each}} - {{#if model.shouldShow}} - {{#parent-component itemId=6}}6{{/parent-component}} - {{/if}} - {{#if model.shouldShow}} - {{#parent-component itemId=7}}7{{/parent-component}} - {{/if}} - `, + {{#each items as |item|}} + {{#parent-component itemId=item.id}}{{item.id}}{{/parent-component}} + {{/each}} + {{#if this.model.shouldShow}} + {{#parent-component itemId=6}}6{{/parent-component}} + {{/if}} + {{#if this.model.shouldShow}} + {{#parent-component itemId=7}}7{{/parent-component}} + {{/if}} + `, { items: array, model: { shouldShow: true }, diff --git a/packages/@ember/-internals/glimmer/tests/integration/components/link-to/query-params-angle-test.js b/packages/@ember/-internals/glimmer/tests/integration/components/link-to/query-params-angle-test.js index f5c72e8ae97..7a9e9da4645 100644 --- a/packages/@ember/-internals/glimmer/tests/integration/components/link-to/query-params-angle-test.js +++ b/packages/@ember/-internals/glimmer/tests/integration/components/link-to/query-params-angle-test.js @@ -235,7 +235,7 @@ moduleFor( }); } - ['@test generates proper href for `LinkTo` with no @route after transitioning to an error route GH#17963']( + ['@feature(!EMBER_ROUTING_MODEL_ARG) generates proper href for `LinkTo` with no @route after transitioning to an error route GH#17963']( assert ) { this.router.map(function() { @@ -258,7 +258,7 @@ moduleFor( }) ); - this.addTemplate('error', `Error: {{model.message}}`); + this.addTemplate('error', `Error: {{this.model.message}}`); this.addTemplate( 'application', @@ -306,6 +306,75 @@ moduleFor( }); } + async ['@feature(EMBER_ROUTING_MODEL_ARG) generates proper href for `LinkTo` with no @route after transitioning to an error route GH#17963']( + assert + ) { + this.router.map(function() { + this.route('bad'); + }); + + this.add( + 'controller:application', + Controller.extend({ + queryParams: ['baz'], + }) + ); + + this.add( + 'route:bad', + Route.extend({ + model() { + throw new Error('bad!'); + }, + }) + ); + + this.addTemplate('error', `Error: {{@model.message}}`); + + this.addTemplate( + 'application', + ` + + Bad + + + + Good + + + {{outlet}} + ` + ); + + await this.visit('/'); + + assert.equal(this.$('#good-link').length, 1, 'good-link should be in the DOM'); + assert.equal(this.$('#bad-link').length, 1, 'bad-link should be in the DOM'); + + let goodLink = this.$('#good-link'); + assert.equal(goodLink.attr('href'), '/?baz=lol'); + + await this.visit('/bad'); + + assert.equal(this.$('#good-link').length, 1, 'good-link should be in the DOM'); + assert.equal(this.$('#bad-link').length, 1, 'bad-link should be in the DOM'); + + goodLink = this.$('#good-link'); + // should still be / because we never entered /bad (it errored before being fully entered) + // and error states do not get represented in the URL, so we are _effectively_ still + // on / + assert.equal(goodLink.attr('href'), '/?baz=lol'); + + runTask(() => this.click('#good-link')); + + let applicationController = this.getController('application'); + assert.deepEqual( + applicationController.getProperties('baz'), + { baz: 'lol' }, + 'index controller QP properties updated' + ); + } + ['@test supplied QP properties can be bound'](assert) { this.addTemplate( 'index', diff --git a/packages/@ember/-internals/glimmer/tests/integration/components/link-to/routing-angle-test.js b/packages/@ember/-internals/glimmer/tests/integration/components/link-to/routing-angle-test.js index ca2ac083b75..1b3969c1afc 100644 --- a/packages/@ember/-internals/glimmer/tests/integration/components/link-to/routing-angle-test.js +++ b/packages/@ember/-internals/glimmer/tests/integration/components/link-to/routing-angle-test.js @@ -916,7 +916,9 @@ moduleFor( }); } - [`@test The component moves into the named route with context`](assert) { + [`@feature(!EMBER_ROUTING_MODEL_ARG) The component moves into the named route with context`]( + assert + ) { this.router.map(function() { this.route('about'); this.route('item', { path: '/item/:id' }); @@ -927,7 +929,7 @@ moduleFor( `

List

    - {{#each model as |person|}} + {{#each this.model as |person|}}
  • {{person.name}} @@ -943,7 +945,7 @@ moduleFor( 'item', `

    Item

    -

    {{model.name}}

    +

    {{this.model.name}}

    Home ` ); @@ -1002,6 +1004,89 @@ moduleFor( }); } + async [`@feature(EMBER_ROUTING_MODEL_ARG) The component moves into the named route with context`]( + assert + ) { + this.router.map(function() { + this.route('about'); + this.route('item', { path: '/item/:id' }); + }); + + this.addTemplate( + 'about', + ` +

    List

    +
      + {{#each @model as |person|}} +
    • + + {{person.name}} + +
    • + {{/each}} +
    + Home + ` + ); + + this.addTemplate( + 'item', + ` +

    Item

    +

    {{@model.name}}

    + Home + ` + ); + + this.addTemplate( + 'index', + ` +

    Home

    + About + ` + ); + + this.add( + 'route:about', + Route.extend({ + model() { + return [ + { id: 'yehuda', name: 'Yehuda Katz' }, + { id: 'tom', name: 'Tom Dale' }, + { id: 'erik', name: 'Erik Brynroflsson' }, + ]; + }, + }) + ); + + await this.visit('/about'); + + assert.equal(this.$('h3.list').length, 1, 'The home template was rendered'); + assert.equal( + normalizeUrl(this.$('#home-link').attr('href')), + '/', + 'The home link points back at /' + ); + + await this.click('#yehuda'); + + assert.equal(this.$('h3.item').length, 1, 'The item template was rendered'); + assert.equal(this.$('p').text(), 'Yehuda Katz', 'The name is correct'); + + await this.click('#home-link'); + + await this.click('#about-link'); + + assert.equal(normalizeUrl(this.$('li a#yehuda').attr('href')), '/item/yehuda'); + assert.equal(normalizeUrl(this.$('li a#tom').attr('href')), '/item/tom'); + assert.equal(normalizeUrl(this.$('li a#erik').attr('href')), '/item/erik'); + + await this.click('#erik'); + + assert.equal(this.$('h3.item').length, 1, 'The item template was rendered'); + assert.equal(this.$('p').text(), 'Erik Brynroflsson', 'The name is correct'); + } + [`@test The component binds some anchor html tag common attributes`](assert) { this.addTemplate( 'index', @@ -1591,7 +1676,7 @@ moduleFor( }); } - ['@test [GH#17018] passing model to with `hash` helper works']() { + ['@feature(!EMBER_ROUTING_MODEL_ARG) [GH#17018] passing model to with `hash` helper works']() { this.router.map(function() { this.route('post', { path: '/posts/:post_id' }); }); @@ -1629,6 +1714,44 @@ moduleFor( }); } + ['@feature(EMBER_ROUTING_MODEL_ARG) [GH#17018] passing model to with `hash` helper works']() { + this.router.map(function() { + this.route('post', { path: '/posts/:post_id' }); + }); + + this.add( + 'route:index', + Route.extend({ + model() { + return RSVP.hash({ + user: { name: 'Papa Smurf' }, + }); + }, + }) + ); + + this.addTemplate( + 'index', + `Post` + ); + + this.addTemplate('post', 'Post: {{@model.user.name}}'); + + return this.visit('/') + .then(() => { + this.assertComponentElement(this.firstChild, { + tagName: 'a', + attrs: { href: '/posts/someId' }, + content: 'Post', + }); + + return this.click('a'); + }) + .then(() => { + this.assertText('Post: Papa Smurf'); + }); + } + [`@test The component can use dynamic params`](assert) { this.router.map(function() { this.route('foo', { path: 'foo/:some/:thing' }); diff --git a/packages/@ember/-internals/glimmer/tests/integration/components/link-to/routing-curly-test.js b/packages/@ember/-internals/glimmer/tests/integration/components/link-to/routing-curly-test.js index 474936af633..1696e201ee0 100644 --- a/packages/@ember/-internals/glimmer/tests/integration/components/link-to/routing-curly-test.js +++ b/packages/@ember/-internals/glimmer/tests/integration/components/link-to/routing-curly-test.js @@ -916,7 +916,9 @@ moduleFor( }); } - [`@test The {{link-to}} component moves into the named route with context`](assert) { + [`@feature(!EMBER_ROUTING_MODEL_ARG) The {{link-to}} component moves into the named route with context`]( + assert + ) { this.router.map(function() { this.route('about'); this.route('item', { path: '/item/:id' }); @@ -927,7 +929,7 @@ moduleFor( `

    List

      - {{#each model as |person|}} + {{#each this.model as |person|}}
    • {{#link-to 'item' person id=person.id}} {{person.name}} @@ -943,7 +945,7 @@ moduleFor( 'item', `

      Item

      -

      {{model.name}}

      +

      {{this.model.name}}

      {{#link-to 'index' id='home-link'}}Home{{/link-to}} ` ); @@ -1002,6 +1004,89 @@ moduleFor( }); } + async [`@feature(EMBER_ROUTING_MODEL_ARG) The {{link-to}} component moves into the named route with context`]( + assert + ) { + this.router.map(function() { + this.route('about'); + this.route('item', { path: '/item/:id' }); + }); + + this.addTemplate( + 'about', + ` +

      List

      +
        + {{#each @model as |person|}} +
      • + {{#link-to 'item' person id=person.id}} + {{person.name}} + {{/link-to}} +
      • + {{/each}} +
      + {{#link-to 'index' id='home-link'}}Home{{/link-to}} + ` + ); + + this.addTemplate( + 'item', + ` +

      Item

      +

      {{@model.name}}

      + {{#link-to 'index' id='home-link'}}Home{{/link-to}} + ` + ); + + this.addTemplate( + 'index', + ` +

      Home

      + {{#link-to 'about' id='about-link'}}About{{/link-to}} + ` + ); + + this.add( + 'route:about', + Route.extend({ + model() { + return [ + { id: 'yehuda', name: 'Yehuda Katz' }, + { id: 'tom', name: 'Tom Dale' }, + { id: 'erik', name: 'Erik Brynroflsson' }, + ]; + }, + }) + ); + + await this.visit('/about'); + + assert.equal(this.$('h3.list').length, 1, 'The home template was rendered'); + assert.equal( + normalizeUrl(this.$('#home-link').attr('href')), + '/', + 'The home link points back at /' + ); + + await this.click('#yehuda'); + + assert.equal(this.$('h3.item').length, 1, 'The item template was rendered'); + assert.equal(this.$('p').text(), 'Yehuda Katz', 'The name is correct'); + + await this.click('#home-link'); + + await this.click('#about-link'); + + assert.equal(normalizeUrl(this.$('li a#yehuda').attr('href')), '/item/yehuda'); + assert.equal(normalizeUrl(this.$('li a#tom').attr('href')), '/item/tom'); + assert.equal(normalizeUrl(this.$('li a#erik').attr('href')), '/item/erik'); + + await this.click('#erik'); + + assert.equal(this.$('h3.item').length, 1, 'The item template was rendered'); + assert.equal(this.$('p').text(), 'Erik Brynroflsson', 'The name is correct'); + } + [`@test The {{link-to}} component binds some anchor html tag common attributes`](assert) { this.addTemplate( 'index', @@ -1539,7 +1624,7 @@ moduleFor( }); } - [`@test The non-block form {{link-to}} component moves into the named route with context`]( + [`@feature(!EMBER_ROUTING_MODEL_ARG) The non-block form {{link-to}} component moves into the named route with context`]( assert ) { assert.expect(5); @@ -1566,7 +1651,7 @@ moduleFor( `

      Home

        - {{#each model as |person|}} + {{#each this.model as |person|}}
      • {{link-to person.name 'item' person id=person.id}}
      • @@ -1578,7 +1663,7 @@ moduleFor( 'item', `

        Item

        -

        {{model.name}}

        +

        {{this.model.name}}

        {{#link-to 'index' id='home-link'}}Home{{/link-to}} ` ); @@ -1600,6 +1685,64 @@ moduleFor( }); } + async [`@feature(EMBER_ROUTING_MODEL_ARG) The non-block form {{link-to}} component moves into the named route with context`]( + assert + ) { + assert.expect(5); + + this.router.map(function() { + this.route('item', { path: '/item/:id' }); + }); + + this.add( + 'route:index', + Route.extend({ + model() { + return [ + { id: 'yehuda', name: 'Yehuda Katz' }, + { id: 'tom', name: 'Tom Dale' }, + { id: 'erik', name: 'Erik Brynroflsson' }, + ]; + }, + }) + ); + + this.addTemplate( + 'index', + ` +

        Home

        +
          + {{#each @model as |person|}} +
        • + {{link-to person.name 'item' person id=person.id}} +
        • + {{/each}} +
        + ` + ); + this.addTemplate( + 'item', + ` +

        Item

        +

        {{@model.name}}

        + {{#link-to 'index' id='home-link'}}Home{{/link-to}} + ` + ); + + await this.visit('/'); + + await this.click('#yehuda'); + + assert.equal(this.$('h3.item').length, 1, 'The item template was rendered'); + assert.equal(this.$('p').text(), 'Yehuda Katz', 'The name is correct'); + + await this.click('#home-link'); + + assert.equal(normalizeUrl(this.$('li a#yehuda').attr('href')), '/item/yehuda'); + assert.equal(normalizeUrl(this.$('li a#tom').attr('href')), '/item/tom'); + assert.equal(normalizeUrl(this.$('li a#erik').attr('href')), '/item/erik'); + } + [`@test The non-block form {{link-to}} performs property lookup`](assert) { this.router.map(function() { this.route('about'); @@ -1865,7 +2008,7 @@ moduleFor( }); } - ['@test [GH#17018] passing model to link-to with `hash` helper works']() { + ['@feature(!EMBER_ROUTING_MODEL_ARG) [GH#17018] passing model to link-to with `hash` helper works']() { this.router.map(function() { this.route('post', { path: '/posts/:post_id' }); }); @@ -1902,6 +2045,40 @@ moduleFor( }); } + ['@feature(EMBER_ROUTING_MODEL_ARG) [GH#17018] passing model to link-to with `hash` helper works']() { + this.router.map(function() { + this.route('post', { path: '/posts/:post_id' }); + }); + + this.add( + 'route:index', + Route.extend({ + model() { + return RSVP.hash({ + user: { name: 'Papa Smurf' }, + }); + }, + }) + ); + + this.addTemplate('index', `{{link-to 'Post' 'post' (hash id="someId" user=@model.user)}}`); + this.addTemplate('post', 'Post: {{@model.user.name}}'); + + return this.visit('/') + .then(() => { + this.assertComponentElement(this.firstChild, { + tagName: 'a', + attrs: { href: '/posts/someId' }, + content: 'Post', + }); + + return this.click('a'); + }) + .then(() => { + this.assertText('Post: Papa Smurf'); + }); + } + [`@test The {{link-to}} component can use dynamic params`](assert) { this.router.map(function() { this.route('foo', { path: 'foo/:some/:thing' }); diff --git a/packages/@ember/-internals/glimmer/tests/integration/components/textarea-angle-test.js b/packages/@ember/-internals/glimmer/tests/integration/components/textarea-angle-test.js index 97a1a025fa6..592107c7143 100644 --- a/packages/@ember/-internals/glimmer/tests/integration/components/textarea-angle-test.js +++ b/packages/@ember/-internals/glimmer/tests/integration/components/textarea-angle-test.js @@ -150,7 +150,7 @@ moduleFor( } ['@test Should bind its contents to the specified @value']() { - this.render('