Skip to content

Commit

Permalink
Merge pull request #307 from typed-ember/native-import-docs
Browse files Browse the repository at this point in the history
  • Loading branch information
dfreeman authored Apr 22, 2022
2 parents 81cd39c + 59b36bf commit 81745f4
Show file tree
Hide file tree
Showing 12 changed files with 304 additions and 713 deletions.
598 changes: 10 additions & 588 deletions README.md

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion docs/contents.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@

- Ember
- [Installation](ember/installation.md)
- [Imports](ember/imports.md)
- [Component Signatures](ember/component-signatures.md)
- [Helper and Modifier Signatures](ember/helper-and-modifier-signatures.md)
- [Template Registry](ember/template-registry.md)
- [Routes and Controllers](ember/routes-and-controllers.md)
- [Template-Only Components](ember/template-only-components.md)
Expand Down
88 changes: 67 additions & 21 deletions docs/ember/component-signatures.md
Original file line number Diff line number Diff line change
@@ -1,18 +1,30 @@
While Glimmer components accept `Args` as a type parameter, and Ember components accept no type parameters at all, the Glint version of each accepts `Signature`, which contains types for `Element`, `Args` and `Yields`.
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`.

The `Args` field functions the same way the `Args` type parameter does with normal `@glimmer/component` usage.
[rfc 748]: https://github.com/emberjs/rfcs/pull/748

The `Element` field declares what type of element(s), if any, the component applies its passed `...attributes` to. This is often the component's root element. Tracking this type ensures any modifiers used on your component will be compatible with the DOM element(s) they're ultimately attached to. If no `Element` is specified, it will be a type error to set any HTML attributes when invoking your component.
`Args` represents the arguments your component accepts. Typically this will be an object type mapping the names of your
args to their expected type. If no `Args` key is specified, it will be a type error to pass any arguments to your
component.

The `Yields` field specifies the names of any blocks the component yields to, as well as the type of any parameter(s) they'll receive. See the [Yieldable Named Blocks RFC] for further details.
(Note that the `inverse` block is an alias for `else`. These should be defined in `Yields` as `else`, though `{{yield to="inverse"}}` will continue to work.)
The `Element` field declares what type of element(s), if any, the component applies its passed `...attributes` to. This
is often the component's root element. Tracking this type ensures any modifiers used on your component will be
compatible with the DOM element(s) they're ultimately attached to. If no `Element` is specified, it will be a type error
to set any HTML attributes or modifiers when invoking your component.

The `Blocks` field specifies the names of any blocks the component yields to, as well as the type of any parameter(s)
those blocks will receive. If no `Blocks` key is specified, it will be a type error to invoke your component in block
form.

Note that the `inverse` block is an alias for `else`. These should be defined in `Blocks` as `else`, though
`{{yield to="inverse"}}` will continue to work.

## Glimmer Components

{% code title="app/components/super-table.ts" %}

```typescript
import Component from '@glint/environment-ember-loose/glimmer-component';
import Component from '@glimmer/component';

export interface SuperTableSignature<T> {
// We have a `<table>` as our root element
Expand All @@ -21,10 +33,11 @@ export interface SuperTableSignature<T> {
Args: {
items: Array<T>;
};
// We accept two named blocks: an optional `header`, and a required
// `row`, which will be invoked with each item and its index.
Yields: {
header?: [];
// We accept two named blocks: a parameter-less `header` block
// and a `row` block which will be invoked with each item and
// its index sequentially.
Blocks: {
header: [];
row: [item: T, index: number];
};
}
Expand All @@ -37,8 +50,6 @@ export default class SuperTable<T> extends Component<SuperTableSignature<T>> {}
{% code title="app/components/super-table.hbs" %}

```handlebars
{{! app/components/super-table.hbs }}
<table ...attributes>
{{#if (has-block 'header')}}
<thead>
Expand All @@ -63,21 +74,24 @@ Since Ember components don't have `this.args`, it takes slightly more boilerplat
{% code title="app/components/greeting.ts" %}

```typescript
import Component, { ArgsFor } from '@glint/environment-ember-loose/ember-component';
import Component from '@ember/component';
import { computed } from '@ember/object';

// We break out the args into their own interface to reference below:
export interface GreetingArgs {
message: string;
target?: string;
}

export interface GreetingSignature {
Args: {
message: string;
target?: string;
};
Yields: {
Args: GreetingArgs;
Blocks: {
default: [greeting: string];
};
}

// This line declares that our component's args will be 'splatted' on to the instance:
export default interface Greeting extends ArgsFor<GreetingSignature> {}
export default interface Greeting extends GreetingArgs {}
export default class Greeting extends Component<GreetingSignature> {
@computed('target')
private get greetingTarget() {
Expand All @@ -97,6 +111,38 @@ export default class Greeting extends Component<GreetingSignature> {

{% endcode %}

Ember components also support `PositionalArgs` in their signature. Such usage is relatively rare, but components such as [`{{animated-if}}`](https://github.com/ember-animation/ember-animated) do take advantage of it. `PositionalArgs` are specified using a tuple type in the same way that block parameters are. You can also include `PositionalArgs` in the signature passed to `ComponentLike` (see below) when declaring types for third-party components.
Ember components also support positional arguments in their signature. Such usage is relatively rare, but components such as [`{{animated-if}}`](https://github.com/ember-animation/ember-animated) do take advantage of it.

{% code title="app/components/greeting.ts" %}

```typescript
// ...

export interface GreetingArgs {
message: string;
target?: string;
}

export interface GreetingSignature {
Args: {
Named: GreetingArgs;
Positional: [extraSpecialPreamble: string];
};
Blocks: {
default: [greeting: string];
};
}

export default interface Greeting extends GreetingArgs {}
export default class Greeting extends Component<GreetingSignature> {
static positionalParams = ['extraSpecialPreamble'];
declare readonly extraSpecialPreamble: string;
// ...
}
```

{% endcode %}

Positional args are specified as a `Positional` tuple nested within `Args`, the same way they are in helper and modifier signatures. You can also specify positional args with `ComponentLike` types in this way.

Note that both `Element` and `PositionalArgs` are not fully integrated with the string-based APIs on the `@ember/component` base class. This means, for example that there's no enforcement that `tagName = 'table'` and `Element: HTMLTableElement` are actually correlated to one another.
Note that both `Positional` args and the `Element` type are not fully integrated with the string-based APIs on the `@ember/component` base class. This means, for example, that there's no enforcement that `tagName = 'table'` and `Element: HTMLTableElement` are actually correlated to one another.
16 changes: 8 additions & 8 deletions docs/ember/contextual-components.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
When you yield a contextual component, e.g. `{{yield (component "my-component" foo="bar")}}`, you need some way to declare the type of that value in your component signature. For this you can use the `ComponentLike` type, or the `ComponentWithBoundArgs` shorthand.
When you yield a contextual component, e.g. `{{yield (component "my-component" foo="bar")}}`, you need some way to declare the type of that value in your component signature. For this you can use the `ComponentLike` type and/or the `WithBoundArgs` utility type, both from `@glint/template`.

```typescript
interface MyComponentSignature {
Expand All @@ -20,32 +20,32 @@ Given a component like the one declared above, if you wrote this in some other c
{{yield (hash foo=(component 'my-component'))}}
```

You could type your `Yields` as:
You could type your `Blocks` as:

```typescript
Yields: {
Blocks: {
default: [{ foo: typeof MyComponent }]
}
```

However, if you pre-set the value of the `@value` arg, the consumer shouldn't _also_ need to set that arg. You need a way to declare that that argument is now optional:

```typescript
import { ComponentWithBoundArgs } from '@glint/environment-ember-loose';
import { WithBoundArgs } from '@glint/template';

// ...

Yields: {
default: [{ foo: ComponentWithBoundArgs<typeof MyComponent, 'value'> }]
Blocks: {
default: [{ foo: WithBoundArgs<typeof MyComponent, 'value'> }]
}
```
If you had pre-bound multiple args, you could union them together with the `|` type operator, e.g. `'value' | 'count'`.
Note that `ComponentWithBoundArgs` is effectively shorthand for writing out a `ComponentLike<Signature>` type, which is a generic type that any Glimmer component, Ember component or `{{component}}` return value is assignable to, assuming it has a matching signature.
Note that `WithBoundArgs` is effectively shorthand for writing out a `ComponentLike<Signature>` type (a generic type to which any Glimmer component, Ember component or `{{component}}` return value can be assigned, assuming it has a matching signature).
```typescript
import { ComponentLike } from '@glint/environment-ember-loose';
import { ComponentLike } from '@glint/template';

type AcceptsAFooString = ComponentLike<{ Args: { foo: string } }>;

Expand Down
151 changes: 151 additions & 0 deletions docs/ember/helper-and-modifier-signatures.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
Like components, helpers and modifiers can also have their behavior in templates described by a `Signature` type.

## Helpers

A helper signature has two keys: `Args` and `Return`. The `Args` key is further broken down into `Named` and
`Positional` elements, representing respectively the expected types of the named and positional arguments your helper
expects to receive.

{% tabs %}
{% tab title="Class-Based" %}

```typescript
import Helper from '@ember/component/helper';

type Positional = Array<number>;
type Named = { andThenMultiplyBy?: number };

export interface AddSignature {
Args: {
Positional: Positional;
Named: Named;
};
Return: number;
}

export default class AddHelper extends Helper<AddSignature> {
public compute(positional: Positional, named: Named): number {
let total = positional.reduce((sum, next) => sum + next, 0);
if (typeof named.andThenMultiplyBy === 'number') {
total *= named.andThenMultiplyBy;
}
return total;
}
}
```

{% endtab %}
{% tab title="Function-Based" %}

```typescript
import { helper } from '@ember/component/helper';

export interface AddSignature {
Args: {
Positional: Array<number>;
Named: { andThenMultiplyBy?: number };
};
Return: number;
}

const add = helper<AddSignature>((values, { andThenMultiplyBy }) => {
let total = positional.reduce((sum, next) => sum + next, 0);
if (typeof andThenMultiplyBy === 'number') {
total *= andThenMultiplyBy;
}
return total;
});

export default add;
```

{% endtab %}
{% tab title="Function-Based (Inferred Signature)" %}

```typescript
import { helper } from '@ember/component/helper';

const add = helper((values: Array<number>, named: { andThenMultiplyBy?: number }) => {
let total = positional.reduce((sum, next) => sum + next, 0);
if (typeof named.andThenMultiplyBy === 'number') {
total *= named.andThenMultiplyBy;
}
return total;
});

export default add;
```

{% endtab %}
{% endtabs %}

## Modifiers

A modifier's `Args` are split into `Named` and `Positional` as with helpers, but unlike helpers they have no `Return`,
since modifiers don't return a value. Their signtures can, however, specify the type of DOM `Element` they are
compatible with.

{% tabs %}
{% tab title="Class-Based" %}

```typescript
import Modifier from 'ember-modifier';
import { renderToCanvas, VertexArray, RenderOptions } from 'neat-webgl-library';

type Positional = [model: VertexArray];

export interface Render3DModelSignature {
Element: HTMLCanvasElement;
Args: {
Positional: Positional;
Named: RenderOptions;
};
}

export default class Render3DModel extends Modifier<Render3DModelSignature> {
modify(element: HTMLCanvasElement, [model]: Positional, named: RenderOptions) {
renderToCanvas(element, model, options);
}
}
```

{% endtab %}
{% tab title="Function-Based" %}

```typescript
import { modifier } from 'ember-modifier';
import { renderToCanvas, RenderOptions, VertexArray } from 'neat-webgl-library';

export interface Render3DModelSignature {
Element: HTMLCanvasElement;
Args: {
Positional: [model: VertexArray];
Named: RenderOptions;
};
}

const render3DModel = modifier<Render3DModelSignature>((element, [model], options) => {
renderToCanvas(element, model, options);
});

export default render3DModel;
```

{% endtab %}
{% tab title="Function-Based (Inferred Signature)" %}

```typescript
import { modifier } from 'ember-modifier';
import { renderToCanvas, RenderOptions, VertexArray } from 'neat-webgl-library';

const render3DModel = modifier<Render3DModelSignature>(
(element: HTMLCanvasElement, [model]: [model: VertexArray], options: RenderOptions): void => {
renderToCanvas(element, model, options);
}
);

export default render3DModel;
```

{% endtab %}
{% endtabs %}
9 changes: 0 additions & 9 deletions docs/ember/imports.md

This file was deleted.

4 changes: 4 additions & 0 deletions docs/ember/installation.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,4 +29,8 @@ include:
Note that specifying `include` globs is optional, but may be a useful way to incrementally migrate your project to Glint over time.

{% hint style="info" %}

To minimize spurious errors when typechecking with vanilla `tsc` or your editor's TypeScript integration, you should add `import '@glint/environment-ember-loose';` somewhere in your project's source or type declarations. You may also choose to disable TypeScript's "unused symbol" warnings in your editor, since `tsserver` won't understand that templates actually are using them.

{% endhint %}
4 changes: 2 additions & 2 deletions docs/ember/template-only-components.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,12 @@ A template-only component is any template for which Ember (and Glint) can't loca
While it's possible to do some simple things like invoking other components from these templates, typically you'll want to create a backing module for your template so you can declare its signature, add it to the template registry, and so on.

```typescript
import templateOnlyComponent from '@glint/environment-ember-loose/ember-component/template-only';
import templateOnlyComponent from '@ember/component/template-only';

interface ShoutSignature {
Element: HTMLDivElement;
Args: { message: string };
Yields: {
Blocks: {
default: [shoutedMessage: string];
};
}
Expand Down
Loading

0 comments on commit 81745f4

Please sign in to comment.