-
-
Notifications
You must be signed in to change notification settings - Fork 502
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add Examples for Contextual Components with Generics #2066
base: master
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change | ||||
---|---|---|---|---|---|---|
|
@@ -643,50 +643,45 @@ Nearly anything you can do with a “regular” TypeScript function or class, yo | |||||
We can make a component accept a [generic][generic] type, or use [union][union] types. | ||||||
With these tools at our disposal, we can even define our signatures to [make illegal states un-representable][illegal]. | ||||||
|
||||||
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. | ||||||
Yielding back out the same type passed in will use generics, and providing an appropriate element target for `...attributes` can use a union type. | ||||||
### Union Types | ||||||
|
||||||
To see this in practice, consider a list component that provides the appropriate element target based on a `type` argument. | ||||||
|
||||||
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: | ||||||
|
||||||
```typescript | ||||||
```gts | ||||||
import Component from '@glimmer/component'; | ||||||
|
||||||
interface OrderedList<T> { | ||||||
interface OrderedList { | ||||||
Args: { | ||||||
items: Array<T>; | ||||||
names: Array<string>; | ||||||
type: 'ordered'; | ||||||
}; | ||||||
Blocks: { | ||||||
default: [item: T]; | ||||||
}; | ||||||
Element: HTMLOListElement; | ||||||
} | ||||||
|
||||||
interface UnorderedList<T> { | ||||||
interface UnorderedList { | ||||||
Args: { | ||||||
items: Array<T>; | ||||||
names: Array<string>; | ||||||
type: 'unordered'; | ||||||
}; | ||||||
Blocks: { | ||||||
default: [item: T]; | ||||||
}; | ||||||
Element: HTMLUListElement; | ||||||
} | ||||||
|
||||||
type ListSignature<T> = OrderedList<T> | UnorderedList<T>; | ||||||
type ListSignature = OrderedList | UnorderedList; | ||||||
|
||||||
export default class List<T> extends Component<ListSignature<T>> { | ||||||
export default class List extends Component<ListSignature> { | ||||||
<template> | ||||||
{{#if (isOrdered @type)}} | ||||||
<ol ...attributes> | ||||||
{{#each @items as |item|}} | ||||||
<li>{{yield item}}</li> | ||||||
{{#each @names as |name|}} | ||||||
<li>{{name}}</li> | ||||||
{{/each}} | ||||||
</ol> | ||||||
{{else}} | ||||||
<ul ...attributes> | ||||||
{{#each @items as |item|}} | ||||||
<li>{{yield item}}</li> | ||||||
{{#each @names as |name|}} | ||||||
<li>{{name}}</li> | ||||||
{{/each}} | ||||||
</ul> | ||||||
{{/if}} | ||||||
|
@@ -701,17 +696,134 @@ function isOrdered(type: 'ordered' | 'unordered'): type is 'ordered' { | |||||
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: | ||||||
|
||||||
```handlebars | ||||||
<List @items={{array 1 2 3}} @type='unordered' {{reverse}} as |item|> | ||||||
The item is | ||||||
{{item}}. | ||||||
</List> | ||||||
<List @items={{array 1 2 3}} @type='unordered' {{reverse}} /> | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This wouldn't type-check since we've passed an array of numbers when an array of strings is expected.
Suggested change
|
||||||
``` | ||||||
|
||||||
The same approach with generics works for class-based helpers and class-based modifiers. | ||||||
Function-based helpers and modifiers can also use generics, but by using them on the function definition rather than via a signature. | ||||||
One caveat: particularly complicated union types in signatures can sometimes become too complex for Glint/TypeScript to resolve when invoking in a template. | ||||||
In those cases, your best bet is to find a simpler way to structure the types while preserving type safety. | ||||||
|
||||||
### Generic Types | ||||||
You can use generic types to improve Intellisense and type checking for consumers of your component: | ||||||
|
||||||
```ts {data-filename="app/components/list.ts"} | ||||||
import Component from '@glimmer/component'; | ||||||
|
||||||
interface ListSignature<T>{ | ||||||
Args: { | ||||||
items: T[]; | ||||||
}; | ||||||
Blocks: { | ||||||
default: [item: T] | ||||||
} | ||||||
} | ||||||
|
||||||
export default class List<T> extends Component<ListSignature<T>>{ | ||||||
... | ||||||
} | ||||||
``` | ||||||
|
||||||
```hbs {data-filename="app/components/list.hbs"} | ||||||
<ul> | ||||||
{{#each @items as |item|}} | ||||||
<li>{{yield item}}</li> | ||||||
{{/each}} | ||||||
</ul> | ||||||
``` | ||||||
|
||||||
When consuming this component, Glint can infer the type of the yielded value to be the same as the type of `@items`: | ||||||
|
||||||
```gts {data-filename="app/components/list-consumer.gts"} | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Let's switch these examples to ts + hbs for now. It's a bit confusing to switch between them in these docs. We should migrate all of these docs to gts at once in a separate PR. |
||||||
const people = [ | ||||||
{ | ||||||
id: 1, | ||||||
name: 'John' | ||||||
}, | ||||||
{ | ||||||
id: 2, | ||||||
name: 'Jane' | ||||||
} | ||||||
]; | ||||||
|
||||||
const Consumer = <template> | ||||||
<List @items={{people}} as |person| > | ||||||
{{person.username}} {{!-- This will throw a type error because 'username' is not defined on our items --}} | ||||||
{{person.name}} | ||||||
</List> | ||||||
</template> | ||||||
``` | ||||||
|
||||||
Function-based helpers and modifiers can also use generics, but by using them on the function definition rather than via a signature. | ||||||
The same approach with generics works for class-based helpers and class-based modifiers. | ||||||
|
||||||
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: | ||||||
|
||||||
```gts {data-filename="app/components/contextual-list.gts"} | ||||||
import Component from '@glimmer/component'; | ||||||
import type { WithBoundArgs } from '@glint/template'; | ||||||
|
||||||
interface ListItemSignature<T>{ | ||||||
Args: { | ||||||
item: T; | ||||||
}; | ||||||
Blocks: { | ||||||
default: [item: T] | ||||||
} | ||||||
} | ||||||
|
||||||
class ListItem<T> extends Component<ListItemSignature<T>>{ | ||||||
<template> | ||||||
<li> | ||||||
{{yield @item}} | ||||||
</li> | ||||||
</template> | ||||||
} | ||||||
|
||||||
interface ListSignature<T>{ | ||||||
Args: { | ||||||
items: T[]; | ||||||
}; | ||||||
Blocks: { | ||||||
default: [WithBoundArgs<typeof ListItem<T>, 'item'>] | ||||||
} | ||||||
} | ||||||
|
||||||
export default class List<T> extends Component<ListSignature<T>>{ | ||||||
ListItemComponent = ListItem<T>; | ||||||
|
||||||
<template> | ||||||
<ul> | ||||||
{{#each @items as |item|}} | ||||||
{{yield (component this.ListItemComponent item=item) }} | ||||||
{{/each}} | ||||||
</ul> | ||||||
</template> | ||||||
} | ||||||
``` | ||||||
|
||||||
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`: | ||||||
```gts {data-filename="app/components/contextual-list-consumer.gts"} | ||||||
const items = [ | ||||||
{ | ||||||
id: 1, | ||||||
name: 'John' | ||||||
}, | ||||||
{ | ||||||
id: 2, | ||||||
name: 'Jane' | ||||||
} | ||||||
]; | ||||||
|
||||||
const Consumer = <template> | ||||||
<List @items={{items}} as |ListItem|> | ||||||
<ListItem as |person|> | ||||||
{{person.username}} {{!-- This will throw a type error because 'username' is not defined on our items --}} | ||||||
{{person.name}} | ||||||
</ListItem> | ||||||
</List> | ||||||
</template> | ||||||
``` | ||||||
|
||||||
|
||||||
<!-- Internal links --> | ||||||
|
||||||
[audio-player-section]: ../../../components/template-lifecycle-dom-and-modifiers/#toc_communicating-between-elements-in-a-component | ||||||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The paragraph before this code block talks about generic types, but this PR has now removed the generics in this section. Probably, that paragraph should be moved to the new section farther down. I do think the note about class backing is important, but it may be sufficiently covered in the added section.