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); };