Skip to content

Commit

Permalink
Merge pull request #20776 from emberjs/feature/rfc-0931
Browse files Browse the repository at this point in the history
Inplement runtime portion of RFC #931
  • Loading branch information
ef4 authored Nov 6, 2024
2 parents cca6965 + fb1c533 commit 988dd79
Show file tree
Hide file tree
Showing 60 changed files with 6,262 additions and 4,733 deletions.
24 changes: 24 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -292,6 +292,30 @@
"@ember/runloop/index.js": "ember-source/@ember/runloop/index.js",
"@ember/service/index.js": "ember-source/@ember/service/index.js",
"@ember/template-compilation/index.js": "ember-source/@ember/template-compilation/index.js",
"@ember/template-compiler/-internal-primitives.js": "ember-source/@ember/template-compiler/-internal-primitives.js",
"@ember/template-compiler/-internal-utils.js": "ember-source/@ember/template-compiler/-internal-utils.js",
"@ember/template-compiler/index.js": "ember-source/@ember/template-compiler/index.js",
"@ember/template-compiler/lib/-internal/primitives.js": "ember-source/@ember/template-compiler/lib/-internal/primitives.js",
"@ember/template-compiler/lib/compile-options.js": "ember-source/@ember/template-compiler/lib/compile-options.js",
"@ember/template-compiler/lib/dasherize-component-name.js": "ember-source/@ember/template-compiler/lib/dasherize-component-name.js",
"@ember/template-compiler/lib/plugins/assert-against-attrs.js": "ember-source/@ember/template-compiler/lib/plugins/assert-against-attrs.js",
"@ember/template-compiler/lib/plugins/assert-against-named-outlets.js": "ember-source/@ember/template-compiler/lib/plugins/assert-against-named-outlets.js",
"@ember/template-compiler/lib/plugins/assert-input-helper-without-block.js": "ember-source/@ember/template-compiler/lib/plugins/assert-input-helper-without-block.js",
"@ember/template-compiler/lib/plugins/assert-reserved-named-arguments.js": "ember-source/@ember/template-compiler/lib/plugins/assert-reserved-named-arguments.js",
"@ember/template-compiler/lib/plugins/index.js": "ember-source/@ember/template-compiler/lib/plugins/index.js",
"@ember/template-compiler/lib/plugins/transform-action-syntax.js": "ember-source/@ember/template-compiler/lib/plugins/transform-action-syntax.js",
"@ember/template-compiler/lib/plugins/transform-each-in-into-each.js": "ember-source/@ember/template-compiler/lib/plugins/transform-each-in-into-each.js",
"@ember/template-compiler/lib/plugins/transform-each-track-array.js": "ember-source/@ember/template-compiler/lib/plugins/transform-each-track-array.js",
"@ember/template-compiler/lib/plugins/transform-in-element.js": "ember-source/@ember/template-compiler/lib/plugins/transform-in-element.js",
"@ember/template-compiler/lib/plugins/transform-quoted-bindings-into-just-bindings.js": "ember-source/@ember/template-compiler/lib/plugins/transform-quoted-bindings-into-just-bindings.js",
"@ember/template-compiler/lib/plugins/transform-resolutions.js": "ember-source/@ember/template-compiler/lib/plugins/transform-resolutions.js",
"@ember/template-compiler/lib/plugins/transform-wrap-mount-and-outlet.js": "ember-source/@ember/template-compiler/lib/plugins/transform-wrap-mount-and-outlet.js",
"@ember/template-compiler/lib/plugins/utils.js": "ember-source/@ember/template-compiler/lib/plugins/utils.js",
"@ember/template-compiler/lib/public-api.js": "ember-source/@ember/template-compiler/lib/public-api.js",
"@ember/template-compiler/lib/runtime.js": "ember-source/@ember/template-compiler/lib/runtime.js",
"@ember/template-compiler/lib/system/calculate-location-display.js": "ember-source/@ember/template-compiler/lib/system/calculate-location-display.js",
"@ember/template-compiler/lib/template.js": "ember-source/@ember/template-compiler/lib/template.js",
"@ember/template-compiler/runtime.js": "ember-source/@ember/template-compiler/runtime.js",
"@ember/template-factory/index.js": "ember-source/@ember/template-factory/index.js",
"@ember/template/index.js": "ember-source/@ember/template/index.js",
"@ember/test/adapter.js": "ember-source/@ember/test/adapter.js",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,338 @@
import { template } from '@ember/template-compiler/runtime';
import { RenderingTestCase, defineSimpleModifier, moduleFor } from 'internal-test-helpers';
import GlimmerishComponent from '../../utils/glimmerish-component';
import { on } from '@ember/modifier/on';
import { fn } from '@ember/helper';

moduleFor(
'Strict Mode - Runtime Template Compiler (explicit)',
class extends RenderingTestCase {
async '@test Can use a component in scope'() {
await this.renderComponentModule(() => {
let Foo = template('Hello, world!');

return template('<Foo />', {
scope: () => ({ Foo }),
});
});

this.assertHTML('Hello, world!');
this.assertStableRerender();
}

async '@test Can use a custom helper in scope (in append position)'() {
await this.renderComponentModule(() => {
let foo = () => 'Hello, world!';

return template('{{foo}}', {
scope: () => ({ foo }),
});
});

this.assertHTML('Hello, world!');
this.assertStableRerender();
}

async '@test Can use a custom modifier in scope'() {
await this.renderComponentModule(() => {
let foo = defineSimpleModifier((element: Element) => (element.innerHTML = 'Hello, world!'));
return template('<div {{foo}}></div>', {
scope: () => ({ foo }),
});
});

this.assertHTML('<div>Hello, world!</div>');
this.assertStableRerender();
}

async '@test Can shadow keywords'() {
await this.renderComponentModule(() => {
let each = template(`{{yield}}`);

return template(`{{#each}}Hello, world!{{/each}}`, {
scope: () => ({ each }),
});
});

this.assertHTML('Hello, world!');
this.assertStableRerender();
}

async '@test Can use constant values in ambiguous helper/component position'() {
await this.renderComponentModule(() => {
let value = 'Hello, world!';

return template(`{{value}}`, {
scope: () => ({ value }),
});
});

this.assertHTML('Hello, world!');
this.assertStableRerender();
}

async '@test Can use inline if and unless in strict mode templates'() {
await this.renderComponentModule(() => {
return template('{{if true "foo" "bar"}}{{unless true "foo" "bar"}}');
});

this.assertHTML('foobar');
this.assertStableRerender();
}

async '@test Can use a dynamic component definition'() {
await this.renderComponentModule(() => {
let Foo = template('Hello, world!');

return class extends GlimmerishComponent {
static {
template('<this.Foo />', {
component: this,
});
}

Foo = Foo;
};
});

this.assertHTML('Hello, world!');
this.assertStableRerender();
}

async '@test Can use a dynamic component definition (curly)'() {
await this.renderComponentModule(() => {
let Foo = template('Hello, world!');

return class extends GlimmerishComponent {
static {
template('{{this.Foo}}', {
component: this,
});
}

Foo = Foo;
};
});

this.assertHTML('Hello, world!');
this.assertStableRerender();
}

async '@test Can use a dynamic helper definition'() {
await this.renderComponentModule(() => {
let foo = () => 'Hello, world!';

return class extends GlimmerishComponent {
static {
template('{{this.foo}}', {
component: this,
});
}

foo = foo;
};
});

this.assertHTML('Hello, world!');
this.assertStableRerender();
}

async '@test Can use a curried dynamic helper'() {
await this.renderComponentModule(() => {
let foo = (v: string) => v;

let Foo = template('{{@value}}');

return template('<Foo @value={{helper foo "Hello, world!"}}/>', {
scope: () => ({ foo, Foo }),
});
});
this.assertHTML('Hello, world!');
this.assertStableRerender();
}

async '@test Can use a curried dynamic modifier'() {
await this.renderComponentModule(() => {
let foo = defineSimpleModifier(
(element: Element, [text]: [string]) => (element.innerHTML = text)
);

let Foo = template('<div {{@value}}></div>');

return template('<Foo @value={{modifier foo "Hello, world!"}}/>', {
scope: () => ({ foo, Foo }),
});
});
this.assertHTML('<div>Hello, world!</div>');
this.assertStableRerender();
}
}
);

moduleFor(
'Strict Mode - Runtime Template Compiler (explicit) - built ins',
class extends RenderingTestCase {
async '@test Can use Input'() {
const { Input } = await import('@ember/component');

await this.renderComponentModule(() => {
return template('<Input/>', {
scope: () => ({
Input,
}),
});
});

this.assertComponentElement(this.firstChild, {
tagName: 'input',
attrs: {
type: 'text',
class: 'ember-text-field ember-view',
},
});
this.assertStableRerender();
}

async '@test Can use Textarea'() {
const { Textarea } = await import('@ember/component');

await this.renderComponentModule(() => {
return template('<Textarea/>', {
scope: () => ({
Textarea,
}),
});
});

this.assertComponentElement(this.firstChild, {
tagName: 'textarea',
attrs: {
class: 'ember-text-area ember-view',
},
});
this.assertStableRerender();
}

async '@test Can use hash'() {
const { hash } = await import('@glimmer/runtime');

await this.renderComponentModule(() => {
return template('{{#let (hash value="Hello, world!") as |hash|}}{{hash.value}}{{/let}}', {
scope: () => ({ hash }),
});
});

this.assertHTML('Hello, world!');
this.assertStableRerender();
}

async '@test Can use array'() {
const { array } = await import('@glimmer/runtime');

await this.renderComponentModule(() => {
return template('{{#each (array "Hello, world!") as |value|}}{{value}}{{/each}}', {
scope: () => ({ array }),
});
});
this.assertHTML('Hello, world!');
this.assertStableRerender();
}

async '@test Can use concat'() {
const { concat } = await import('@glimmer/runtime');

await this.renderComponentModule(() => {
return template('{{(concat "Hello" ", " "world!")}}', {
scope: () => ({ concat }),
});
});

this.assertHTML('Hello, world!');
this.assertStableRerender();
}

async '@test Can use get'() {
const { hash, get } = await import('@glimmer/runtime');

await this.renderComponentModule(() => {
return template(
'{{#let (hash value="Hello, world!") as |hash|}}{{(get hash "value")}}{{/let}}',
{
scope: () => ({ hash, get }),
}
);
});

this.assertHTML('Hello, world!');
this.assertStableRerender();
}

async '@test Can use on and fn'(assert: QUnit['assert']) {
assert.expect(1);

await this.renderComponentModule(() => {
let handleClick = (value: unknown) => {
assert.equal(value, 123);
};

return template('<button {{on "click" (fn handleClick 123)}}>Click</button>', {
scope: () => ({ handleClick, on, fn }),
});
});

this.click('button');
}

// Test some of the additional keywords not built-in to glimmer-vm (those
// we specifically enable them when calling `precompile`)

// Ember currently uses AST plugins to implement certain features that
// glimmer-vm does not natively provide, such as {{#each-in}}, {{outlet}}
// {{mount}} and some features in {{#in-element}}. These rewrites the AST
// and insert private keywords e.g. `{{#each (-each-in)}}`. These tests
// ensures we have _some_ basic coverage for those features in strict mode.
//
// Ultimately, our test coverage for strict mode is quite inadequate. This
// is particularly important as we expect more apps to start adopting the
// feature. Ideally we would run our entire/most of our test suite against
// both strict and resolution modes, and these things would be implicitly
// covered elsewhere, but until then, these coverage are essential.

async '@test Can use each-in'() {
let obj = {
foo: 'FOO',
bar: 'BAR',
};

await this.renderComponentModule(() => {
return template('{{#each-in obj as |k v|}}[{{k}}:{{v}}]{{/each-in}}', {
scope: () => ({ obj }),
});
});

this.assertHTML('[foo:FOO][bar:BAR]');
this.assertStableRerender();
}

async '@test Can use in-element'() {
const fixture = document.querySelector('#qunit-fixture')!;
const element: HTMLTemplateElement = document.createElement('template');
element.innerHTML = '[<div id="in-element-test"></div>]';
fixture.appendChild(element.content);

const getElement = (id: string) => document.querySelector(`#${id}`)!;

await this.renderComponentModule(() => {
return template(
'{{#in-element (getElement "in-element-test")}}before{{/in-element}}after',
{
scope: () => ({ getElement }),
}
);
});

this.assertText('[before]after');
this.assertStableRerender();
}
}
);
Loading

0 comments on commit 988dd79

Please sign in to comment.