Skip to content

Commit

Permalink
Merge pull request #15974 from emberjs/template-only-component
Browse files Browse the repository at this point in the history
Implement template-only components RFC
  • Loading branch information
chancancode committed Dec 13, 2017
2 parents 04ba9ae + 0fd6f64 commit 9536e13
Show file tree
Hide file tree
Showing 13 changed files with 295 additions and 29 deletions.
5 changes: 5 additions & 0 deletions FEATURES.md
Expand Up @@ -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-template-only-components`

Use Glimmer Components semantics for template-only components per
[RFC](https://github.com/emberjs/rfcs/pull/278).

* `ember-module-unification`

Introduces support for Module Unification
Expand Down
1 change: 1 addition & 0 deletions features.json
Expand Up @@ -4,6 +4,7 @@
"ember-libraries-isregistered": null,
"ember-improved-instrumentation": null,
"ember-glimmer-named-arguments": null,
"ember-glimmer-template-only-components": null,
"ember-routing-router-service": true,
"ember-engines-mount-params": true,
"ember-module-unification": null,
Expand Down
11 changes: 6 additions & 5 deletions packages/ember-glimmer/externs.d.ts
@@ -1,9 +1,10 @@
declare module 'ember/features' {
export const EMBER_MODULE_UNIFICATION: any;
export const GLIMMER_CUSTOM_COMPONENT_MANAGER: any;
export const EMBER_ENGINES_MOUNT_PARAMS: any;
export const EMBER_GLIMMER_DETECT_BACKTRACKING_RERENDER: any;
export const MANDATORY_SETTER: any;
export const EMBER_MODULE_UNIFICATION: boolean | null;
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_TEMPLATE_ONLY_COMPONENTS: boolean | null;
export const MANDATORY_SETTER: boolean | null;
}

declare module 'ember-env-flags' {
Expand Down
44 changes: 44 additions & 0 deletions packages/ember-glimmer/lib/component-managers/template-only.ts
@@ -0,0 +1,44 @@
import { VersionedPathReference } from '@glimmer/reference';
import { CompiledDynamicProgram, ComponentDefinition, NULL_REFERENCE } from '@glimmer/runtime';
import { Opaque } from '@glimmer/util';
import Environment from '../environment';
import { OwnedTemplate, WrappedTemplateFactory } from '../template';
import AbstractManager from './abstract';

class TemplateOnlyComponentLayoutCompiler {
static id = 'template-only';

constructor(public template: WrappedTemplateFactory) {
}

compile(builder: any) {
// TODO: use fromLayout
builder.wrapLayout(this.template);
}
}

export default class TemplateOnlyComponentManager extends AbstractManager<null> {
create(): null {
return null;
}

layoutFor({ template }: TemplateOnlyComponentDefinition, _: null, env: Environment): CompiledDynamicProgram {
return env.getCompiledBlock(TemplateOnlyComponentLayoutCompiler, template);
}

getSelf(): VersionedPathReference<Opaque> {
return NULL_REFERENCE;
}

getDestructor() {
return null;
}
}

const MANAGER = new TemplateOnlyComponentManager();

export class TemplateOnlyComponentDefinition extends ComponentDefinition<null> {
constructor(name: string, public template: OwnedTemplate) {
super(name, MANAGER, null);
}
}
10 changes: 8 additions & 2 deletions packages/ember-glimmer/lib/environment.ts
Expand Up @@ -35,6 +35,9 @@ import {
import {
CurlyComponentDefinition,
} from './component-managers/curly';
import {
TemplateOnlyComponentDefinition,
} from './component-managers/template-only';
import {
populateMacros,
} from './syntax';
Expand Down Expand Up @@ -73,6 +76,7 @@ import installPlatformSpecificProtocolForURL from './protocol-for-url';

import {
EMBER_MODULE_UNIFICATION,
EMBER_GLIMMER_TEMPLATE_ONLY_COMPONENTS,
GLIMMER_CUSTOM_COMPONENT_MANAGER,
} from 'ember/features';
import { Container, OwnedTemplate, WrappedTemplateFactory } from './template';
Expand Down Expand Up @@ -110,7 +114,7 @@ export default class Environment extends GlimmerEnvironment {
name: string;
source: string;
owner: Container;
}, CurlyComponentDefinition | undefined>;
}, CurlyComponentDefinition | TemplateOnlyComponentDefinition | undefined>;
private _templateCache: Cache<{
Template: WrappedTemplateFactory | OwnedTemplate;
owner: Container;
Expand All @@ -130,7 +134,9 @@ export default class Environment extends GlimmerEnvironment {
this._definitionCache = new Cache(2000, ({ name, source, owner }) => {
let { component: componentFactory, layout } = lookupComponent(owner, name, { source });
let customManager: any;
if (componentFactory || layout) {
if (EMBER_GLIMMER_TEMPLATE_ONLY_COMPONENTS && layout && !componentFactory) {
return new TemplateOnlyComponentDefinition(name, layout);
} else if (componentFactory || layout) {
if (GLIMMER_CUSTOM_COMPONENT_MANAGER) {
let managerId = layout && layout.meta.managerId;

Expand Down
6 changes: 5 additions & 1 deletion packages/ember-glimmer/lib/setup-registry.ts
Expand Up @@ -17,6 +17,7 @@ import OutletTemplate from './templates/outlet';
import RootTemplate from './templates/root';
import OutletView from './views/outlet';
import loc from './helpers/loc';
import { EMBER_GLIMMER_TEMPLATE_ONLY_COMPONENTS } from 'ember/features';

interface Registry {
injection(name: string, name2: string, name3: string): void;
Expand Down Expand Up @@ -73,5 +74,8 @@ export function setupEngineRegistry(registry: Registry) {
registry.register('component:-text-area', TextArea);
registry.register('component:-checkbox', Checkbox);
registry.register('component:link-to', LinkToComponent);
registry.register(P`component:-default`, Component);

if (!EMBER_GLIMMER_TEMPLATE_ONLY_COMPONENTS) {
registry.register(P`component:-default`, Component);
}
}
Expand Up @@ -50,18 +50,6 @@ moduleFor('Components test: curly components', class extends RenderingTest {
this.assertComponentElement(this.firstChild, { content: 'hello' });
}

['@test it can render a template only component']() {
this.registerComponent('foo-bar', { template: 'hello' });

this.render('{{foo-bar}}');

this.assertComponentElement(this.firstChild, { content: 'hello' });

this.runTask(() => this.rerender());

this.assertComponentElement(this.firstChild, { content: 'hello' });
}

['@test it can have a custom id and it is not bound']() {
this.registerComponent('foo-bar', { template: '{{id}} {{elementId}}' });

Expand Down
Expand Up @@ -263,7 +263,7 @@ if (EMBER_MODULE_UNIFICATION) {
return new LocalLookupTestResolver();
}

registerComponent(name, { ComponentClass = null, template = null }) {
registerComponent(name, { ComponentClass = Component, template = null }) {
let { resolver } = this;

if (ComponentClass) {
Expand Down
@@ -0,0 +1,196 @@
import { moduleFor, RenderingTest } from '../../utils/test-case';
import { classes } from '../../utils/test-helpers';
import { EMBER_GLIMMER_TEMPLATE_ONLY_COMPONENTS } from 'ember/features';

class TemplateOnlyComponentsTest extends RenderingTest {
registerComponent(name, template) {
super.registerComponent(name, { template, ComponentClass: null });
}
}

if (EMBER_GLIMMER_TEMPLATE_ONLY_COMPONENTS) {
moduleFor('Components test: template-only components (glimmer components)', class extends TemplateOnlyComponentsTest {
['@test it can render a template-only component']() {
this.registerComponent('foo-bar', 'hello');

this.render('{{foo-bar}}');

this.assertInnerHTML('hello');

this.assertStableRerender();
}

['@feature(ember-glimmer-named-arguments) it can render named arguments']() {
this.registerComponent('foo-bar', '|{{@foo}}|{{@bar}}|');

this.render('{{foo-bar foo=foo bar=bar}}', {
foo: 'foo', bar: 'bar'
});

this.assertInnerHTML('|foo|bar|');

this.assertStableRerender();

this.runTask(() => this.context.set('foo', 'FOO'));

this.assertInnerHTML('|FOO|bar|');

this.runTask(() => this.context.set('bar', 'BAR'));

this.assertInnerHTML('|FOO|BAR|');

this.runTask(() => this.context.setProperties({ foo: 'foo', bar: 'bar' }));

this.assertInnerHTML('|foo|bar|');
}

['@test it does not reflected arguments as properties']() {
this.registerComponent('foo-bar', '|{{foo}}|{{this.bar}}|');

this.render('{{foo-bar foo=foo bar=bar}}', {
foo: 'foo', bar: 'bar'
});

this.assertInnerHTML('|||');

this.assertStableRerender();

this.runTask(() => this.context.set('foo', 'FOO'));

this.assertInnerHTML('|||');

this.runTask(() => this.context.set('bar', null));

this.assertInnerHTML('|||');

this.runTask(() => this.context.setProperties({ foo: 'foo', bar: 'bar' }));

this.assertInnerHTML('|||');
}

['@test it does not have curly component features']() {
this.registerComponent('foo-bar', 'hello');

this.render('{{foo-bar tagName="p" class=class}}', {
class: 'foo bar'
});

this.assertInnerHTML('hello');


this.assertStableRerender();

this.runTask(() => this.context.set('class', 'foo'));

this.assertInnerHTML('hello');

this.runTask(() => this.context.set('class', null));

this.assertInnerHTML('hello');

this.runTask(() => this.context.set('class', 'foo bar'));

this.assertInnerHTML('hello');
}
});
} else {
moduleFor('Components test: template-only components (curly components)', class extends TemplateOnlyComponentsTest {
['@test it can render a template-only component']() {
this.registerComponent('foo-bar', 'hello');

this.render('{{foo-bar}}');

this.assertComponentElement(this.firstChild, { content: 'hello' });

this.assertStableRerender();
}

['@feature(ember-glimmer-named-arguments) it can render named arguments']() {
this.registerComponent('foo-bar', '|{{@foo}}|{{@bar}}|');

this.render('{{foo-bar foo=foo bar=bar}}', {
foo: 'foo', bar: 'bar'
});

this.assertComponentElement(this.firstChild, { content: '|foo|bar|' });

this.assertStableRerender();

this.runTask(() => this.context.set('foo', 'FOO'));

this.assertComponentElement(this.firstChild, { content: '|FOO|bar|' });

this.runTask(() => this.context.set('bar', 'BAR'));

this.assertComponentElement(this.firstChild, { content: '|FOO|BAR|' });

this.runTask(() => this.context.setProperties({ foo: 'foo', bar: 'bar' }));

this.assertComponentElement(this.firstChild, { content: '|foo|bar|' });
}

['@test it renders named arguments as reflected properties']() {
this.registerComponent('foo-bar', '|{{foo}}|{{this.bar}}|');

this.render('{{foo-bar foo=foo bar=bar}}', {
foo: 'foo', bar: 'bar'
});

this.assertComponentElement(this.firstChild, { content: '|foo|bar|' });

this.assertStableRerender();

this.runTask(() => this.context.set('foo', 'FOO'));

this.assertComponentElement(this.firstChild, { content: '|FOO|bar|' });

this.runTask(() => this.context.set('bar', null));

this.assertComponentElement(this.firstChild, { content: '|FOO||' });

this.runTask(() => this.context.setProperties({ foo: 'foo', bar: 'bar' }));

this.assertComponentElement(this.firstChild, { content: '|foo|bar|' });
}

['@test it has curly component features']() {
this.registerComponent('foo-bar', 'hello');

this.render('{{foo-bar tagName="p" class=class}}', {
class: 'foo bar'
});

this.assertComponentElement(this.firstChild, {
tagName: 'p',
attrs: { class: classes('foo bar ember-view') },
content: 'hello'
});

this.assertStableRerender();

this.runTask(() => this.context.set('class', 'foo'));

this.assertComponentElement(this.firstChild, {
tagName: 'p',
attrs: { class: classes('foo ember-view') },
content: 'hello'
});

this.runTask(() => this.context.set('class', null));

this.assertComponentElement(this.firstChild, {
tagName: 'p',
attrs: { class: classes('ember-view') },
content: 'hello'
});

this.runTask(() => this.context.set('class', 'foo bar'));

this.assertComponentElement(this.firstChild, {
tagName: 'p',
attrs: { class: classes('foo bar ember-view') },
content: 'hello'
});
}
});
}
14 changes: 11 additions & 3 deletions packages/ember-views/lib/utils/lookup-component.js
@@ -1,5 +1,9 @@
import { privatize as P } from 'container';
import { EMBER_MODULE_UNIFICATION } from 'ember/features';
import {
EMBER_MODULE_UNIFICATION,
EMBER_GLIMMER_TEMPLATE_ONLY_COMPONENTS
} from 'ember/features';
import { ENV } from 'ember-environment';

function lookupModuleUnificationComponentPair(componentLookup, owner, name, options) {
let localComponent = componentLookup.componentFor(name, owner, options);
Expand All @@ -19,7 +23,11 @@ function lookupModuleUnificationComponentPair(componentLookup, owner, name, opti
return { layout: null, component: localComponent };
}

let defaultComponentFactory = owner.factoryFor(P`component:-default`);
let defaultComponentFactory = null;

if (!EMBER_GLIMMER_TEMPLATE_ONLY_COMPONENTS) {
defaultComponentFactory = owner.factoryFor(P`component:-default`);
}

if (!localAndUniqueComponent && localAndUniqueLayout) {
return { layout: localLayout, component: defaultComponentFactory };
Expand All @@ -39,7 +47,7 @@ function lookupComponentPair(componentLookup, owner, name, options) {

let result = { layout, component };

if (layout && !component) {
if (!EMBER_GLIMMER_TEMPLATE_ONLY_COMPONENTS && layout && !component) {
result.component = owner.factoryFor(P`component:-default`);
}

Expand Down

0 comments on commit 9536e13

Please sign in to comment.