Skip to content

Commit 1268486

Browse files
authored
Add Examples for Contextual Components with Generics
1 parent 4563b16 commit 1268486

File tree

1 file changed

+137
-25
lines changed

1 file changed

+137
-25
lines changed

guides/release/typescript/core-concepts/invokables.md

Lines changed: 137 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -643,50 +643,45 @@ Nearly anything you can do with a “regular” TypeScript function or class, yo
643643
We can make a component accept a [generic][generic] type, or use [union][union] types.
644644
With these tools at our disposal, we can even define our signatures to [make illegal states un-representable][illegal].
645645
646-
To see this in practice, consider a list component which yields back out instances of the same type it provides, and provides the appropriate element target based on a `type` argument.
647-
Yielding back out the same type passed in will use generics, and providing an appropriate element target for `...attributes` can use a union type.
646+
### Union Types
647+
648+
To see this in practice, consider a list component that provides the appropriate element target based on a `type` argument.
648649
649650
Here is how that might look, using a class-backed component rather than a template-only component, since the only places TypeScript allows us to name new generic types are on functions and classes:
650651
651-
```typescript
652+
```gts
652653
import Component from '@glimmer/component';
653654

654-
interface OrderedList<T> {
655+
interface OrderedList {
655656
Args: {
656-
items: Array<T>;
657+
names: Array<string>;
657658
type: 'ordered';
658659
};
659-
Blocks: {
660-
default: [item: T];
661-
};
662660
Element: HTMLOListElement;
663661
}
664662

665-
interface UnorderedList<T> {
663+
interface UnorderedList {
666664
Args: {
667-
items: Array<T>;
665+
names: Array<string>;
668666
type: 'unordered';
669667
};
670-
Blocks: {
671-
default: [item: T];
672-
};
673668
Element: HTMLUListElement;
674669
}
675670

676-
type ListSignature<T> = OrderedList<T> | UnorderedList<T>;
671+
type ListSignature = OrderedList | UnorderedList;
677672

678-
export default class List<T> extends Component<ListSignature<T>> {
673+
export default class List extends Component<ListSignature> {
679674
<template>
680675
{{#if (isOrdered @type)}}
681676
<ol ...attributes>
682-
{{#each @items as |item|}}
683-
<li>{{yield item}}</li>
677+
{{#each @names as |name|}}
678+
<li>{{name}}</li>
684679
{{/each}}
685680
</ol>
686681
{{else}}
687682
<ul ...attributes>
688-
{{#each @items as |item|}}
689-
<li>{{yield item}}</li>
683+
{{#each @names as |name|}}
684+
<li>{{name}}</li>
690685
{{/each}}
691686
</ul>
692687
{{/if}}
@@ -701,17 +696,134 @@ function isOrdered(type: 'ordered' | 'unordered'): type is 'ordered' {
701696
If you are using Glint, when this component is invoked, the `@type` argument will determine what kinds of modifiers are legal to apply to it. For example, if you defined a modifier `reverse` which required an `HTMLOListElement`, this invocation would be rejected:
702697
703698
```handlebars
704-
<List @items={{array 1 2 3}} @type='unordered' {{reverse}} as |item|>
705-
The item is
706-
{{item}}.
707-
</List>
699+
<List @items={{array 1 2 3}} @type='unordered' {{reverse}} />
708700
```
709701
710-
The same approach with generics works for class-based helpers and class-based modifiers.
711-
Function-based helpers and modifiers can also use generics, but by using them on the function definition rather than via a signature.
712702
One caveat: particularly complicated union types in signatures can sometimes become too complex for Glint/TypeScript to resolve when invoking in a template.
713703
In those cases, your best bet is to find a simpler way to structure the types while preserving type safety.
714704
705+
### Generic Types
706+
You can use generic types to improve Intellisense and type checking for consumers of your component:
707+
708+
```ts {data-filename="app/components/list.ts"}
709+
import Component from '@glimmer/component';
710+
711+
interface ListSignature<T>{
712+
Args: {
713+
items: T[];
714+
};
715+
Blocks: {
716+
default: [item: T]
717+
}
718+
}
719+
720+
export default class List<T> extends Component<ListSignature<T>>{
721+
...
722+
}
723+
```
724+
725+
```hbs {data-filename="app/components/list.hbs"}
726+
<ul>
727+
{{#each @items as |item|}}
728+
<li>{{yield item}}</li>
729+
{{/each}}
730+
</ul>
731+
```
732+
733+
When consuming this component, Glint can infer the type of the yielded value to be the same as the type of `@items`:
734+
735+
```gts {data-filename="app/components/list-consumer.gts"}
736+
const people = [
737+
{
738+
id: 1,
739+
name: 'John'
740+
},
741+
{
742+
id: 2,
743+
name: 'Jane'
744+
}
745+
];
746+
747+
const Consumer = <template>
748+
<List @items={{people}} as |person| >
749+
{{person.username}} {{!-- This will throw a type error because 'username' is not defined on our items --}}
750+
{{person.name}}
751+
</List>
752+
</template>
753+
```
754+
755+
Function-based helpers and modifiers can also use generics, but by using them on the function definition rather than via a signature.
756+
The same approach with generics works for class-based helpers and class-based modifiers.
757+
758+
You can also use generic types when yielding a contextual component by creating a property on the class that implements the generic type on the relevant component:
759+
760+
```gts {data-filename="app/components/contextual-list.gts"}
761+
import Component from '@glimmer/component';
762+
import type { WithBoundArgs } from '@glint/template';
763+
764+
interface ListItemSignature<T>{
765+
Args: {
766+
item: T;
767+
};
768+
Blocks: {
769+
default: [item: T]
770+
}
771+
}
772+
773+
class ListItem<T> extends Component<ListItemSignature<T>>{
774+
<template>
775+
<li>
776+
{{yield @item}}
777+
</li>
778+
</template>
779+
}
780+
781+
interface ListSignature<T>{
782+
Args: {
783+
items: T[];
784+
};
785+
Blocks: {
786+
default: [WithBoundArgs<typeof ListItem<T>, 'item'>]
787+
}
788+
}
789+
790+
export default class List<T> extends Component<ListSignature<T>>{
791+
ListItemComponent = ListItem<T>;
792+
793+
<template>
794+
<ul>
795+
{{#each @items as |item|}}
796+
{{yield (component this.ListItemComponent item=item) }}
797+
{{/each}}
798+
</ul>
799+
</template>
800+
}
801+
```
802+
803+
When consuming this component, and it's yielded contextual component, Glint will again infer the type of the yielded value to be the same as the type of `@items`:
804+
```gts {data-filename="app/components/contextual-list-consumer.gts"}
805+
const items = [
806+
{
807+
id: 1,
808+
name: 'John'
809+
},
810+
{
811+
id: 2,
812+
name: 'Jane'
813+
}
814+
];
815+
816+
const Consumer = <template>
817+
<List @items={{items}} as |ListItem|>
818+
<ListItem as |person|>
819+
{{person.username}} {{!-- This will throw a type error because 'username' is not defined on our items --}}
820+
{{person.name}}
821+
</ListItem>
822+
</List>
823+
</template>
824+
```
825+
826+
715827
<!-- Internal links -->
716828

717829
[audio-player-section]: ../../../components/template-lifecycle-dom-and-modifiers/#toc_communicating-between-elements-in-a-component

0 commit comments

Comments
 (0)