diff --git a/packages/@ember/-internals/glimmer/tests/integration/components/runtime-template-compiler-explicit-test.ts b/packages/@ember/-internals/glimmer/tests/integration/components/runtime-template-compiler-explicit-test.ts
index 99dd11c8a8d..6d0625a37db 100644
--- a/packages/@ember/-internals/glimmer/tests/integration/components/runtime-template-compiler-explicit-test.ts
+++ b/packages/@ember/-internals/glimmer/tests/integration/components/runtime-template-compiler-explicit-test.ts
@@ -1,5 +1,6 @@
+import { tracked } from '@ember/-internals/metal';
import { template } from '@ember/template-compiler/runtime';
-import { RenderingTestCase, defineSimpleModifier, moduleFor } from 'internal-test-helpers';
+import { RenderingTestCase, defineSimpleModifier, moduleFor, runTask } from 'internal-test-helpers';
import GlimmerishComponent from '../../utils/glimmerish-component';
import { on } from '@ember/modifier/on';
import { fn } from '@ember/helper';
@@ -20,6 +21,63 @@ moduleFor(
this.assertStableRerender();
}
+ async '@test can derive the template string from tracked'(assert: Assert) {
+ class State {
+ @tracked str = `hello there`;
+
+ get component() {
+ assert.step('get component');
+ return template(this.str);
+ }
+ }
+ let state = new State();
+
+ await this.renderComponentModule(() => {
+ return template('', { scope: () => ({ state }) });
+ });
+
+ this.assertHTML('hello there');
+ this.assertStableRerender();
+ assert.verifySteps(['get component']);
+
+ runTask(() => (state.str += '!'));
+
+ this.assertHTML('hello there!');
+ this.assertStableRerender();
+ assert.verifySteps(['get component']);
+ }
+
+ async '@test can have tracked data in the scope bag - and changes to that scope bag dont re-compile'(
+ assert: Assert
+ ) {
+ class State {
+ @tracked str = `hello there`;
+
+ get component() {
+ assert.step('get component');
+ return template(`{{greeting}}`, {
+ scope: () => {
+ return { greeting: this.str };
+ },
+ });
+ }
+ }
+ let state = new State();
+
+ await this.renderComponentModule(() => {
+ return template('', { scope: () => ({ state }) });
+ });
+
+ assert.verifySteps(['get component']);
+ this.assertHTML('hello there');
+ this.assertStableRerender();
+
+ runTask(() => (state.str += '!'));
+ assert.verifySteps([]);
+ this.assertHTML('hello there!');
+ this.assertStableRerender();
+ }
+
async '@test Can use a custom helper in scope (in append position)'() {
await this.renderComponentModule(() => {
let foo = () => 'Hello, world!';
diff --git a/packages/@ember/-internals/glimmer/tests/integration/components/runtime-template-compiler-implicit-test.ts b/packages/@ember/-internals/glimmer/tests/integration/components/runtime-template-compiler-implicit-test.ts
index 3aaf14d29f4..7d35a2fab58 100644
--- a/packages/@ember/-internals/glimmer/tests/integration/components/runtime-template-compiler-implicit-test.ts
+++ b/packages/@ember/-internals/glimmer/tests/integration/components/runtime-template-compiler-implicit-test.ts
@@ -1,5 +1,6 @@
+import { tracked } from '@ember/-internals/metal';
import { template } from '@ember/template-compiler/runtime';
-import { RenderingTestCase, defineSimpleModifier, moduleFor } from 'internal-test-helpers';
+import { RenderingTestCase, defineSimpleModifier, moduleFor, runTask } from 'internal-test-helpers';
import GlimmerishComponent from '../../utils/glimmerish-component';
import { on } from '@ember/modifier/on';
import { fn } from '@ember/helper';
@@ -7,6 +8,58 @@ import { fn } from '@ember/helper';
moduleFor(
'Strict Mode - Runtime Template Compiler (implicit)',
class extends RenderingTestCase {
+ async '@test can have in-scope tracked data'(assert: Assert) {
+ class State {
+ @tracked str = `hello there`;
+
+ get component() {
+ assert.step('get component');
+
+ let getStr = () => {
+ assert.step('getStr()');
+ return this.str;
+ };
+
+ hide(getStr);
+
+ return template(`{{ (getStr) }}`, {
+ eval() {
+ return eval(arguments[0]);
+ },
+ });
+ }
+ }
+
+ let state = new State();
+
+ await this.renderComponentModule(() => {
+ return template('', {
+ eval() {
+ assert.step('eval');
+ return eval(arguments[0]);
+ },
+ });
+ });
+
+ this.assertHTML('hello there');
+ this.assertStableRerender();
+ assert.verifySteps([
+ // for every value in the component, for eevry node traversed in the compiler
+ 'eval', // precompileJSON -> ... ElementNode -> ... -> lexicalScope -> isScope('state', ...)
+ 'eval', // "..."
+ 'eval', // "..."
+ 'eval', // creating the templateFactory
+ 'get component',
+ 'getStr()',
+ ]);
+
+ runTask(() => (state.str += '!'));
+
+ this.assertHTML('hello there!');
+ this.assertStableRerender();
+ assert.verifySteps(['getStr()']);
+ }
+
async '@test Can use a component in scope'() {
await this.renderComponentModule(() => {
let Foo = template('Hello, world!', {
diff --git a/packages/@ember/template-compiler/lib/template.ts b/packages/@ember/template-compiler/lib/template.ts
index 7140656fffd..4d31b51f857 100644
--- a/packages/@ember/template-compiler/lib/template.ts
+++ b/packages/@ember/template-compiler/lib/template.ts
@@ -1,3 +1,4 @@
+import { untrack } from '@glimmer/validator';
import templateOnly, { type TemplateOnlyComponent } from '@ember/component/template-only';
import { precompile as glimmerPrecompile } from '@glimmer/compiler';
import type { SerializedTemplateWithLazyBlock } from '@glimmer/interfaces';
@@ -236,18 +237,21 @@ export function template(
templateString: string,
providedOptions?: BaseTemplateOptions | BaseClassTemplateOptions
): object {
+ // When figuring out how to compile the template, we don't want to engage
+ // with auto-tracking in a way that would cause the template
+ // to be re-rendered when the intended-to-be-lazily-accessesd scope bag's contents changes.
+ //
+ // This is why we use our reactivity-evading utility untrack() here
const options: EmberPrecompileOptions = { strictMode: true, ...providedOptions };
- const evaluate = buildEvaluator(options);
+ const evaluate = untrack(() => buildEvaluator(options));
- const normalizedOptions = compileOptions(options);
+ const normalizedOptions = untrack(() => compileOptions(options));
const component = normalizedOptions.component ?? templateOnly();
- queueMicrotask(() => {
- const source = glimmerPrecompile(templateString, normalizedOptions);
- const template = templateFactory(evaluate(`(${source})`) as SerializedTemplateWithLazyBlock);
+ const source = glimmerPrecompile(templateString, normalizedOptions);
+ const template = templateFactory(evaluate(`(${source})`) as SerializedTemplateWithLazyBlock);
- setComponentTemplate(template, component);
- });
+ setComponentTemplate(template, component);
return component;
}
@@ -264,15 +268,15 @@ function buildEvaluator(options: Partial | undefined) {
if (options.eval) {
return options.eval;
} else {
- const scope = options.scope?.();
+ let scope = options.scope?.();
if (!scope) {
return evaluator;
}
return (source: string) => {
- const argNames = Object.keys(scope);
- const argValues = Object.values(scope);
+ const argNames = Object.keys(scope ?? {});
+ const argValues = Object.values(scope ?? {});
return new Function(...argNames, `return (${source})`)(...argValues);
};