emberjs / babel-plugin-ember-template-compilation

Babel implementation of Ember's low-level template-compilation API
9 stars 11 forks source link

Accessing `this` inside a template tag in a test context #61

Open evoactivity opened 1 week ago

evoactivity commented 1 week ago

In a class backed component a template tag has access to implicit this

class FooBar extends Component {
  foo = 'bar';
  <template>{{this.foo}}</template> // <-- totally fine to access this here
}

In a test context, a template tag does not have access to implicit this

hooks.beforeEach(function () {
  class State {
    @tracked whatever;
  }
  this.instance = new State();
})

test('...', async function () {
  let context = this.instance; // <-- I can access this here
  await render(<template>
     {{context.whatever}} <-- this is ok
     {{this.instance.whatever}} <-- why can't I access this here?
  </template>);
});

This difference in behavior can be confusing.

This has been talked about before on this PR https://github.com/emberjs/babel-plugin-ember-template-compilation/pull/40

elwayman02 commented 1 week ago

Relevant context from @NullVoxPopuli:

This syntax:

const whatever = '...';

<template>

</template>

is template-only syntax.

Which uses this component manager, and has no getContext method https://github.com/glimmerjs/glimmer-vm/blob/main/packages/%40glimmer/runtime/lib/component/template-only.ts#L32

like the @glimmer/component does: https://github.com/glimmerjs/glimmer.js/blob/v1.1.2/packages/%40glimmer/component/addon/-private/base-component-manager.ts#L57

elwayman02 commented 1 week ago

The key here, in my mind, is that it would be nice if it were possible to pass a context to <template> as a general API, one application of which would be to send a TestContext to the template so we can invoke this.whatever the same way we would in the hbs format, allowing developers to utilize the established patterns of setting up shared test context in QUnit's beforeEach hook without having to do strange workarounds like variables scoped to the module function, which would be prone to state leakage especially if we wanted to parallelize individual tests in the future.

elwayman02 commented 1 week ago

Another potentially confusing thing here is that I believe the context setting for <template> only happens if it's a component class, not all classes. So, while it seems unlikely someone might try to do this (the testing pattern is far more important and useful), it does present another confusing inconsistency in the developer experience:

class FooBar {
  foo = 'bar';
  <template>{{this.foo}}</template> // <-- cannot access this here
}

class FooBar extends Component {
  foo = 'bar';
  <template>{{this.foo}}</template> // <-- totally fine to access this here
}
NullVoxPopuli commented 1 week ago

I think one way forward, would be three phase:

Of note, something that would still work through this process, is the XState component manager:

const Toggle = createMachine(...);

<template>
  <Toggle as |blah|>
  </Toggle>
</template>

because you can't extend it in any way that would give you access to a this anyway.

This probably requires an RFC