diff --git a/packages/@ember/-internals/glimmer/tests/integration/this-binding-test.gjs b/packages/@ember/-internals/glimmer/tests/integration/this-binding-test.gjs
new file mode 100644
index 00000000000..05e77c7664b
--- /dev/null
+++ b/packages/@ember/-internals/glimmer/tests/integration/this-binding-test.gjs
@@ -0,0 +1,326 @@
+import { set } from '@ember/object';
+import { RenderingTestCase, moduleFor, runTask } from 'internal-test-helpers';
+import { fn } from '@ember/helper';
+import { helperCapabilities, setHelperManager } from '@glimmer/manager';
+import { Component } from '../utils/helpers';
+
+moduleFor(
+ 'Automatic this-binding for class methods directly called from the template',
+ class extends RenderingTestCase {
+ ['@test class method'](assert) {
+ let instance;
+ let seenThis;
+
+ class Demo extends Component {
+ constructor(...args) {
+ super(...args);
+ instance = this;
+ }
+
+ foo() {
+ seenThis = this;
+ }
+
+ {{(this.foo)}}
+ }
+
+ this.render(``, { Demo });
+
+ assert.ok(instance);
+ assert.strictEqual(seenThis, instance);
+ }
+
+ ['@test maintains binding through property chain'](assert) {
+ let innerInstance;
+ let seenThis;
+
+ class Inner {
+ constructor() {
+ innerInstance = this;
+ }
+
+ method() {
+ seenThis = this;
+ }
+ }
+
+ class Demo extends Component {
+ constructor(...args) {
+ super(...args);
+ this.obj = new Inner();
+ }
+
+ {{ (this.obj.method) }}
+ }
+
+ this.render(``, { Demo });
+
+ assert.ok(innerInstance);
+ assert.strictEqual(seenThis, innerInstance);
+ }
+
+ ['@test replacing the base object uses correct `this`'](assert) {
+ let instance;
+ let seenThis;
+
+ class Inner {
+ method() {
+ seenThis = this;
+ }
+ }
+
+ class Demo extends Component {
+ constructor(...args) {
+ super(...args);
+ instance = this;
+ this.obj = new Inner();
+ }
+
+ {{ (this.obj.method) }}
+ }
+
+ this.render(``, { Demo });
+
+ let first = instance.obj;
+ assert.strictEqual(seenThis, first);
+
+ let second = new Inner();
+ runTask(() => set(instance, 'obj', second));
+
+ assert.strictEqual(seenThis, second);
+ }
+
+ ['@test a method passed as an argument stays bound to its original object'](assert) {
+ let instance;
+ let seenThis;
+
+ class Child extends Component {
+ {{ (@cb) }}
+ }
+
+ class Demo extends Component {
+ constructor(...args) {
+ super(...args);
+ instance = this;
+ }
+
+ foo() {
+ seenThis = this;
+ }
+
+
+ }
+
+ this.render(``, { Demo });
+
+ assert.strictEqual(seenThis, instance);
+ }
+
+ ['@test #let block param preserves this binding on method access'](assert) {
+ let innerInstance;
+ let seenThis;
+ let receivedArg;
+
+ class Inner {
+ constructor() {
+ innerInstance = this;
+ }
+
+ method(arg) {
+ seenThis = this;
+ receivedArg = arg;
+ }
+ }
+
+ class Demo extends Component {
+ constructor(...args) {
+ super(...args);
+ this.obj = new Inner();
+ }
+
+
+ {{#let this.obj as |o|}}
+ {{(o.method "did it")}}
+ {{/let}}
+
+ }
+
+ this.render(``, { Demo });
+
+ assert.ok(innerInstance);
+ assert.strictEqual(seenThis, innerInstance);
+ assert.strictEqual(receivedArg, 'did it', 'argument is passed through to the method');
+ }
+
+ ['@test each over array of objects preserves this binding on item methods'](assert) {
+ let seenPairs = [];
+
+ class Item {
+ constructor(name) {
+ this.name = name;
+ }
+
+ greet() {
+ seenPairs.push({ self: this, name: this.name });
+ }
+ }
+
+ let items = [new Item('alice'), new Item('bob'), new Item('carol')];
+
+ class Demo extends Component {
+ constructor(...args) {
+ super(...args);
+ this.items = items;
+ }
+
+
+
+ {{#each this.items as |item|}}
+ - {{item.greet}}: {{item.name}}
+ {{/each}}
+
+
+ }
+
+ this.render(``, { Demo });
+
+ assert.strictEqual(seenPairs.length, 3);
+ assert.strictEqual(seenPairs[0].self, items[0], 'alice: this is the Item instance');
+ assert.strictEqual(seenPairs[0].name, 'alice', 'alice: this.name is correct');
+ assert.strictEqual(seenPairs[1].self, items[1], 'bob: this is the Item instance');
+ assert.strictEqual(seenPairs[1].name, 'bob', 'bob: this.name is correct');
+ assert.strictEqual(seenPairs[2].self, items[2], 'carol: this is the Item instance');
+ assert.strictEqual(seenPairs[2].name, 'carol', 'carol: this.name is correct');
+ }
+
+ ['@test already-bound functions are unaffected'](assert) {
+ let instance;
+ let seenThis;
+
+ class Demo extends Component {
+ constructor(...args) {
+ super(...args);
+ instance = this;
+ }
+
+ foo = () => {
+ seenThis = this;
+ };
+
+ {{this.foo}}
+ }
+
+ this.render(``, { Demo });
+
+ assert.strictEqual(seenThis, instance);
+ }
+
+ ['@test (fn) on methods still behaves appropriately'](assert) {
+ let instance;
+ let seenThis;
+
+ class Demo extends Component {
+ constructor(...args) {
+ super(...args);
+ instance = this;
+ }
+
+ foo = () => {
+ seenThis = this;
+ };
+
+
+ {{#let (fn this.foo) as |fned|}}
+ {{ (fned) }}
+ {{/let}}
+
+ }
+
+ this.render(``, { Demo });
+
+ assert.strictEqual(seenThis, instance);
+ }
+
+ ['@test a function with a custom helper manager read off a path keeps its manager'](assert) {
+ let sawDefinition;
+
+ class MyHelperManager {
+ capabilities = helperCapabilities('3.23', { hasValue: true });
+
+ createHelper(definition, args) {
+ return { definition, args };
+ }
+
+ getValue({ definition }) {
+ sawDefinition = definition;
+ return 'CUSTOM_MANAGER_RAN';
+ }
+
+ getDebugName() {
+ return 'my-custom-helper';
+ }
+ }
+
+ function customHelper() {
+ return 'PLAIN_FN_RAN';
+ }
+ setHelperManager(() => new MyHelperManager(), customHelper);
+
+ class Inner {
+ customHelper = customHelper;
+ }
+
+ class Demo extends Component {
+ constructor(...args) {
+ super(...args);
+ this.obj = new Inner();
+ }
+
+ {{(this.obj.customHelper)}}
+ }
+
+ this.render(``, { Demo });
+
+ this.assertText('CUSTOM_MANAGER_RAN');
+ assert.strictEqual(
+ sawDefinition,
+ customHelper,
+ // i.e.: a.foo !== a.foo.bind(a)
+ 'the custom manager received the original function, not a `.bind()` wrapper'
+ );
+ }
+
+ ['@test a plain method passed through (fn) is not this-bound'](assert) {
+ let innerInstance;
+ let seenThis = 'UNSET';
+
+ class Inner {
+ constructor() {
+ innerInstance = this;
+ }
+
+ method() {
+ seenThis = this;
+ }
+ }
+
+ class Demo extends Component {
+ constructor(...args) {
+ super(...args);
+ this.obj = new Inner();
+ }
+
+ {{#let (fn this.obj.method) as |f|}}{{(f)}}{{/let}}
+ }
+
+ this.render(``, { Demo });
+
+ assert.notStrictEqual(seenThis, 'UNSET', 'the method actually ran');
+ assert.notStrictEqual(
+ seenThis,
+ innerInstance,
+ 'a plain method passed through `(fn)` is NOT bound to the object it was read from — `(fn)` value-passes the function and only the direct template call site binds'
+ );
+ }
+ }
+);
diff --git a/packages/@glimmer/interfaces/lib/references.d.ts b/packages/@glimmer/interfaces/lib/references.d.ts
index 9bb1415eb57..86abdaa9d39 100644
--- a/packages/@glimmer/interfaces/lib/references.d.ts
+++ b/packages/@glimmer/interfaces/lib/references.d.ts
@@ -26,4 +26,5 @@ export interface Reference {
debugLabel?: string | false | undefined;
compute: Nullable<() => T>;
children: null | Map;
+ parent: Nullable;
}
diff --git a/packages/@glimmer/manager/lib/internal/api.ts b/packages/@glimmer/manager/lib/internal/api.ts
index c5b5135e1ae..bf077812c93 100644
--- a/packages/@glimmer/manager/lib/internal/api.ts
+++ b/packages/@glimmer/manager/lib/internal/api.ts
@@ -235,6 +235,16 @@ export function hasInternalModifierManager(definition: object): boolean {
);
}
+/**
+ * Whether a value has a helper manager explicitly associated with it, as
+ * opposed to a plain function (which falls back to the default function helper
+ * manager). Used to decide whether a function invoked from a path expression
+ * may be safely re-bound to the object it was read from.
+ */
+export function hasCustomHelperManager(definition: object): boolean {
+ return getManager(HELPER_MANAGERS, definition) !== undefined;
+}
+
function hasDefaultComponentManager(_definition: object): boolean {
return false;
}
diff --git a/packages/@glimmer/reference/lib/reference.ts b/packages/@glimmer/reference/lib/reference.ts
index c7232d0469a..6e0aa154994 100644
--- a/packages/@glimmer/reference/lib/reference.ts
+++ b/packages/@glimmer/reference/lib/reference.ts
@@ -42,6 +42,12 @@ class ReferenceImpl implements Reference {
public children: Nullable