diff --git a/FEATURES.md b/FEATURES.md index 3b2356e0f8f..7f8b5ed0027 100644 --- a/FEATURES.md +++ b/FEATURES.md @@ -32,6 +32,11 @@ for a detailed explanation. Add `{{@foo}}` syntax to access named arguments in component templates per [RFC](https://github.com/emberjs/rfcs/pull/276). +* `ember-glimmer-remove-application-template-wrapper` + + Remove the `
` wrapper around the application template per + [RFC](https://github.com/emberjs/rfcs/pull/280). + * `ember-glimmer-template-only-components` Use Glimmer Components semantics for template-only components per diff --git a/features.json b/features.json index 7912a91f9ce..9507641ea05 100644 --- a/features.json +++ b/features.json @@ -4,6 +4,7 @@ "ember-libraries-isregistered": null, "ember-improved-instrumentation": null, "ember-glimmer-named-arguments": null, + "ember-glimmer-remove-application-template-wrapper": null, "ember-glimmer-template-only-components": null, "ember-routing-router-service": true, "ember-engines-mount-params": true, diff --git a/packages/ember-application/tests/system/application_test.js b/packages/ember-application/tests/system/application_test.js index 3437abb871b..ce831e6f0aa 100644 --- a/packages/ember-application/tests/system/application_test.js +++ b/packages/ember-application/tests/system/application_test.js @@ -215,10 +215,9 @@ moduleFor('Ember.Application, default resolver with autoboot', class extends Def } [`@test Minimal Application initialized with just an application template`](assert) { - this.$().html(''); + jQuery('#qunit-fixture').html(''); this.runTask(() => this.createApplication()); - - assert.equal(this.$().text().trim(), 'Hello World'); + this.assertInnerHTML('Hello World'); } }); diff --git a/packages/ember-application/tests/system/visit_test.js b/packages/ember-application/tests/system/visit_test.js index d6bc02079fa..5bbbd105cb3 100644 --- a/packages/ember-application/tests/system/visit_test.js +++ b/packages/ember-application/tests/system/visit_test.js @@ -285,7 +285,7 @@ moduleFor('Ember.Application - visit()', class extends ApplicationTestCase { 'promise is resolved with an ApplicationInstance' ); assert.equal( - this.$('.ember-view h1').text(), 'Hello world', + this.$('h1').text(), 'Hello world', 'the application was rendered once the promise resolves' ); }); @@ -576,8 +576,8 @@ moduleFor('Ember.Application - visit()', class extends ApplicationTestCase { } })); - let $foo = jQuery('
').appendTo(this.$()); - let $bar = jQuery('
').appendTo(this.$()); + let $foo = jQuery('
').appendTo('#qunit-fixture'); + let $bar = jQuery('
').appendTo('#qunit-fixture'); let data = encodeURIComponent(JSON.stringify({ name: 'Godfrey' })); let instances = []; diff --git a/packages/ember-glimmer/externs.d.ts b/packages/ember-glimmer/externs.d.ts index ebc11625777..38b837f6545 100644 --- a/packages/ember-glimmer/externs.d.ts +++ b/packages/ember-glimmer/externs.d.ts @@ -3,6 +3,7 @@ declare module 'ember/features' { export const GLIMMER_CUSTOM_COMPONENT_MANAGER: boolean | null; export const EMBER_ENGINES_MOUNT_PARAMS: boolean | null; export const EMBER_GLIMMER_DETECT_BACKTRACKING_RERENDER: boolean | null; + export const EMBER_GLIMMER_REMOVE_APPLICATION_TEMPLATE_WRAPPER: boolean | null; export const EMBER_GLIMMER_TEMPLATE_ONLY_COMPONENTS: boolean | null; export const MANDATORY_SETTER: boolean | null; } diff --git a/packages/ember-glimmer/lib/component-managers/outlet.ts b/packages/ember-glimmer/lib/component-managers/outlet.ts index c194d936430..91234f2ef29 100644 --- a/packages/ember-glimmer/lib/component-managers/outlet.ts +++ b/packages/ember-glimmer/lib/component-managers/outlet.ts @@ -9,6 +9,7 @@ import { Destroyable } from '@glimmer/util/dist/types'; import { DEBUG } from 'ember-env-flags'; import { _instrumentStart } from 'ember-metal'; import { generateGuid, guidFor } from 'ember-utils'; +import { EMBER_GLIMMER_REMOVE_APPLICATION_TEMPLATE_WRAPPER } from 'ember/features'; import EmberEnvironment from '../environment'; import { OwnedTemplate, @@ -92,13 +93,35 @@ class TopLevelOutletComponentManager extends OutletComponentManager { } return new StateBucket(dynamicScope.outletState.value()); } - - layoutFor(definition: OutletComponentDefinition, _bucket: StateBucket, env: Environment) { - return (env as EmberEnvironment).getCompiledBlock(TopLevelOutletLayoutCompiler, definition.template); - } } -const TOP_LEVEL_MANAGER = new TopLevelOutletComponentManager(); +const TOP_LEVEL_MANAGER = (() => { + if (EMBER_GLIMMER_REMOVE_APPLICATION_TEMPLATE_WRAPPER) { + return new TopLevelOutletComponentManager(); + } else { + class WrappedTopLevelOutletLayoutCompiler { + static id = 'wrapped-top-level-outlet'; + + constructor(public template: WrappedTemplateFactory) { + } + + compile(builder: any) { + builder.wrapLayout(this.template); + builder.tag.static('div'); + builder.attrs.static('id', guidFor(this)); + builder.attrs.static('class', 'ember-view'); + } + } + + class WrappedTopLevelOutletComponentManager extends TopLevelOutletComponentManager { + layoutFor(definition: OutletComponentDefinition, _bucket: StateBucket, env: Environment) { + return (env as EmberEnvironment).getCompiledBlock(WrappedTopLevelOutletLayoutCompiler, definition.template); + } + } + + return new WrappedTopLevelOutletComponentManager(); + } +})(); export class TopLevelOutletComponentDefinition extends ComponentDefinition { public template: WrappedTemplateFactory; @@ -109,23 +132,6 @@ export class TopLevelOutletComponentDefinition extends ComponentDefinition { public outletName: string; public template: OwnedTemplate; diff --git a/packages/ember-glimmer/tests/integration/application/rendering-test.js b/packages/ember-glimmer/tests/integration/application/rendering-test.js index fab18587e1e..ab3154213a0 100644 --- a/packages/ember-glimmer/tests/integration/application/rendering-test.js +++ b/packages/ember-glimmer/tests/integration/application/rendering-test.js @@ -3,14 +3,23 @@ import { moduleFor, ApplicationTest } from '../../utils/test-case'; import { strip } from '../../utils/abstract-test-case'; import { Route } from 'ember-routing'; import { Component } from 'ember-glimmer'; +import { jQuery } from 'ember-views'; moduleFor('Application test: rendering', class extends ApplicationTest { - ['@test it can render the application template'](assert) { + ['@feature(!ember-glimmer-remove-application-template-wrapper) it can render the application template'](assert) { this.addTemplate('application', 'Hello world!'); return this.visit('/').then(() => { - this.assertText('Hello world!'); + this.assertComponentElement(this.element, { content: 'Hello world!' }); + }); + } + + ['@feature(ember-glimmer-remove-application-template-wrapper) it can render the application template'](assert) { + this.addTemplate('application', 'Hello world!'); + + return this.visit('/').then(() => { + this.assertInnerHTML('Hello world!'); }); } @@ -30,15 +39,13 @@ moduleFor('Application test: rendering', class extends ApplicationTest { `); return this.visit('/').then(() => { - this.assertComponentElement(this.firstChild, { - content: strip` -
    -
  • red
  • -
  • yellow
  • -
  • blue
  • -
- ` - }); + this.assertInnerHTML(strip` +
    +
  • red
  • +
  • yellow
  • +
  • blue
  • +
+ `); }); } @@ -67,15 +74,13 @@ moduleFor('Application test: rendering', class extends ApplicationTest { `); return this.visit('/lists/colors/favorite').then(() => { - this.assertComponentElement(this.firstChild, { - content: strip` -
    -
  • red
  • -
  • yellow
  • -
  • blue
  • -
- ` - }); + this.assertInnerHTML(strip` +
    +
  • red
  • +
  • yellow
  • +
  • blue
  • +
+ `); }); } @@ -118,20 +123,18 @@ moduleFor('Application test: rendering', class extends ApplicationTest { `); return this.visit('/colors').then(() => { - this.assertComponentElement(this.firstChild, { - content: strip` - -
-
    -
  • red
  • -
  • yellow
  • -
  • blue
  • -
-
- ` - }); + this.assertInnerHTML(strip` + +
+
    +
  • red
  • +
  • yellow
  • +
  • blue
  • +
+
+ `); }); } @@ -174,20 +177,18 @@ moduleFor('Application test: rendering', class extends ApplicationTest { `); return this.visit('/colors').then(() => { - this.assertComponentElement(this.firstChild, { - content: strip` - -
-
    -
  • red
  • -
  • yellow
  • -
  • blue
  • -
-
- ` - }); + this.assertInnerHTML(strip` + +
+
    +
  • red
  • +
  • yellow
  • +
  • blue
  • +
+
+ `); }); } @@ -233,11 +234,11 @@ moduleFor('Application test: rendering', class extends ApplicationTest { this.addTemplate('color', 'color: {{model}}'); return this.visit('/colors/red').then(() => { - this.assertComponentElement(this.firstChild, { content: 'color: red' }); + this.assertInnerHTML('color: red'); this.takeSnapshot(); return this.visit('/colors/green'); }).then(() => { - this.assertComponentElement(this.firstChild, { content: 'color: green' }); + this.assertInnerHTML('color: green'); this.assertInvariants(); }); } @@ -291,12 +292,10 @@ moduleFor('Application test: rendering', class extends ApplicationTest { this.addTemplate('color', 'model color: {{model.color}}, controller color: {{color}}'); return this.visit('/colors/red').then(() => { - this.assertComponentElement(this.firstChild, { content: 'model color: red, controller color: red' }); - this.takeSnapshot(); + this.assertInnerHTML('model color: red, controller color: red'); return this.visit('/colors/green'); }).then(() => { - this.assertComponentElement(this.firstChild, { content: 'model color: green, controller color: green' }); - this.assertInvariants(); + this.assertInnerHTML('model color: green, controller color: green'); }); } @@ -333,11 +332,11 @@ moduleFor('Application test: rendering', class extends ApplicationTest { this.addTemplate('common', '{{prefix}} {{model}}'); return this.visit('/a').then(() => { - this.assertComponentElement(this.firstChild, { content: 'common A' }); + this.assertInnerHTML('common A'); this.takeSnapshot(); return this.visit('/b'); }).then(() => { - this.assertComponentElement(this.firstChild, { content: 'common B' }); + this.assertInnerHTML('common B'); this.assertInvariants(); }); } @@ -350,7 +349,7 @@ moduleFor('Application test: rendering', class extends ApplicationTest { this.addTemplate('application', '{{outlet}}'); this.addTemplate('index', '{{#if true}}1{{/if}}
2
'); return this.visit('/').then(() => { - this.assertComponentElement(this.firstChild, { content: '1
2
' }); + this.assertInnerHTML('1
2
'); }); } @@ -368,9 +367,7 @@ moduleFor('Application test: rendering', class extends ApplicationTest { this.addTemplate('a', 'Hello from A!'); return this.visit('/').then(() => { - this.assertComponentElement(this.firstChild, { - content: `Hello from A!` - }); + this.assertInnerHTML('Hello from A!'); }); } diff --git a/packages/ember-glimmer/tests/integration/components/link-to-test.js b/packages/ember-glimmer/tests/integration/components/link-to-test.js index 0e85ec24eaa..3cfec32da8a 100644 --- a/packages/ember-glimmer/tests/integration/components/link-to-test.js +++ b/packages/ember-glimmer/tests/integration/components/link-to-test.js @@ -118,7 +118,7 @@ moduleFor('Link-to component with query-params', class extends ApplicationTest { this.addTemplate('index', `{{#link-to 'index' (query-params foo='456' bar='NAW')}}Index{{/link-to}}`); return this.visit('/').then(() => { - this.assertComponentElement(this.firstChild.firstElementChild, { + this.assertComponentElement(this.firstChild, { tagName: 'a', attrs: { href: '/?bar=NAW&foo=456' }, content: 'Index' @@ -130,7 +130,7 @@ moduleFor('Link-to component with query-params', class extends ApplicationTest { this.addTemplate('index', `{{#link-to 'index' (query-params foo='123')}}Index{{/link-to}}`); return this.visit('/').then(() => { - this.assertComponentElement(this.firstChild.firstElementChild, { + this.assertComponentElement(this.firstChild, { tagName: 'a', attrs: { href: '/', class: classMatcher('ember-view active') }, content: 'Index' diff --git a/packages/ember-glimmer/tests/integration/mount-test.js b/packages/ember-glimmer/tests/integration/mount-test.js index dc6997b07f0..54b4f7939ee 100644 --- a/packages/ember-glimmer/tests/integration/mount-test.js +++ b/packages/ember-glimmer/tests/integration/mount-test.js @@ -83,15 +83,15 @@ moduleFor('{{mount}} test', class extends ApplicationTest { let engineInstance = getOwner(controller); assert.strictEqual(getEngineParent(engineInstance), this.applicationInstance, 'engine instance has the application instance as its parent'); - this.assertComponentElement(this.firstChild, { content: '

Chat here, dgeb

' }); + this.assertInnerHTML('

Chat here, dgeb

'); this.runTask(() => set(controller, 'username', 'chancancode')); - this.assertComponentElement(this.firstChild, { content: '

Chat here, chancancode

' }); + this.assertInnerHTML('

Chat here, chancancode

'); this.runTask(() => set(controller, 'username', 'dgeb')); - this.assertComponentElement(this.firstChild, { content: '

Chat here, dgeb

' }); + this.assertInnerHTML('

Chat here, dgeb

'); }); } @@ -155,31 +155,31 @@ moduleFor('{{mount}} test', class extends ApplicationTest { })); return this.visit('/bound-engine-name').then(() => { - this.assertComponentElement(this.firstChild, { content: '' }); + this.assertInnerHTML(''); this.runTask(() => set(controller, 'engineName', 'foo')); - this.assertComponentElement(this.firstChild, { content: '

Foo Engine

' }); + this.assertInnerHTML('

Foo Engine

'); this.runTask(() => set(controller, 'engineName', undefined)); - this.assertComponentElement(this.firstChild, { content: '' }); + this.assertInnerHTML(''); this.runTask(() => set(controller, 'engineName', 'foo')); - this.assertComponentElement(this.firstChild, { content: '

Foo Engine

' }); + this.assertInnerHTML('

Foo Engine

'); this.runTask(() => set(controller, 'engineName', 'bar')); - this.assertComponentElement(this.firstChild, { content: '

Bar Engine

' }); + this.assertInnerHTML('

Bar Engine

'); this.runTask(() => set(controller, 'engineName', 'foo')); - this.assertComponentElement(this.firstChild, { content: '

Foo Engine

' }); + this.assertInnerHTML('

Foo Engine

'); this.runTask(() => set(controller, 'engineName', null)); - this.assertComponentElement(this.firstChild, { content: '' }); + this.assertInnerHTML(''); }); } @@ -216,7 +216,7 @@ moduleFor('{{mount}} test', class extends ApplicationTest { })); return this.visit('/engine-event-dispatcher-singleton').then(() => { - this.assertComponentElement(this.firstChild, { content: '

Foo Engine: Tagless Component

' }); + this.assertInnerHTML('

Foo Engine: Tagless Component

'); let controllerOwnerEventDispatcher = getOwner(controller).lookup('event_dispatcher:main'); let taglessComponentOwnerEventDispatcher = getOwner(component).lookup('event_dispatcher:main'); @@ -248,7 +248,7 @@ if (EMBER_ENGINES_MOUNT_PARAMS) { this.addTemplate('engine-params-static', '{{mount "paramEngine" model=(hash foo="bar")}}'); return this.visit('/engine-params-static').then(() => { - this.assertComponentElement(this.firstChild, { content: '

Param Engine: bar

' }); + this.assertInnerHTML('

Param Engine: bar

'); }); } @@ -267,31 +267,31 @@ if (EMBER_ENGINES_MOUNT_PARAMS) { this.addTemplate('engine-params-bound', '{{mount "paramEngine" model=(hash foo=boundParamValue)}}'); return this.visit('/engine-params-bound').then(() => { - this.assertComponentElement(this.firstChild, { content: '

Param Engine:

' }); + this.assertInnerHTML('

Param Engine:

'); this.runTask(() => set(controller, 'boundParamValue', 'bar')); - this.assertComponentElement(this.firstChild, { content: '

Param Engine: bar

' }); + this.assertInnerHTML('

Param Engine: bar

'); this.runTask(() => set(controller, 'boundParamValue', undefined)); - this.assertComponentElement(this.firstChild, { content: '

Param Engine:

' }); + this.assertInnerHTML('

Param Engine:

'); this.runTask(() => set(controller, 'boundParamValue', 'bar')); - this.assertComponentElement(this.firstChild, { content: '

Param Engine: bar

' }); + this.assertInnerHTML('

Param Engine: bar

'); this.runTask(() => set(controller, 'boundParamValue', 'baz')); - this.assertComponentElement(this.firstChild, { content: '

Param Engine: baz

' }); + this.assertInnerHTML('

Param Engine: baz

'); this.runTask(() => set(controller, 'boundParamValue', 'bar')); - this.assertComponentElement(this.firstChild, { content: '

Param Engine: bar

' }); + this.assertInnerHTML('

Param Engine: bar

'); this.runTask(() => set(controller, 'boundParamValue', null)); - this.assertComponentElement(this.firstChild, { content: '

Param Engine:

' }); + this.assertInnerHTML('

Param Engine:

'); }); } @@ -317,7 +317,7 @@ if (EMBER_ENGINES_MOUNT_PARAMS) { this.addTemplate('engine-params-contextual-component', '{{mount "componentParamEngine" model=(hash foo=(component "foo-component"))}}'); return this.visit('/engine-params-contextual-component').then(() => { - this.assertComponentElement(this.firstChild.firstChild, { content: 'foo-component rendered! - rendered app-bar-component from the app' }); + this.assertComponentElement(this.firstChild, { content: 'foo-component rendered! - rendered app-bar-component from the app' }); }); } }); diff --git a/packages/ember/tests/component_registration_test.js b/packages/ember/tests/component_registration_test.js index 19e6f52d96d..80c7ac274e3 100644 --- a/packages/ember/tests/component_registration_test.js +++ b/packages/ember/tests/component_registration_test.js @@ -12,8 +12,8 @@ moduleFor('Application Lifecycle - Component Registration', class extends Autobo this.addTemplate('application', 'Hello world {{#expand-it}}world{{/expand-it}}'); }); - let text = this.$('div.ember-view > div.ember-view').html().trim(); - assert.equal(text, '

hello world

', 'The component is composed correctly'); + this.assertText('Hello world hello world'); + this.assertComponentElement(this.element.firstElementChild, { tagName: 'div', content: '

hello world

' }); } ['@feature(ember-glimmer-template-only-components) The helper becomes the body of the component'](assert) { @@ -24,8 +24,7 @@ moduleFor('Application Lifecycle - Component Registration', class extends Autobo this.addTemplate('application', 'Hello world {{#expand-it}}world{{/expand-it}}'); }); - let text = this.$('div.ember-view').html().trim(); - assert.equal(text, 'Hello world

hello world

', 'The component is composed correctly'); + this.assertInnerHTML('Hello world

hello world

'); } ['@test If a component is registered, it is used'](assert) { diff --git a/packages/ember/tests/routing/basic_test.js b/packages/ember/tests/routing/basic_test.js index 5b51eadc832..b03d501699c 100644 --- a/packages/ember/tests/routing/basic_test.js +++ b/packages/ember/tests/routing/basic_test.js @@ -1738,7 +1738,7 @@ QUnit.test('Child routes should render inside the application template if the ap bootApplication(); - equal(jQuery('#qunit-fixture > div').text(), 'App posts'); + equal(jQuery('#qunit-fixture').text(), 'App posts'); }); QUnit.test('The template is not re-rendered when the route\'s context changes', function() { diff --git a/packages/ember/tests/routing/toplevel_dom_test.js b/packages/ember/tests/routing/toplevel_dom_test.js index e3f68e438cd..b992b329c75 100644 --- a/packages/ember/tests/routing/toplevel_dom_test.js +++ b/packages/ember/tests/routing/toplevel_dom_test.js @@ -1,11 +1,19 @@ import { moduleFor, ApplicationTestCase } from 'internal-test-helpers'; moduleFor('Top Level DOM Structure', class extends ApplicationTestCase { - ['@test Topmost template always get an element'](assert) { + ['@feature(!ember-glimmer-remove-application-template-wrapper) Topmost template always get an element'](assert) { this.addTemplate('application', 'hello world'); return this.visit('/').then(() => { - assert.equal(this.$('> .ember-view').text(), 'hello world'); + this.assertComponentElement(this.element, { content: 'hello world' }); + }); + } + + ['@feature(ember-glimmer-remove-application-template-wrapper) Topmost template does not get an element'](assert) { + this.addTemplate('application', 'hello world'); + + return this.visit('/').then(() => { + this.assertInnerHTML('hello world'); }); } }); diff --git a/packages/internal-test-helpers/lib/test-cases/abstract-application.js b/packages/internal-test-helpers/lib/test-cases/abstract-application.js index ced04a3715b..eeb4339a4a4 100644 --- a/packages/internal-test-helpers/lib/test-cases/abstract-application.js +++ b/packages/internal-test-helpers/lib/test-cases/abstract-application.js @@ -1,13 +1,23 @@ import { compile } from 'ember-template-compiler'; -import AbstractTestCase from './abstract'; import { jQuery } from 'ember-views'; +import { EMBER_GLIMMER_REMOVE_APPLICATION_TEMPLATE_WRAPPER } from 'ember/features'; +import AbstractTestCase from './abstract'; import { runDestroy } from '../run'; export default class AbstractApplicationTestCase extends AbstractTestCase { - constructor() { - super(); - this.element = jQuery('#qunit-fixture')[0]; + get element() { + if (this._element) { + return this._element; + } else if (EMBER_GLIMMER_REMOVE_APPLICATION_TEMPLATE_WRAPPER) { + return this._element = jQuery('#qunit-fixture')[0]; + } else { + return this._element = jQuery('#qunit-fixture > div.ember-view')[0]; + } + } + + set element(element) { + this._element = element; } teardown() {