Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -463,6 +463,56 @@
this.assertText('[before]after');
this.assertStableRerender();
}

async '@test Can access private fields in templates'() {
await this.renderComponentModule(() => {
return class extends GlimmerishComponent {
#count = 0;

Check failure on line 470 in packages/@ember/-internals/glimmer/tests/integration/components/runtime-template-compiler-implicit-test.ts

View workflow job for this annotation

GitHub Actions / tests / Linting

'#count' is defined but never used

#increment = () => {

Check failure on line 472 in packages/@ember/-internals/glimmer/tests/integration/components/runtime-template-compiler-implicit-test.ts

View workflow job for this annotation

GitHub Actions / tests / Linting

'#increment' is defined but never used
this.#count++;
};

static {
template('<p>Count: {{this.#count}}</p><button {{on "click" this.#increment}}>Increment</button>', {
component: this,
eval() {
return eval(arguments[0]);
},
});
}
};
});

this.assertHTML('<p>Count: 0</p><button>Increment</button>');
this.assertStableRerender();
}

async '@test Private field methods work with on modifier'() {
await this.renderComponentModule(() => {
hide(on);

return class extends GlimmerishComponent {
#message = 'Hello';

Check failure on line 496 in packages/@ember/-internals/glimmer/tests/integration/components/runtime-template-compiler-implicit-test.ts

View workflow job for this annotation

GitHub Actions / tests / Linting

'#message' is defined but never used

#updateMessage = () => {

Check failure on line 498 in packages/@ember/-internals/glimmer/tests/integration/components/runtime-template-compiler-implicit-test.ts

View workflow job for this annotation

GitHub Actions / tests / Linting

'#updateMessage' is defined but never used
this.#message = 'Updated!';
};

static {
template('<button type="button" {{on "click" this.#updateMessage}}>Click</button>', {
component: this,
eval() {
return eval(arguments[0]);
},
});
}
};
});

this.assertHTML('<button type="button">Click</button>');
this.assertStableRerender();
}
}
);

Expand Down
12 changes: 12 additions & 0 deletions packages/@ember/template-compiler/lib/compile-options.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
25 changes: 19 additions & 6 deletions packages/@ember/template-compiler/lib/template.ts
Original file line number Diff line number Diff line change
Expand Up @@ -128,7 +128,7 @@ export interface ExplicitClassOptions<C extends ComponentClass>
* ### 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
Expand Down Expand Up @@ -213,12 +213,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(
* '<button {{on "click" this.#increment}}>{{this.#count}}</button>',
* { component: this },
* eval() { return arguments[0] }
* );
* }
* }
* ```
*/
export type ImplicitClassOptions<C extends ComponentClass> = BaseClassTemplateOptions<C> &
ImplicitEvalOption;
Expand Down
Loading