Skip to content

Commit

Permalink
Merge pull request #553 from typed-ember/helper-and-modifier
Browse files Browse the repository at this point in the history
  • Loading branch information
dfreeman committed Apr 14, 2023
2 parents c2ce26c + 0893423 commit 3501e8b
Show file tree
Hide file tree
Showing 33 changed files with 878 additions and 629 deletions.
2 changes: 1 addition & 1 deletion docs/contents.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@
* [Routes and Controllers](ember/routes-and-controllers.md)
* [Template-Only Components](ember/template-only-components.md)
* [Rendering Tests](ember/rendering-tests.md)
* [Contextual Components](ember/contextual-components.md)
* [Using Addons](ember/using-addons.md)
* [Authoring Addons](ember/authoring-addons.md)
* [GlimmerX](using-glint/glimmerx/README.md)
Expand All @@ -24,6 +23,7 @@
* [Template Components](glimmerx/template-components.md)
* [Glint with JavaScript](with-js.md)
* [@glint Directives](directives.md)
* [Glint Types](glint-types.md)
* [Migrating](migrating.md)
* [Diagnosing Common Error Messages](diagnosing-common-error-messages.md)
* [Known Limitations](known-limitations.md)
2 changes: 1 addition & 1 deletion docs/diagnosing-common-error-messages.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ declare module '@glint/environment-ember-loose/registry' {
{{yield (component 'my-component' someArg=true)}}
```

In that case, you can use the `ComponentLike` or `WithBoundArgs` helpers as discussed in [Contextual Components](ember/contextual-components.md):
In that case, you can use the `ComponentLike` or `WithBoundArgs` types as discussed in [Glint Types](./glint-types.md):

```typescript
import Component from '@glimmer/component';
Expand Down
55 changes: 0 additions & 55 deletions docs/ember/contextual-components.md

This file was deleted.

81 changes: 81 additions & 0 deletions docs/glint-types.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
## `ComponentLike`, `HelperLike` and `ModifierLike`

While we often work in terms of specific implementations of components, helpers and modifiers, when we're using e.g. `MyComponent` in a template, it doesn't matter whether `MyComponent` is a template-only component, or a subclass of `@glimmer/component` or is a completely different object with a [custom component manager](https://github.com/emberjs/rfcs/blob/master/text/0213-custom-components.md).

To account for this, the `@glint/template` package provides a set of more general types: the `ComponentLike`, `HelperLike` and `ModifierLike` types describe _any_ value that is usable as the respective type of entity in a template.

For example, in Ember all of the following values are `ComponentLike`:

- a subclass of `@glimmer/component`
- a subclass of `@ember/component`
- the return value of `templateOnlyComponent()` from `@ember/component/template-only`
- a `<template>` expression in [a `.gts` file](https://github.com/emberjs/rfcs/blob/master/text/0779-first-class-component-templates.md)
- the result of a `{{component ...}}` expression in a template

These types each accept signatures in the same format that the base classes for [components](./ember/component-signatures.md) and [helpers/modifiers](./ember/helper-and-modifier-signatures.md) do.

## `WithBoundArgs` and `WithBoundPositionals`

When you yield a "contextual component" (or helper or modifier), you need some way to declare the type of that value in the signature of the yielding component.

```handlebars
{{yield (hash banner=(component "some-banner" kind="warning"))}}
```

The return value from `{{component}}` component isn't the actual `SomeBanner` class—it won't have e.g. any of `SomeBanner`'s static members, and it also no longer requires a `@kind` arg, since a default value has been set as part of the `(component)` invocation.

We could use `ComponentLike` to describe the type of this value:

```typescript
import { ComponentLike } from '@glint/template';
import { SomeBannerSignature } from './some-banner';

interface MyComponentSignature {
Blocks: {
default: [{
banner: ComponentLike<{
Element: SomeBannerSignature['Element'];
Blocks: SomeBannerSignature['Blocks'];
Args:
Omit<SomeBannerSignature['Args'], 'kind'>
& { kind?: SomeBannerSignature['Args']['kind'] };
}>;
}];
};
}
```

However, that's quite a lot of boilerplate to essentially express "it's like `SomeBanner` except `kind` is already set". Instead, you can use the `WithBoundArgs` type to express the same thing:

```typescript
import { WithBoundArgs } from '@glint/template';
import SomeBanner from './some-banner';

interface MyComponentSignature {
Blocks: {
default: [{
banner: WithBoundArgs<typeof SomeBanner, 'kind'>;
}];
};
}
```

If you had pre-bound multiple named args, you could union them together with the `|` type operator, e.g. `'kind' | 'title'`.

Similarly, when working with a component/helper/modifier where you're pre-binding positional arguments, you can use `WithBoundPositionals` to indicate to downstream consumers that those arguments are already set:

```handlebars
{{yield (hash greetChris=(helper greetHelper "Chris"))}}
```

```typescript
interface MyComponentSignature {
Blocks: {
default: [{
greetChris: WithBoundPositionals<typeof greetHelper, 1>
}];
};
}
```

Where `WithBoundArgs` accepts the names of the pre-bound arguments, `WithBoundPositionals` accepts the number of positional arguments that are pre-bound, since binding a positional argument with `{{component}}`/`{{modifier}}`/`{{helper}}` sets that argument in a way that downstream users can't override.
Original file line number Diff line number Diff line change
Expand Up @@ -699,9 +699,7 @@ describe('Language Server: Diagnostic Augmentation', () => {
"code": 2769,
"message": "Unknown component name 'foo'. If this isn't a typo, you may be missing a registry entry for this name; see the Template Registry page in the Glint documentation for more details.
No overload matches this call.
Overload 1 of 6, '(component: keyof Globals): void | LetKeyword | ComponentKeyword<Globals> | ConcatHelper | FnHelper | ... 19 more ... | WithKeyword', gave the following error.
Argument of type '\\"foo\\"' is not assignable to parameter of type 'keyof Globals'.
Overload 2 of 6, '(component: keyof Globals | null | undefined): void | LetKeyword | ComponentKeyword<Globals> | ConcatHelper | ... 21 more ... | null', gave the following error.
The last overload gave the following error.
Argument of type '\\"foo\\"' is not assignable to parameter of type 'keyof Globals | null | undefined'.",
"range": {
"end": {
Expand All @@ -719,11 +717,9 @@ describe('Language Server: Diagnostic Augmentation', () => {
},
{
"code": 2769,
"message": "The type of this expression doesn't appear to be a valid value to pass the {{component}} helper. If possible, you may need to give the expression a narrower type, for example \`'component-a' | 'component-b'\` rather than \`string\`.
"message": "The type of this expression doesn't appear to be a valid value to pass the {{component}} helper. If possible, you may need to give the expression a narrower type, for example \`'thing-a' | 'thing-b'\` rather than \`string\`.
No overload matches this call.
Overload 1 of 6, '(component: keyof Globals): void | LetKeyword | ComponentKeyword<Globals> | ConcatHelper | FnHelper | ... 19 more ... | WithKeyword', gave the following error.
Argument of type '\\"bar\\"' is not assignable to parameter of type 'keyof Globals'.
Overload 2 of 6, '(component: keyof Globals | null | undefined): void | LetKeyword | ComponentKeyword<Globals> | ConcatHelper | ... 21 more ... | null', gave the following error.
The last overload gave the following error.
Argument of type '\\"bar\\"' is not assignable to parameter of type 'keyof Globals | null | undefined'.",
"range": {
"end": {
Expand All @@ -741,11 +737,9 @@ describe('Language Server: Diagnostic Augmentation', () => {
},
{
"code": 2769,
"message": "The type of this expression doesn't appear to be a valid value to pass the {{component}} helper. If possible, you may need to give the expression a narrower type, for example \`'component-a' | 'component-b'\` rather than \`string\`.
"message": "The type of this expression doesn't appear to be a valid value to pass the {{component}} helper. If possible, you may need to give the expression a narrower type, for example \`'thing-a' | 'thing-b'\` rather than \`string\`.
No overload matches this call.
Overload 1 of 6, '(component: keyof Globals): void | LetKeyword | ComponentKeyword<Globals> | ConcatHelper | FnHelper | ... 19 more ... | WithKeyword', gave the following error.
Argument of type 'string' is not assignable to parameter of type 'keyof Globals'.
Overload 2 of 6, '(component: keyof Globals | null | undefined): void | LetKeyword | ComponentKeyword<Globals> | ConcatHelper | ... 21 more ... | null', gave the following error.
The last overload gave the following error.
Argument of type 'string' is not assignable to parameter of type 'keyof Globals | null | undefined'.",
"range": {
"end": {
Expand All @@ -765,112 +759,86 @@ describe('Language Server: Diagnostic Augmentation', () => {
`);
});

test('direct invocation of `{{component}}`', () => {
project.setGlintConfig({ environment: ['ember-loose'] });
test('bad `component`/`helper`/`modifier` arg type', () => {
project.setGlintConfig({ environment: ['ember-loose', 'ember-template-imports'] });
project.write({
'index.ts': stripIndent`
import Component from '@glimmer/component';
export interface MyComponentSignature {
Args: {
message?: string;
};
Blocks: {
default: [];
};
}
export default class MyComponent extends Component<MyComponentSignature> {}
'index.gts': stripIndent`
import { ComponentLike, HelperLike, ModifierLike } from '@glint/template';
declare module '@glint/environment-ember-loose/registry' {
export default interface Registry {
'my-component': typeof MyComponent;
}
}
`,
'index.hbs': stripIndent`
{{! inline invocation }}
{{component 'my-component'}}
{{component 'my-component' message="hi"}}
declare const Comp: ComponentLike<{ Args: { foo: string } }>;
declare const help: HelperLike<{ Args: { Named: { foo: string } } }>;
declare const mod: ModifierLike<{ Args: { Named: { foo: string } } }>;
{{! block invocation }}
{{#component 'my-component'}}{{/component}}
{{#component 'my-component' message="hi"}}{{/component}}
<template>
{{#let
(component Comp foo=123)
(helper help foo=123)
(modifier mod foo=123)
}}
{{/let}}
</template>
`,
});

let server = project.startLanguageServer();
let diagnostics = server.getDiagnostics(project.fileURI('index.hbs'));
let diagnostics = server.getDiagnostics(project.fileURI('index.gts'));

expect(diagnostics).toMatchInlineSnapshot(`
[
{
"code": 2345,
"message": "The {{component}} helper can't be used to directly invoke a component under Glint. Consider first binding the result to a variable, e.g. '{{#let (component 'component-name') as |ComponentName|}}' and then invoking it as '<ComponentName @arg={{value}} />'.
Argument of type 'typeof MyComponent' is not assignable to parameter of type 'ContentValue'.",
"range": {
"end": {
"character": 28,
"line": 1,
},
"start": {
"character": 0,
"line": 1,
},
},
"severity": 1,
"source": "glint",
"tags": [],
},
{
"code": 2345,
"message": "The {{component}} helper can't be used to directly invoke a component under Glint. Consider first binding the result to a variable, e.g. '{{#let (component 'component-name') as |ComponentName|}}' and then invoking it as '<ComponentName @arg={{value}} />'.
Argument of type 'Invokable<(named?: PrebindArgs<{ message?: string | undefined; }, \\"message\\"> | undefined) => ComponentReturn<FlattenBlockParams<{ default: { Params: { Positional: []; }; }; }>, unknown>>' is not assignable to parameter of type 'ContentValue'.",
"code": 2769,
"message": "Unable to pre-bind the given args to the given component. This likely indicates a type mismatch between its signature and the values you're passing.
No overload matches this call.
The last overload gave the following error.
Type 'number' is not assignable to type 'string'.",
"range": {
"end": {
"character": 41,
"line": 2,
"character": 23,
"line": 8,
},
"start": {
"character": 0,
"line": 2,
"character": 20,
"line": 8,
},
},
"severity": 1,
"source": "glint",
"tags": [],
},
{
"code": 2345,
"message": "The {{component}} helper can't be used to directly invoke a component under Glint. Consider first binding the result to a variable, e.g. '{{#let (component 'component-name') as |ComponentName|}}' and then invoking it as '<ComponentName @arg={{value}}>...</ComponentName>'.
Argument of type 'typeof MyComponent' is not assignable to parameter of type 'ComponentReturn<any, any>'.
Type 'typeof MyComponent' is missing the following properties from type 'ComponentReturn<any, any>': [Blocks], [Element]",
"code": 2769,
"message": "Unable to pre-bind the given args to the given helper. This likely indicates a type mismatch between its signature and the values you're passing.
No overload matches this call.
The last overload gave the following error.
Type 'number' is not assignable to type 'string'.",
"range": {
"end": {
"character": 43,
"line": 5,
"character": 20,
"line": 9,
},
"start": {
"character": 0,
"line": 5,
"character": 17,
"line": 9,
},
},
"severity": 1,
"source": "glint",
"tags": [],
},
{
"code": 2345,
"message": "The {{component}} helper can't be used to directly invoke a component under Glint. Consider first binding the result to a variable, e.g. '{{#let (component 'component-name') as |ComponentName|}}' and then invoking it as '<ComponentName @arg={{value}}>...</ComponentName>'.
Argument of type 'Invokable<(named?: PrebindArgs<{ message?: string | undefined; }, \\"message\\"> | undefined) => ComponentReturn<FlattenBlockParams<{ default: { Params: { Positional: []; }; }; }>, unknown>>' is not assignable to parameter of type 'ComponentReturn<any, any>'.",
"code": 2769,
"message": "Unable to pre-bind the given args to the given modifier. This likely indicates a type mismatch between its signature and the values you're passing.
No overload matches this call.
The last overload gave the following error.
Type 'number' is not assignable to type 'string'.",
"range": {
"end": {
"character": 56,
"line": 6,
"character": 21,
"line": 10,
},
"start": {
"character": 0,
"line": 6,
"character": 18,
"line": 10,
},
},
"severity": 1,
Expand Down
2 changes: 2 additions & 0 deletions packages/core/src/config/types.cts
Original file line number Diff line number Diff line change
Expand Up @@ -47,11 +47,13 @@ export type GlintSpecialForm =
| 'yield'
| 'object-literal'
| 'array-literal'
| 'bind-invokable'
| '==='
| '!=='
| '&&'
| '||'
| '!';

export type GlintSpecialFormConfig = {
globals?: { [global: string]: GlintSpecialForm };
imports?: {
Expand Down
Loading

0 comments on commit 3501e8b

Please sign in to comment.