From 1ee5f73a608614e0800bd067d0dca7d5a04cacee Mon Sep 17 00:00:00 2001 From: Aniruddh-14 Date: Sat, 22 Nov 2025 13:24:57 +0530 Subject: [PATCH 1/7] Update runtime-template-compiler-implicit-test.ts Update compile-options.ts Enhance documentation for private fields support Updated documentation to reflect support for private fields in templates. Enable access to private properties in Glimmer templates This fix enables access to private class properties in Glimmer templates, resolving issues with private fields being treated as undefined. It includes updates to the template compiler and documentation to support private field identifiers. Create test-private-fields.html Remove private fields test HTML file Update FIX_SUMMARY.md Delete FIX_SUMMARY.md Delete test-private-fields.html --- ...runtime-template-compiler-implicit-test.ts | 50 +++++++++++++++++++ .../template-compiler/lib/compile-options.ts | 12 +++++ .../@ember/template-compiler/lib/template.ts | 25 +++++++--- 3 files changed, 81 insertions(+), 6 deletions(-) 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 5d4e975acc1..544fe30f710 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 @@ -463,6 +463,56 @@ moduleFor( this.assertText('[before]after'); this.assertStableRerender(); } + + async '@test Can access private fields in templates'() { + await this.renderComponentModule(() => { + return class extends GlimmerishComponent { + #count = 0; + + #increment = () => { + this.#count++; + }; + + static { + template('

Count: {{this.#count}}

', { + component: this, + eval() { + return eval(arguments[0]); + }, + }); + } + }; + }); + + this.assertHTML('

Count: 0

'); + this.assertStableRerender(); + } + + async '@test Private field methods work with on modifier'() { + await this.renderComponentModule(() => { + hide(on); + + return class extends GlimmerishComponent { + #message = 'Hello'; + + #updateMessage = () => { + this.#message = 'Updated!'; + }; + + static { + template('', { + component: this, + eval() { + return eval(arguments[0]); + }, + }); + } + }; + }); + + this.assertHTML(''); + this.assertStableRerender(); + } } ); diff --git a/packages/@ember/template-compiler/lib/compile-options.ts b/packages/@ember/template-compiler/lib/compile-options.ts index a5459e412c4..48a2433cbb3 100644 --- a/packages/@ember/template-compiler/lib/compile-options.ts +++ b/packages/@ember/template-compiler/lib/compile-options.ts @@ -110,7 +110,19 @@ type Evaluator = (value: string) => unknown; // https://tc39.es/ecma262/2020/#prod-IdentifierName const IDENT = /^[\p{ID_Start}$_][\p{ID_Continue}$_\u200C\u200D]*$/u; +// https://tc39.es/ecma262/#prod-PrivateIdentifier +const PRIVATE_IDENT = /^#[\p{ID_Start}$_][\p{ID_Continue}$_\u200C\u200D]*$/u; + function inScope(variable: string, evaluator: Evaluator): boolean { + // Check if it's a private field syntax + if (PRIVATE_IDENT.exec(variable)) { + // Private fields are always considered "in scope" when referenced in a template + // since they are class members, not lexical variables. The actual access check + // will happen at runtime when the template accesses `this.#fieldName`. + // We just need to ensure they're treated as valid identifiers and passed through. + return true; + } + // If the identifier is not a valid JS identifier, it's definitely not in scope if (!IDENT.exec(variable)) { return false; diff --git a/packages/@ember/template-compiler/lib/template.ts b/packages/@ember/template-compiler/lib/template.ts index ca83645457e..36fa145940d 100644 --- a/packages/@ember/template-compiler/lib/template.ts +++ b/packages/@ember/template-compiler/lib/template.ts @@ -129,7 +129,7 @@ export interface ExplicitClassOptions< * ### The Technical Requirements of the `eval` Option * * The `eval` function is passed a single parameter that is a JavaScript - * identifier. This will be extended in the future to support private fields. + * identifier or a private field identifier (starting with `#`). * * Since keywords in JavaScript are contextual (e.g. `await` and `yield`), the * parameter might be a keyword. The `@ember/template-compiler/runtime` expects @@ -214,12 +214,25 @@ export type ImplicitTemplateOnlyOptions = BaseTemplateOptions & ImplicitEvalOpti * } * ``` * - * ## Note on Private Fields + * ## Private Fields Support * - * The current implementation of `@ember/template-compiler` does not support - * private fields, but once the Handlebars parser adds support for private field - * syntax and it's implemented in the Glimmer compiler, the implicit form should - * be able to support them. + * The implicit form now supports private fields. You can reference private + * class members in templates using the `this.#fieldName` syntax: + * + * ```ts + * class MyComponent extends Component { + * #count = 0; + * #increment = () => this.#count++; + * + * static { + * template( + * '', + * { component: this }, + * eval() { return arguments[0] } + * ); + * } + * } + * ``` */ export type ImplicitClassOptions = BaseClassTemplateOptions & ImplicitEvalOption; From bd68a6185c277c097baa70aa131412942b1079ff Mon Sep 17 00:00:00 2001 From: NullVoxPopuli <199018+NullVoxPopuli@users.noreply.github.com> Date: Wed, 11 Feb 2026 17:23:41 -0500 Subject: [PATCH 2/7] Fix private field access for the runtime compiler --- .../-internals/metal/lib/property_get.ts | 25 +++++++++++++ .../@ember/template-compiler/lib/template.ts | 37 +++++++++++++++++++ 2 files changed, 62 insertions(+) diff --git a/packages/@ember/-internals/metal/lib/property_get.ts b/packages/@ember/-internals/metal/lib/property_get.ts index ef86dd3b8d4..8e170e888c5 100644 --- a/packages/@ember/-internals/metal/lib/property_get.ts +++ b/packages/@ember/-internals/metal/lib/property_get.ts @@ -102,11 +102,36 @@ export function get(obj: unknown, keyName: string): unknown { return isPath(keyName) ? _getPath(obj, keyName) : _getProp(obj, keyName); } +/** + * Well-known symbol key for private field getter closures stored on a + * component class. Must match the symbol defined in + * `@ember/template-compiler/lib/template.ts`. + */ +const PRIVATE_FIELD_GETTERS = Symbol.for('ember:private-field-getters'); + export function _getProp(obj: unknown, keyName: string) { if (obj == null) { return; } + // Private field access: use the getter closure stored on the class + // constructor instead of bracket notation (which cannot access # fields). + if (keyName.length > 0 && keyName[0] === '#') { + const getters = (obj as any)?.constructor?.[PRIVATE_FIELD_GETTERS] as + | Record unknown> + | undefined; + if (getters?.[keyName]) { + let value = getters[keyName]!(obj as object); + + if (isTracking()) { + consumeTag(tagFor(obj, keyName)); + } + + return value; + } + return undefined; + } + let value: unknown; if (typeof obj === 'object' || typeof obj === 'function') { diff --git a/packages/@ember/template-compiler/lib/template.ts b/packages/@ember/template-compiler/lib/template.ts index 36fa145940d..ce2e1ada30b 100644 --- a/packages/@ember/template-compiler/lib/template.ts +++ b/packages/@ember/template-compiler/lib/template.ts @@ -246,6 +246,29 @@ export function template( templateString: string, options: ExplicitClassOptions | ImplicitClassOptions | BaseClassTemplateOptions ): C; +/** + * Well-known symbol key used to store private field getter closures on a + * component class. The getters are created via `eval` inside the class's + * `static` block so they retain access to the class's private field brand. + * + * Shape: `Record unknown>` + */ +export const PRIVATE_FIELD_GETTERS = Symbol.for('ember:private-field-getters'); + +/** + * Extract private field names (e.g. `#count`, `#increment`) referenced + * via `this.#field` paths in a Handlebars template string. + */ +function extractPrivateFields(templateString: string): string[] { + const re = /this\.#([\p{ID_Start}$_][\p{ID_Continue}$_\u200C\u200D]*)/gu; + const fields = new Set(); + let match: RegExpExecArray | null; + while ((match = re.exec(templateString)) !== null) { + fields.add(`#${match[1]}`); + } + return [...fields]; +} + export function template( templateString: string, providedOptions?: BaseTemplateOptions | BaseClassTemplateOptions @@ -256,6 +279,20 @@ export function template( const normalizedOptions = compileOptions(options); const component = normalizedOptions.component ?? templateOnly(); + // If the template references private fields (this.#field) and we have an + // eval function from the class scope, create getter closures that can + // access the private fields. These are stored on the component class and + // looked up by _getProp at runtime. + const privateFields = extractPrivateFields(templateString); + if (privateFields.length > 0 && evaluate !== evaluator) { + const getterEntries = privateFields.map((f) => `${JSON.stringify(f)}: (obj) => obj.${f}`); + const getters = evaluate(`({${getterEntries.join(', ')}})`) as Record< + string, + (obj: object) => unknown + >; + (component as any)[PRIVATE_FIELD_GETTERS] = getters; + } + const source = glimmerPrecompile(templateString, normalizedOptions); const template = templateFactory(evaluate(`(${source})`) as SerializedTemplateWithLazyBlock); From e2e754b3c317c43cf3c0f6e00c3b4f9f501de8b1 Mon Sep 17 00:00:00 2001 From: NullVoxPopuli <199018+NullVoxPopuli@users.noreply.github.com> Date: Wed, 11 Feb 2026 17:39:51 -0500 Subject: [PATCH 3/7] ope --- .../@ember/template-compiler/lib/template.ts | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/packages/@ember/template-compiler/lib/template.ts b/packages/@ember/template-compiler/lib/template.ts index ce2e1ada30b..9869b6336f0 100644 --- a/packages/@ember/template-compiler/lib/template.ts +++ b/packages/@ember/template-compiler/lib/template.ts @@ -237,15 +237,6 @@ export type ImplicitTemplateOnlyOptions = BaseTemplateOptions & ImplicitEvalOpti export type ImplicitClassOptions = BaseClassTemplateOptions & ImplicitEvalOption; -export function template( - templateString: string, - options?: ExplicitTemplateOnlyOptions | ImplicitTemplateOnlyOptions -): TemplateOnlyComponent; - -export function template( - templateString: string, - options: ExplicitClassOptions | ImplicitClassOptions | BaseClassTemplateOptions -): C; /** * Well-known symbol key used to store private field getter closures on a * component class. The getters are created via `eval` inside the class's @@ -269,6 +260,15 @@ function extractPrivateFields(templateString: string): string[] { return [...fields]; } +export function template( + templateString: string, + options?: ExplicitTemplateOnlyOptions | ImplicitTemplateOnlyOptions +): TemplateOnlyComponent; + +export function template( + templateString: string, + options: ExplicitClassOptions | ImplicitClassOptions | BaseClassTemplateOptions +): C; export function template( templateString: string, providedOptions?: BaseTemplateOptions | BaseClassTemplateOptions From bc65e30338b5d024ca47dde64c337130da252091 Mon Sep 17 00:00:00 2001 From: NullVoxPopuli <199018+NullVoxPopuli@users.noreply.github.com> Date: Wed, 11 Feb 2026 17:47:03 -0500 Subject: [PATCH 4/7] lint:fix --- ...runtime-template-compiler-implicit-test.ts | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) 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 544fe30f710..b4b186a5db3 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 @@ -467,19 +467,24 @@ moduleFor( async '@test Can access private fields in templates'() { await this.renderComponentModule(() => { return class extends GlimmerishComponent { + // eslint-disable-next-line no-unused-private-class-members #count = 0; + // eslint-disable-next-line no-unused-private-class-members #increment = () => { this.#count++; }; static { - template('

Count: {{this.#count}}

', { - component: this, - eval() { - return eval(arguments[0]); - }, - }); + template( + '

Count: {{this.#count}}

', + { + component: this, + eval() { + return eval(arguments[0]); + }, + } + ); } }; }); @@ -493,8 +498,10 @@ moduleFor( hide(on); return class extends GlimmerishComponent { + // eslint-disable-next-line no-unused-private-class-members #message = 'Hello'; + // eslint-disable-next-line no-unused-private-class-members #updateMessage = () => { this.#message = 'Updated!'; }; From 7cb6e48818af466e3803d789d484ef1c96b6f793 Mon Sep 17 00:00:00 2001 From: NullVoxPopuli <199018+NullVoxPopuli@users.noreply.github.com> Date: Thu, 12 Feb 2026 01:30:44 -0500 Subject: [PATCH 5/7] explicit scope tests --- ...runtime-template-compiler-explicit-test.ts | 133 ++++++++++++++++++ 1 file changed, 133 insertions(+) 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..19d02668df8 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 @@ -336,3 +336,136 @@ moduleFor( } } ); + +moduleFor( + 'Strict Mode - Runtime Template Compiler (explicit) - private fields', + class extends RenderingTestCase { + async '@test Can render a private field value'() { + await this.renderComponentModule(() => { + return class extends GlimmerishComponent { + // eslint-disable-next-line no-unused-private-class-members + #greeting = 'Hello, world!'; + + static { + template('

{{this.#greeting}}

', { + component: this, + scope: (instance: any) => ({ + '#greeting': instance.#greeting, + }), + }); + } + }; + }); + + this.assertHTML('

Hello, world!

'); + this.assertStableRerender(); + } + + async '@test Can render multiple private fields'() { + await this.renderComponentModule(() => { + return class extends GlimmerishComponent { + // eslint-disable-next-line no-unused-private-class-members + #firstName = 'Jane'; + // eslint-disable-next-line no-unused-private-class-members + #lastName = 'Doe'; + + static { + template('

{{this.#firstName}} {{this.#lastName}}

', { + component: this, + scope: (instance: any) => ({ + '#firstName': instance.#firstName, + '#lastName': instance.#lastName, + }), + }); + } + }; + }); + + this.assertHTML('

Jane Doe

'); + this.assertStableRerender(); + } + + async '@test Can use private field method with on modifier'() { + await this.renderComponentModule(() => { + return class extends GlimmerishComponent { + // eslint-disable-next-line no-unused-private-class-members + #message = 'Hello'; + + // eslint-disable-next-line no-unused-private-class-members + #updateMessage = () => { + this.#message = 'Updated!'; + }; + + static { + template('', { + component: this, + scope: (instance: any) => ({ + on, + '#updateMessage': instance.#updateMessage, + }), + }); + } + }; + }); + + this.assertHTML(''); + this.assertStableRerender(); + } + + async '@test Can mix private fields with local scope variables'() { + await this.renderComponentModule(() => { + let Greeting = template('{{yield}}'); + + return class extends GlimmerishComponent { + // eslint-disable-next-line no-unused-private-class-members + #name = 'Ember'; + + static { + template('Hello, {{this.#name}}!', { + component: this, + scope: (instance: any) => ({ + Greeting, + '#name': instance.#name, + }), + }); + } + }; + }); + + this.assertHTML('Hello, Ember!'); + this.assertStableRerender(); + } + + async '@test Can use private field with on modifier and fn helper'(assert: QUnit['assert']) { + assert.expect(1); + + await this.renderComponentModule(() => { + let checkValue = (value: number) => { + assert.equal(value, 42); + }; + + return class extends GlimmerishComponent { + // eslint-disable-next-line no-unused-private-class-members + #secretValue = 42; + + static { + template( + '', + { + component: this, + scope: (instance: any) => ({ + on, + fn, + checkValue, + '#secretValue': instance.#secretValue, + }), + } + ); + } + }; + }); + + this.click('button'); + } + } +); From b6330327725e533e0b2d85a90b209055ac977409 Mon Sep 17 00:00:00 2001 From: NullVoxPopuli <199018+NullVoxPopuli@users.noreply.github.com> Date: Thu, 12 Feb 2026 02:18:42 -0500 Subject: [PATCH 6/7] Checkpoint --- ...runtime-template-compiler-explicit-test.ts | 14 +-- .../@ember/template-compiler/lib/template.ts | 106 +++++++++++++++--- 2 files changed, 97 insertions(+), 23 deletions(-) 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 19d02668df8..69ab43e8198 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,7 +1,7 @@ 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 { on } from '@ember/modifier'; import { fn } from '@ember/helper'; moduleFor( @@ -350,7 +350,7 @@ moduleFor( template('

{{this.#greeting}}

', { component: this, scope: (instance: any) => ({ - '#greeting': instance.#greeting, + '#greeting': instance?.#greeting, }), }); } @@ -373,8 +373,8 @@ moduleFor( template('

{{this.#firstName}} {{this.#lastName}}

', { component: this, scope: (instance: any) => ({ - '#firstName': instance.#firstName, - '#lastName': instance.#lastName, + '#firstName': instance?.#firstName, + '#lastName': instance?.#lastName, }), }); } @@ -401,7 +401,7 @@ moduleFor( component: this, scope: (instance: any) => ({ on, - '#updateMessage': instance.#updateMessage, + '#updateMessage': instance?.#updateMessage, }), }); } @@ -425,7 +425,7 @@ moduleFor( component: this, scope: (instance: any) => ({ Greeting, - '#name': instance.#name, + '#name': instance?.#name, }), }); } @@ -457,7 +457,7 @@ moduleFor( on, fn, checkValue, - '#secretValue': instance.#secretValue, + '#secretValue': instance?.#secretValue, }), } ); diff --git a/packages/@ember/template-compiler/lib/template.ts b/packages/@ember/template-compiler/lib/template.ts index 9869b6336f0..11ee63a91b6 100644 --- a/packages/@ember/template-compiler/lib/template.ts +++ b/packages/@ember/template-compiler/lib/template.ts @@ -87,7 +87,7 @@ export interface ExplicitTemplateOnlyOptions extends BaseTemplateOptions { * static { * template('{{this.#greeting}}, {{@place}}!', * { component: this }, - * scope: (instance) => ({ '#greeting': instance.#greeting }), + * scope: (instance) => ({ '#greeting': instance?.#greeting }), * ); * } * } @@ -274,23 +274,84 @@ export function template( providedOptions?: BaseTemplateOptions | BaseClassTemplateOptions ): object { const options: EmberPrecompileOptions = { strictMode: true, ...providedOptions }; + + const privateFields = extractPrivateFields(templateString); + + // When using the explicit scope form with private fields, the scope + // function has the shape: + // + // (instance) => ({ Section, '#secret': instance?.#secret }) + // + // The optional chaining (`?.`) on the instance parameter is required so + // that calling the scope function without an instance (at compile time) + // returns `undefined` for private field entries instead of crashing. + // + // At compile time we use the scope to: + // 1. Discover which variables are in lexical scope (non-private entries) + // 2. Build an evaluator that can resolve the compiled wire format + // + // Private fields are not resolved through the evaluator. Instead, at + // runtime they are accessed via PRIVATE_FIELD_GETTERS stored on the + // component class, which _getProp looks up when it encounters a `#` key. + let originalScopeWithInstance: ((instance: any) => Record) | undefined; + + if ( + privateFields.length > 0 && + options.scope && + !options.eval && + options.component + ) { + originalScopeWithInstance = options.scope as (instance: any) => Record; + const origScope = originalScopeWithInstance; + + // Wrap the scope function so that compile-time callers (compileOptions + // and buildEvaluator) receive only non-private entries. The private + // field names are injected as placeholders so lexicalScope recognises + // them as in-scope. + options.scope = () => { + // Call with no instance — private fields evaluate to `undefined` + // thanks to optional chaining in the scope function. + const fullScope = origScope(undefined); + const safeScope: Record = {}; + for (const key of Object.keys(fullScope)) { + if (key[0] !== '#') { + safeScope[key] = fullScope[key]; + } + } + // Inject private field names so lexicalScope reports them as in-scope. + for (const field of privateFields) { + safeScope[field] = true; + } + return safeScope; + }; + } + const evaluate = buildEvaluator(options); const normalizedOptions = compileOptions(options); const component = normalizedOptions.component ?? templateOnly(); - // If the template references private fields (this.#field) and we have an - // eval function from the class scope, create getter closures that can - // access the private fields. These are stored on the component class and - // looked up by _getProp at runtime. - const privateFields = extractPrivateFields(templateString); - if (privateFields.length > 0 && evaluate !== evaluator) { - const getterEntries = privateFields.map((f) => `${JSON.stringify(f)}: (obj) => obj.${f}`); - const getters = evaluate(`({${getterEntries.join(', ')}})`) as Record< - string, - (obj: object) => unknown - >; - (component as any)[PRIVATE_FIELD_GETTERS] = getters; + // If the template references private fields (this.#field), create getter + // closures that can access the private fields at runtime. These are stored + // on the component class and looked up by _getProp. + if (privateFields.length > 0) { + if (originalScopeWithInstance) { + // Explicit form: build getters from the scope(instance) function. + const scopeFn = originalScopeWithInstance; + const getters: Record unknown> = {}; + for (const field of privateFields) { + getters[field] = (obj: object) => scopeFn(obj)[field]; + } + (component as any)[PRIVATE_FIELD_GETTERS] = getters; + } else if (evaluate !== evaluator) { + // Implicit (eval) form: generate getter closures via eval. + const getterEntries = privateFields.map((f) => `${JSON.stringify(f)}: (obj) => obj.${f}`); + const getters = evaluate(`({${getterEntries.join(', ')}})`) as Record< + string, + (obj: object) => unknown + >; + (component as any)[PRIVATE_FIELD_GETTERS] = getters; + } } const source = glimmerPrecompile(templateString, normalizedOptions); @@ -319,10 +380,23 @@ function buildEvaluator(options: Partial | undefined) { return evaluator; } - return (source: string) => { - const argNames = Object.keys(scope); - const argValues = Object.values(scope); + // Filter out private field entries (#-prefixed keys) — those are not + // lexical variables and cannot be used as Function parameter names. + // Private fields are handled separately via PRIVATE_FIELD_GETTERS. + const argNames: string[] = []; + const argValues: unknown[] = []; + for (const [key, value] of Object.entries(scope)) { + if (key[0] !== '#') { + argNames.push(key); + argValues.push(value); + } + } + if (argNames.length === 0) { + return evaluator; + } + + return (source: string) => { return new Function(...argNames, `return (${source})`)(...argValues); }; } From dc9b2957f3c2edf5c88fd1fd3ff2d84622925a32 Mon Sep 17 00:00:00 2001 From: NullVoxPopuli <199018+NullVoxPopuli@users.noreply.github.com> Date: Thu, 12 Feb 2026 03:22:14 -0500 Subject: [PATCH 7/7] Fix types --- ...runtime-template-compiler-explicit-test.ts | 47 ++++++++++--------- .../@ember/template-compiler/lib/template.ts | 20 ++++---- 2 files changed, 36 insertions(+), 31 deletions(-) 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 69ab43e8198..5c74f98a722 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 @@ -342,19 +342,20 @@ moduleFor( class extends RenderingTestCase { async '@test Can render a private field value'() { await this.renderComponentModule(() => { - return class extends GlimmerishComponent { + class TestComponent extends GlimmerishComponent { // eslint-disable-next-line no-unused-private-class-members #greeting = 'Hello, world!'; static { template('

{{this.#greeting}}

', { component: this, - scope: (instance: any) => ({ - '#greeting': instance?.#greeting, + scope: (instance) => ({ + '#greeting': instance ? instance.#greeting : undefined, }), }); } - }; + } + return TestComponent; }); this.assertHTML('

Hello, world!

'); @@ -363,7 +364,7 @@ moduleFor( async '@test Can render multiple private fields'() { await this.renderComponentModule(() => { - return class extends GlimmerishComponent { + class TestComponent extends GlimmerishComponent { // eslint-disable-next-line no-unused-private-class-members #firstName = 'Jane'; // eslint-disable-next-line no-unused-private-class-members @@ -372,13 +373,14 @@ moduleFor( static { template('

{{this.#firstName}} {{this.#lastName}}

', { component: this, - scope: (instance: any) => ({ - '#firstName': instance?.#firstName, - '#lastName': instance?.#lastName, + scope: (instance?: InstanceType) => ({ + '#firstName': instance ? instance.#firstName : undefined, + '#lastName': instance ? instance.#lastName : undefined, }), }); } - }; + } + return TestComponent; }); this.assertHTML('

Jane Doe

'); @@ -387,7 +389,7 @@ moduleFor( async '@test Can use private field method with on modifier'() { await this.renderComponentModule(() => { - return class extends GlimmerishComponent { + class TestComponent extends GlimmerishComponent { // eslint-disable-next-line no-unused-private-class-members #message = 'Hello'; @@ -399,13 +401,14 @@ moduleFor( static { template('', { component: this, - scope: (instance: any) => ({ + scope: (instance?: InstanceType) => ({ on, - '#updateMessage': instance?.#updateMessage, + '#updateMessage': instance ? instance.#updateMessage : undefined, }), }); } - }; + } + return TestComponent; }); this.assertHTML(''); @@ -416,20 +419,21 @@ moduleFor( await this.renderComponentModule(() => { let Greeting = template('{{yield}}'); - return class extends GlimmerishComponent { + class TestComponent extends GlimmerishComponent { // eslint-disable-next-line no-unused-private-class-members #name = 'Ember'; static { template('Hello, {{this.#name}}!', { component: this, - scope: (instance: any) => ({ + scope: (instance?: InstanceType) => ({ Greeting, - '#name': instance?.#name, + '#name': instance ? instance.#name : undefined, }), }); } - }; + } + return TestComponent; }); this.assertHTML('Hello, Ember!'); @@ -444,7 +448,7 @@ moduleFor( assert.equal(value, 42); }; - return class extends GlimmerishComponent { + class TestComponent extends GlimmerishComponent { // eslint-disable-next-line no-unused-private-class-members #secretValue = 42; @@ -453,16 +457,17 @@ moduleFor( '', { component: this, - scope: (instance: any) => ({ + scope: (instance?: InstanceType) => ({ on, fn, checkValue, - '#secretValue': instance?.#secretValue, + '#secretValue': instance ? instance.#secretValue : undefined, }), } ); } - }; + } + return TestComponent; }); this.click('button'); diff --git a/packages/@ember/template-compiler/lib/template.ts b/packages/@ember/template-compiler/lib/template.ts index 11ee63a91b6..4022ecbc825 100644 --- a/packages/@ember/template-compiler/lib/template.ts +++ b/packages/@ember/template-compiler/lib/template.ts @@ -87,7 +87,7 @@ export interface ExplicitTemplateOnlyOptions extends BaseTemplateOptions { * static { * template('{{this.#greeting}}, {{@place}}!', * { component: this }, - * scope: (instance) => ({ '#greeting': instance?.#greeting }), + * scope: (instance) => ({ '#greeting': instance ? instance.#greeting : undefined }), * ); * } * } @@ -96,7 +96,7 @@ export interface ExplicitTemplateOnlyOptions extends BaseTemplateOptions { export interface ExplicitClassOptions< C extends ComponentClass, > extends BaseClassTemplateOptions { - scope(instance?: InstanceType): Record; + scope: (instance: InstanceType) => Record; } /** @@ -262,12 +262,17 @@ function extractPrivateFields(templateString: string): string[] { export function template( templateString: string, - options?: ExplicitTemplateOnlyOptions | ImplicitTemplateOnlyOptions + options?: (ExplicitTemplateOnlyOptions | ImplicitTemplateOnlyOptions) & { component?: never } ): TemplateOnlyComponent; export function template( templateString: string, - options: ExplicitClassOptions | ImplicitClassOptions | BaseClassTemplateOptions + options: ExplicitClassOptions +): C; + +export function template( + templateString: string, + options: (ImplicitClassOptions | BaseClassTemplateOptions & { scope?: never }) ): C; export function template( templateString: string, @@ -295,12 +300,7 @@ export function template( // component class, which _getProp looks up when it encounters a `#` key. let originalScopeWithInstance: ((instance: any) => Record) | undefined; - if ( - privateFields.length > 0 && - options.scope && - !options.eval && - options.component - ) { + if (privateFields.length > 0 && options.scope && !options.eval && options.component) { originalScopeWithInstance = options.scope as (instance: any) => Record; const origScope = originalScopeWithInstance;