Skip to content
Draft
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
@@ -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(
Expand Down Expand Up @@ -336,3 +336,133 @@
}
}
);

moduleFor(
'Strict Mode - Runtime Template Compiler (explicit) - private fields',
class extends RenderingTestCase {
async '@test Can render a private field value'() {
await this.renderComponentModule(() => {
class TestComponent extends GlimmerishComponent {
#greeting = 'Hello, world!';

static {
template('<p>{{this.#greeting}}</p>', {
component: this,
scope: (instance) => ({

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

View workflow job for this annotation

GitHub Actions / tests / Type Checking (current version)

Parameter 'instance' implicitly has an 'any' type.
'#greeting': instance ? instance.#greeting : undefined,
}),
});
}
}
return TestComponent;
});

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

async '@test Can render multiple private fields'() {
await this.renderComponentModule(() => {
class TestComponent extends GlimmerishComponent {
#firstName = 'Jane';

#lastName = 'Doe';

static {
template('<p>{{this.#firstName}} {{this.#lastName}}</p>', {
component: this,
scope: (instance?: InstanceType<typeof TestComponent>) => ({
'#firstName': instance ? instance.#firstName : undefined,
'#lastName': instance ? instance.#lastName : undefined,
}),
});
}
}
return TestComponent;
});

this.assertHTML('<p>Jane Doe</p>');
this.assertStableRerender();
}

async '@test Can use private field method with on modifier'() {
await this.renderComponentModule(() => {
class TestComponent extends GlimmerishComponent {
// eslint-disable-next-line no-unused-private-class-members
#message = 'Hello';

#updateMessage = () => {
this.#message = 'Updated!';
};

static {
template('<button type="button" {{on "click" this.#updateMessage}}>Click</button>', {
component: this,
scope: (instance?: InstanceType<typeof TestComponent>) => ({
on,
'#updateMessage': instance ? instance.#updateMessage : undefined,
}),
});
}
}
return TestComponent;
});

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

async '@test Can mix private fields with local scope variables'() {
await this.renderComponentModule(() => {
let Greeting = template('<span>{{yield}}</span>');

class TestComponent extends GlimmerishComponent {
#name = 'Ember';

static {
template('<Greeting>Hello, {{this.#name}}!</Greeting>', {
component: this,
scope: (instance?: InstanceType<typeof TestComponent>) => ({
Greeting,
'#name': instance ? instance.#name : undefined,
}),
});
}
}
return TestComponent;
});

this.assertHTML('<span>Hello, Ember!</span>');
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);
};

class TestComponent extends GlimmerishComponent {
#secretValue = 42;

static {
template('<button {{on "click" (fn checkValue this.#secretValue)}}>Click</button>', {
component: this,
scope: (instance?: InstanceType<typeof TestComponent>) => ({
on,
fn,
checkValue,
'#secretValue': instance ? instance.#secretValue : undefined,
}),
});
}
}
return TestComponent;
});

this.click('button');
}
}
);
Original file line number Diff line number Diff line change
Expand Up @@ -463,6 +463,63 @@ moduleFor(
this.assertText('[before]after');
this.assertStableRerender();
}

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(
'<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 {
// 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('<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
28 changes: 28 additions & 0 deletions packages/@ember/-internals/metal/lib/property_get.ts
Original file line number Diff line number Diff line change
Expand Up @@ -102,11 +102,39 @@ 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 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: look up getter closures stored on the class
// constructor. Private fields cannot be accessed via bracket notation,
// so we rely on closures created inside the class's static block.
if (keyName.length > 0 && keyName[0] === '#') {
const getters = (obj as any)?.constructor?.[PRIVATE_FIELD_GETTERS] as
| Record<string, (obj: object) => unknown>
| undefined;

if (getters?.[keyName]) {
const 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') {
Expand Down
Loading
Loading