Skip to content

Reflect the introduction of Component Signatures #1530

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

Merged
merged 3 commits into from
Oct 25, 2022
Merged
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
113 changes: 49 additions & 64 deletions docs/ember/components.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,19 +36,46 @@ export default class Counter extends Component {

Notice that there are no type declarations here – but this _is_ actually a well-typed component. The type of `count` is `number`, and if we accidentally wrote something like `this.count = "hello"` the compiler would give us an error.

## Adding arguments
## Adding arguments and giving them a type

So far so good, but of course most components aren’t quite this simple! Instead, they’re invoked by other templates and they can invoke other components themselves in their own templates.

Glimmer components can receive both _arguments_ and _attributes_ when they are invoked. When you are working with a component’s backing class, you have access to the arguments but _not_ to the attributes. The arguments are passed to the constructor, and then available as `this.args` on the component instance afterward. Let’s imagine a component which just logs the names of its arguments when it is first constructed:
Glimmer components can receive both _arguments_ and _attributes_ when they are invoked. When you are working with a component’s backing class, you have access to the arguments but _not_ to the attributes. The arguments are passed to the constructor, and then available as `this.args` on the component instance afterward.

Since the implementation of [RFC 748], Glimmer and Ember components accept a `Signature` type parameter as part of their definition. This parameter is expected to be an object type with (up to) three members: `Args`, `Element` and `Blocks`.

[rfc 748]: https://github.com/emberjs/rfcs/pull/748

`Args` represents the arguments your component accepts. Typically this will be an object type mapping the names of your args to their expected type. For example:

```
export interface MySignature {
Args: {
arg1: string;
arg2: number;
arg3: boolean;
}
}
```
If no `Args` key is specified, it will be a type error to pass any arguments to your component. You can read more about `Element` and `Block` in the Glint [Component Signatures documentation](https://typed-ember.gitbook.io/glint/using-glint/ember/component-signatures).

Let’s imagine a component which just logs the names of its arguments when it is first constructed. First, we must define the Signature and pass it into our component, then we can use the `Args` member in our Signature to set the type of `args` in the constructor:

```typescript
import Component from '@glimmer/component';

const log = console.log.bind(console);

export default class ArgsDisplay extends Component {
constructor(owner: unknown, args: {}) {
export interface ArgsDisplaySignature {
Args: {
arg1: string;
arg2: number;
arg3: boolean;
}
}

export default class ArgsDisplay extends Component<ArgsDisplaySignature> {
constructor(owner: unknown, args: ArgsDisplaySignature['Args]) {
super(owner, args);

Object.keys(args).forEach(log);
Expand All @@ -69,24 +96,22 @@ Notice that we have to start by calling `super` with `owner` and `args`. This ma
This might change in the future! If TypeScript eventually adds [support for “variadic kinds”](https://github.com/Microsoft/TypeScript/issues/5453), using `...arguments` could become safe.
{% endhint %}

The types for `owner` here and `args` line up with what the `constructor` for Glimmer components expect. The `owner` is specified as `unknown` because this is a detail we explicitly _don’t_ need to know about. The `args` are `{}` because a Glimmer component _always_ receives an object containing its arguments, even if the caller didn’t pass anything: then it would just be an empty object.

`{}` is an empty object type – all objects extend from it, but there will be no properties on it. This is distinct from the `object` type, which the TypeScript docs describe as:

> any thing that is not `number`, `string`, `boolean`, `symbol`, `null`, or `undefined`.

If we used `object`, we could end up with TypeScript thinking `args` were an array, or a `Set`, or anything else that isn’t a primitive. Since we have `{}`, we _know_ that it's an object.

{% hint style="info" %}
For some further details, check out [this blog post](https://mariusschulz.com/blog/the-object-type-in-typescript).
{% endhint %}
The types for `owner` here and `args` line up with what the `constructor` for Glimmer components expect. The `owner` is specified as `unknown` because this is a detail we explicitly _don’t_ need to know about. The `args` are the `Args` from the Signature we defined.

The `args` passed to a Glimmer Component [are available on `this`](https://github.com/glimmerjs/glimmer.js/blob/2f840309f013898289af605abffe7aee7acc6ed5/packages/%40glimmer/component/src/component.ts#L12), so we could change our definition to return the names of the arguments from a getter:

```typescript
import Component from '@glimmer/component';

export default class ArgsDisplay extends Component {
export interface ArgsDisplaySignature {
Args: {
arg1: string;
arg2: number;
arg3: boolean;
}
}

export default class ArgsDisplay extends Component<ArgsDisplaySignature> {
get argNames(): string[] {
return Object.keys(this.args);
}
Expand All @@ -101,21 +126,19 @@ export default class ArgsDisplay extends Component {
{{/each}}
</ul>
```

### Understanding `args`
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's go ahead and leave this section (I'll do a follow-up on it to clean it up further). If you can just drop it back in, the rest of it looks like a great first iteration on this update, and will already be a big improvement for users. Thank you!

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've added that section back in. I also started to update the section but got out of my depth pretty quickly so will leave the rest with you 🤣

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Haha that's exactly what I suggested this narrower path. Thank you!


Now, looking at that bit of code, you might be wondering how it knows what the type of `this.args` is. In the `constructor` version, we explicitly _named_ the type of the `args` argument. Here, it seems to just work automatically. This works because the type definition for a Glimmer component looks roughly like this:
Looking at that example above, Typescript knows what types `this.args` has, but how? In the `constructor` version, we explicitly _named_ the type of the `args` argument. Here, it seems to just work automatically. This works because the type definition for a Glimmer component looks roughly like this:

```typescript
export default class Component<Args extends {} = {}> {
readonly args: Args;

constructor(owner: unknown, args: Args);
}
```
```typescript
export default class Component {
args: Readonly<Args>;
constructor(owner: unknown, args: Args);
}
```

{% hint style="info" %}
Not sure what’s up with `<Args>` _at all_? We highly recommend the [TypeScript Deep Dive](https://basarat.gitbooks.io/typescript/) book’s [chapter on generics ](https://basarat.gitbooks.io/typescript/docs/types/generics.html) to be quite helpful in understanding this part.
Not sure what’s up with `<Args>` or `<S>` _at all_? We highly recommend the [TypeScript Deep Dive](https://basarat.gitbooks.io/typescript/) book’s [chapter on generics ](https://basarat.gitbooks.io/typescript/docs/types/generics.html) to be quite helpful in understanding this part.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's just leave these as they were as well, despite the slight incoherence. I will do a follow-up PR later today after merging yours which updates all of this language (and possibly does some restructuring as well).

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All done and both sections are back in their original state. Thanks!

{% endhint %}

The type signature for Component, with `Args extends {} = {}`, means that the component _always_ has a property named `args` —
Expand All @@ -133,44 +156,6 @@ let b = ["hello", "goodbye"]; // Array<string>

In the case of the Component, we have the types the way we do so that you can’t accidentally define `args` as a string, or `undefined` , or whatever: it _has_ to be an object. Thus, `Component<Args extends {}>` . But we also want to make it so that you can just write `extends Component` , so that needs to have a default value. Thus, `Component<Args extends {} = {}>`.

### Giving `args` a type
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Whoops, looks like this needs to be added back in as well!


Now let’s put this to use. Imagine we’re constructing a user profile component which displays the user’s name and optionally an avatar and bio. The template might look something like this:

```text
<div class='user-profile' ...attributes>
{{#if this.avatar}}
<img src={{this.avatar}} class='user-profile__avatar'>
{{/if}}
<p class='user-profile__bio'>{{this.userInfo}}</p>
</div>
```

Then we could capture the types for the profile with an interface representing the _arguments_:

```typescript
import Component from '@glimmer/component';
import { generateUrl } from '../lib/generate-avatar';

interface User {
name: string;
avatar?: string;
bio?: string;
}

export default class UserProfile extends Component<User> {
get userInfo(): string {
return this.args.bio ? `${this.args.name} ${this.args.bio}` : this.args.name;
}

get avatar(): string {
return this.args.avatar ?? generateUrl();
}
}
```

Assuming the default `tsconfig.json` settings \(with `strictNullChecks: true`\), this wouldn't type-check if we didn't _check_ whether the `bio` argument were set.

## Generic subclasses

If you'd like to make your _own_ component subclass-able, you need to make it generic as well.
Expand Down