Skip to content

Convert services/accessibility/typescript/models to gjs/gts #2126

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

Open
wants to merge 10 commits into
base: gjs
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all 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
35 changes: 19 additions & 16 deletions guides/release/accessibility/page-template-considerations.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,29 +17,32 @@ Consider this format:

Note that the unique page title is first. This is because it is the most important piece of information from a contextual perspective. Since a user with a screen reader can interrupt the screen reader as they wish, it introduces less fatigue when the unique page title is first, but provides the additional guidance if it is desired.

A simple way to add page titles is to use the `page-title` helper which comes from the [ember-page-title](https://github.com/ember-cli/ember-page-title) addon that is installed by default in new apps. We can use this helper to set the page title at any point in any template.
A simple way to add page titles is to use the `pageTitle` helper which comes from the [ember-page-title](https://github.com/ember-cli/ember-page-title) addon that is installed by default in new apps. We can use this helper to set the page title at any point in any template.

For example, if we have a “posts” route, we can set the page title for it like so:

```gjs {data-filename=app/routes/posts.gjs}
import { pageTitle } from 'ember-page-title';

```handlebars {data-filename=app/routes/posts.hbs}
{{page-title "Posts - Site Title"}}

{{outlet}}
<template>
{{pageTitle "Posts"}}
{{outlet}}
</template>
```

Extending the example, if we have a “post” route that lives within the “posts” route, we could set its page title like so:

```handlebars {data-filename=app/routes/posts/post.hbs}
{{page-title (concat @model.title " - Site Title")}}
```gjs {data-filename=app/routes/posts/post.gjs}
import { pageTitle } from 'ember-page-title';

<h1>{{@model.title}}</h1>
```
<template>
{{pageTitle @model.title}} {{! e.g., "My Title" }}

When your needs become more complex, the following addons facilitate page titles in a more dynamic and maintainable way.
<h1>{{@model.title}}</h1>
</template>
```

- [ember-cli-head](https://github.com/ronco/ember-cli-head)
- [ember-cli-document-title](https://github.com/kimroen/ember-cli-document-title)
Each call to the `{{pageTitle}}` helper will prepend the title string to the existing title all the way up to the root title in `application.gts`. So, if your application is titled "My App", then the full title for the above example would be "My Title | Posts | My App".

To evaluate more addons to add/manage content in the `<head>` of a page, view this category on [Ember Observer](https://emberobserver.com/categories/header-content).

Expand All @@ -48,14 +51,14 @@ You can test that page titles are generated correctly by asserting on the value
```javascript {data-filename=tests/acceptance/posts-test.js}
import { module, test } from 'qunit';
import { visit, currentURL } from '@ember/test-helpers';
import { setupApplicationTest } from 'my-app-name/tests/helpers';
import { setupApplicationTest } from 'my-app/tests/helpers';

module('Acceptance | posts', function(hooks) {
module('Acceptance | posts', function (hooks) {
setupApplicationTest(hooks);

test('visiting /posts', async function(assert) {
test('visiting /posts', async function (assert) {
await visit('/posts');
assert.equal(document.title, 'Posts - Site Title');
assert.equal(document.title, 'Posts | My App');
});
});
```
Expand Down
37 changes: 17 additions & 20 deletions guides/release/models/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -90,9 +90,9 @@ writing the admin section of a blogging app, which has a feature that
lists the drafts for the currently logged in user.

You might be tempted to make the component responsible for fetching that
data and storing it:
data and storing it and showing the list of drafts, like this:

```javascript {data-filename=app/components/list-of-drafts.js}
```gjs {data-filename=app/components/list-of-drafts.gjs}
import Component from "@glimmer/component";
import { tracked } from "@glimmer/tracking";
import fetch from "fetch";
Expand All @@ -107,30 +107,27 @@ export default class ListOfDraftsComponent extends Component {
this.drafts = data;
});
}
<template>
<ul>
{{#each this.drafts key="id" as |draft|}}
<li>{{draft.title}}</li>
{{/each}}
</ul>
</template>
}
```

You could then show the list of drafts in your component's template like
this:

```handlebars {data-filename=app/components/list-of-drafts.hbs}
<ul>
{{#each this.drafts key="id" as |draft|}}
<li>{{draft.title}}</li>
{{/each}}
</ul>
```

This works great for the `list-of-drafts` component. However, your app
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
This works great for the `list-of-drafts` component. However, your app
This works great for the `ListOfDrafts` component. However, your app

is likely made up of many different components. On another page you
may want a component to display the number of drafts. You may be
tempted to copy and paste your existing `willRender` code into the new
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
tempted to copy and paste your existing `willRender` code into the new
tempted to copy and paste your existing `constructor` code into the new

How this persisted 😓

component.

```javascript {data-filename=app/components/drafts-button.js}
```gjs {data-filename=app/components/drafts-button.gjs}
import Component from "@glimmer/component";
import { tracked } from "@glimmer/tracking";
import fetch from "fetch";
import { LinkTo } from '@ember/routing';

export default class DraftsButtonComponent extends Component {
@tracked drafts;
Expand All @@ -142,13 +139,13 @@ export default class DraftsButtonComponent extends Component {
this.drafts = data;
});
}
}
```

```handlebars {data-filename=app/components/drafts-button.hbs}
<LinkTo @route="drafts">
Drafts ({{this.drafts.length}})
</LinkTo>
<template>
<LinkTo @route="drafts">
Drafts ({{this.drafts.length}})
</LinkTo>
</template>
}
```

Unfortunately, the app will now make two separate requests for the
Expand Down
56 changes: 34 additions & 22 deletions guides/release/services/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -62,27 +62,35 @@ You can either invoke it with no arguments, or you can pass it the registered na
When no arguments are passed, the service is loaded based on the name of the decorated property.
You can load the shopping cart service with no arguments like below.

```javascript {data-filename=app/components/cart-contents.js}
```gjs {data-filename=app/components/cart-contents.gjs}
import Component from '@glimmer/component';
import { service } from '@ember/service';

export default class CartContentsComponent extends Component {
// Will load the service defined in: app/services/shopping-cart.js
@service shoppingCart;

<template>
<h2>Shopping Cart</h2>
</template>
}
```

This injects the shopping cart service into the component and makes it available as the `shoppingCart` property.

Another way to inject a service is to provide the name of the service as an argument to the decorator.

```javascript {data-filename=app/components/cart-contents.js}
```gjs {data-filename=app/components/cart-contents.gjs}
import Component from '@glimmer/component';
import { service } from '@ember/service';

export default class CartContentsComponent extends Component {
// Will load the service defined in: app/services/shopping-cart.js
@service('shopping-cart') cart;

<template>
<h2>Shopping Cart</h2>
</template>
}
```

Expand All @@ -92,7 +100,7 @@ Sometimes a service may or may not exist, like when an initializer conditionally
Since normal injection will throw an error if the service doesn't exist,
you must look up the service using Ember's [`getOwner`](https://api.emberjs.com/ember/release/classes/@ember%2Fapplication/methods/getOwner?anchor=getOwner) instead.

```javascript {data-filename=app/components/cart-contents.js}
```gjs {data-filename=app/components/cart-contents.gjs}
import Component from '@glimmer/component';
import { getOwner } from '@ember/application';

Expand All @@ -101,42 +109,46 @@ export default class CartContentsComponent extends Component {
get cart() {
return getOwner(this).lookup('service:shopping-cart');
}

<template>
<h2>Shopping Cart</h2>
</template>
}
```

Injected properties are lazy loaded; meaning the service will not be instantiated until the property is explicitly called.

Once loaded, a service will persist until the application exits.

Once injected into a component, a service can also be used in the template.

Below we add a remove action to the `cart-contents` component.

```javascript {data-filename=app/components/cart-contents.js}
```gjs {data-filename=app/components/cart-contents.gjs}
import Component from '@glimmer/component';
import { service } from '@ember/service';
import { action } from '@ember/object';
import { on } from '@ember/modifier';
import { fn } from '@ember/helper';

export default class CartContentsComponent extends Component {
@service('shopping-cart') cart;

@action
remove(item) {
remove = (item) => {
this.cart.remove(item);
}
};

<template>
<h2>Shopping Cart</h2>
<ul>
{{#each this.cart.items as |item|}}
<li>
{{item.name}}
<button type="button" {{on "click" (fn this.remove item)}}>Remove</button>
</li>
{{/each}}
</ul>
</template>
}
```

Once injected into a component, a service can also be used in the template.
Note `cart` being used below to get data from the cart.

```handlebars {data-filename=app/components/cart-contents.hbs}
<ul>
{{#each this.cart.items as |item|}}
<li>
{{item.name}}
<button type="button" {{on "click" (fn this.remove item)}}>Remove</button>
</li>
{{/each}}
</ul>
```

<!-- eof - needed for pages that end in a code block -->
29 changes: 4 additions & 25 deletions guides/release/typescript/additional-resources/gotchas.md
Original file line number Diff line number Diff line change
Expand Up @@ -54,34 +54,11 @@ For examples, see:
- EmberData [`@belongsTo`][model-belongsto]
- EmberData [`@hasMany`][model-hasmany]

## Templates

Templates are currently totally non-type-checked. This means that you lose any safety when moving into a template context, even if using a Glimmer `Component` in Ember Octane. (Looking for type-checking in templates? Try [Glint][]!)

For example, TypeScript won't detect a mismatch between this action and the corresponding call in the template:

```typescript {data-filename="app/components/my-game.ts"}
import Component from '@ember/component';
import { action } from '@ember/object';

export default class MyGame extends Component {
@action turnWheel(degrees: number) {
// ...
}
}
```

```handlebars {data-filename="app/components/my-game.hbs"}
<button {{on 'click' (fn this.turnWheel 'potato')}}>
Click Me
</button>
```

## Hook Types and Autocomplete

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 {data-filename="app/components/args-display.ts"}
```gts {data-filename="app/components/args-display.gts"}
import type Owner from '@ember/owner';
import Component from '@glimmer/component';

Expand All @@ -100,6 +77,9 @@ export default class ArgsDisplay extends Component<ArgsDisplaySignature> {
super(owner, args);
Object.keys(args).forEach(log);
}

<template>
</template>
}
```

Expand Down Expand Up @@ -136,4 +116,3 @@ export default class MyRoute extends Route {
<!-- External links -->

[declare]: https://www.typescriptlang.org/docs/handbook/release-notes/typescript-3-7.html#the-usedefineforclassfields-flag-and-the-declare-property-modifier
[glint]: https://typed-ember.gitbook.io/glint/
Loading