diff --git a/guides/release/accessibility/page-template-considerations.md b/guides/release/accessibility/page-template-considerations.md
index 8ad0724f31..8d616240b2 100644
--- a/guides/release/accessibility/page-template-considerations.md
+++ b/guides/release/accessibility/page-template-considerations.md
@@ -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}}
+
+ {{pageTitle "Posts"}}
+ {{outlet}}
+
```
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';
-
{{@model.title}}
-```
+
+ {{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.
+
{{@model.title}}
+
+```
-- [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 `` of a page, view this category on [Ember Observer](https://emberobserver.com/categories/header-content).
@@ -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');
});
});
```
diff --git a/guides/release/models/index.md b/guides/release/models/index.md
index fc61fd0aac..ccb506441c 100644
--- a/guides/release/models/index.md
+++ b/guides/release/models/index.md
@@ -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";
@@ -107,30 +107,27 @@ export default class ListOfDraftsComponent extends Component {
this.drafts = data;
});
}
+
+
+ {{#each this.drafts key="id" as |draft|}}
+
{{draft.title}}
+ {{/each}}
+
+
}
```
-You could then show the list of drafts in your component's template like
-this:
-
-```handlebars {data-filename=app/components/list-of-drafts.hbs}
-
- {{#each this.drafts key="id" as |draft|}}
-
{{draft.title}}
- {{/each}}
-
-```
-
-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
+tempted to copy and paste your existing `constructor` code into the new
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;
@@ -142,13 +139,13 @@ export default class DraftsButtonComponent extends Component {
this.drafts = data;
});
}
-}
-```
-```handlebars {data-filename=app/components/drafts-button.hbs}
-
- Drafts ({{this.drafts.length}})
-
+
+
+ Drafts ({{this.drafts.length}})
+
+
+}
```
Unfortunately, the app will now make two separate requests for the
diff --git a/guides/release/services/index.md b/guides/release/services/index.md
index 59d2627b89..d653adbd20 100644
--- a/guides/release/services/index.md
+++ b/guides/release/services/index.md
@@ -62,13 +62,17 @@ 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;
+
+
+
Shopping Cart
+
}
```
@@ -76,13 +80,17 @@ This injects the shopping cart service into the component and makes it available
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;
+
+
+
Shopping Cart
+
}
```
@@ -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';
@@ -101,6 +109,10 @@ export default class CartContentsComponent extends Component {
get cart() {
return getOwner(this).lookup('service:shopping-cart');
}
+
+
+
Shopping Cart
+
}
```
@@ -108,35 +120,35 @@ Injected properties are lazy loaded; meaning the service will not be instantiate
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);
- }
+ };
+
+
+
Shopping Cart
+
+ {{#each this.cart.items as |item|}}
+
+ {{item.name}}
+
+
+ {{/each}}
+
+
}
```
-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}
-
- {{#each this.cart.items as |item|}}
-
- {{item.name}}
-
-
- {{/each}}
-
-```
-
diff --git a/guides/release/typescript/additional-resources/gotchas.md b/guides/release/typescript/additional-resources/gotchas.md
index d54caa1b91..74be3c9241 100644
--- a/guides/release/typescript/additional-resources/gotchas.md
+++ b/guides/release/typescript/additional-resources/gotchas.md
@@ -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"}
-
-```
-
## 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';
@@ -100,6 +77,9 @@ export default class ArgsDisplay extends Component {
super(owner, args);
Object.keys(args).forEach(log);
}
+
+
+
}
```
@@ -136,4 +116,3 @@ export default class MyRoute extends Route {
[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/
diff --git a/guides/release/typescript/application-development/testing.md b/guides/release/typescript/application-development/testing.md
index d47c9e3087..f6ce154a16 100644
--- a/guides/release/typescript/application-development/testing.md
+++ b/guides/release/typescript/application-development/testing.md
@@ -17,22 +17,10 @@ export default interface User {
Then our component might be defined like this:
-```handlebars {data-filename="app/components/profile.hbs"}
-
-
- {{@user.displayName}}
-
-```
-
-```typescript {data-filename="app/components/profile.ts"}
+```gts {data-filename="app/components/profile.gts"}
import Component from '@glimmer/component';
import type User from 'app/types/user';
-import { randomAvatarURL } from 'app/utils/avatar';
+import { randomAvatarURL } from 'my-app/utils/avatar';
interface ProfileSignature {
Args: {
@@ -50,18 +38,29 @@ export default class Profile extends Component {
? `${this.args.user.displayName}'s custom profile picture`
: 'a randomly generated placeholder avatar';
}
+
+
+
+
+ {{@user.displayName}}
+
+
}
```
To test the `Profile` component, we need to set up a `User` on `this` to pass into the component as an argument. With TypeScript on our side, we can even make sure our user actually has the correct type!
-```typescript {data-filename="tests/integration/components/profile.ts"}
+```gts {data-filename="tests/integration/components/profile.gts"}
import { module, test } from 'qunit';
import { render } from '@ember/test-helpers';
-import { hbs } from 'ember-cli-htmlbars';
-import { setupRenderingTest } from 'app/tests/helpers';
-import type User from 'app/types/user';
+import { setupRenderingTest } from 'my-app/tests/helpers';
+import type User from 'my-app/types/user';
module('Integration | Component | Profile', function (hooks) {
setupRenderingTest(hooks);
@@ -73,7 +72,7 @@ module('Integration | Component | Profile', function (hooks) {
};
this.user = user;
- await render(hbs`);
assert.dom('[data-test-name]').hasText(this.user.displayName);
assert
@@ -88,7 +87,7 @@ module('Integration | Component | Profile', function (hooks) {
};
this.user = user;
- await render(hbs`);
assert.dom('[data-test-name]').hasText(this.user.displayName);
assert
@@ -105,7 +104,7 @@ To inform TypeScript about this, we need to tell it that the type of `this` in e
```typescript {data-filename="tests/integration/components/profile.ts"}
import type { TestContext } from '@ember/test-helpers';
-import type User from 'app/types/user';
+import type User from 'my-app/types/user';
interface Context extends TestContext {
user: User;
@@ -120,14 +119,13 @@ test('...', function (this: Context, assert) {});
Putting it all together, this is what our updated test definition would look like:
-```typescript {data-filename="tests/integration/components/profile.ts"}
+```gts {data-filename="tests/integration/components/profile.gts"}
import { module, test } from 'qunit';
import { render } from '@ember/test-helpers';
import type { TestContext } from '@ember/test-helpers';
-import { hbs } from 'ember-cli-htmlbars';
-import { setupRenderingTest } from 'app/tests/helpers';
-import type User from 'app/types/user';
+import { setupRenderingTest } from 'my-app/tests/helpers';
+import type User from 'my-app/types/user';
interface Context extends TestContext {
user: User;
@@ -142,7 +140,7 @@ module('Integration | Component | Profile', function (hooks) {
avatarUrl: 'https://example.com/star-wars/rey',
};
- await render(hbs`);
assert.dom('[data-test-name]').hasText(this.user.displayName);
assert
@@ -156,7 +154,7 @@ module('Integration | Component | Profile', function (hooks) {
displayName: 'Rey',
};
- await render(hbs`);
assert.dom('[data-test-name]').hasText(this.user.displayName);
assert
@@ -198,7 +196,7 @@ The test for our function might look something like this:
```javascript {data-filename="tests/unit/utils/math-test.js"}
import { module, test } from 'qunit';
-import { add } from 'app/utils/math';
+import { add } from 'my-app/utils/math';
module('the `add` function', function (hooks) {
test('adds numbers correctly', function (assert) {
@@ -234,7 +232,7 @@ We can also drop the assertion from our function definition, because the _compil
```typescript {data-filename="tests/unit/utils/math-test.ts"}
import { module, test } from 'qunit';
-import { add } from 'app/utils/math';
+import { add } from 'my-app/utils/math';
module('the `add` function', function (hooks) {
test('adds numbers correctly', function (assert) {
@@ -268,7 +266,7 @@ Now, in our test file, we're similarly back to testing all those extra scenarios
```typescript {data-filename="tests/unit/utils/math-test.ts"}
import { module, test } from 'qunit';
-import { add } from 'app/utils/math';
+import { add } from 'my-app/utils/math';
module('the `add` function', function (hooks) {
test('adds numbers correctly', function (assert) {
diff --git a/guides/release/typescript/core-concepts/invokables.md b/guides/release/typescript/core-concepts/invokables.md
index e14f50083a..8c0de1868e 100644
--- a/guides/release/typescript/core-concepts/invokables.md
+++ b/guides/release/typescript/core-concepts/invokables.md
@@ -81,31 +81,30 @@ For example, consider the `AudioPlayer` described in the
There, we defined component which accepted a `srcUrl` argument and used a `play-when` modifier to manage the behavior of the element:
-```typescript {data-filename="app/components/audio-player.ts"}
+```gts {data-filename="app/components/audio-player.gts"}
import Component from '@glimmer/component';
import { tracked } from '@glimmer/tracking';
-import { action } from '@ember/object';
+import playWhen from 'my-app/modifiers/play-when';
+import { on } from '@ember/modifier';
export default class AudioPlayer extends Component {
@tracked isPlaying = false;
- @action
- play() {
+ play = () => {
this.isPlaying = true;
- }
+ };
- @action
- pause() {
+ pause = () => {
this.isPlaying = false;
- }
-}
-```
+ };
-```handlebars {data-filename="app/components/audio-player.hbs"}
-
+
+
-
-
+
+
+
+}
```
What elements do we need to build a signature for this component?
@@ -116,10 +115,11 @@ What elements do we need to build a signature for this component?
We can define a signature with those `Args` on it and apply it to the component definition by adding it as a type parameter to the `extends Component` clause:
-```typescript {data-filename="app/components/audio-player.ts" data-diff="+5,+6,+7,+8,+9,+10,+11,-12,+13"}
-import Component from "@glimmer/component";
-import { tracked } from "@glimmer/tracking";
-import { action } from "@ember/object";
+```gts { data-filename="app/components/audio-player.gts" data-diff="+6,+7,+8,+9,+10,+11,-13,+14" }
+import Component from '@glimmer/component';
+import { tracked } from '@glimmer/tracking';
+import playWhen from 'my-app/modifiers/play-when';
+import { on } from '@ember/modifier';
interface AudioPlayerSignature {
Args: {
@@ -132,24 +132,30 @@ export default class AudioPlayer extends Component {
export default class AudioPlayer extends Component {
@tracked isPlaying = false;
- @action
- play() {
+ play = () => {
this.isPlaying = true;
- }
+ };
- @action
- pause() {
+ pause = () => {
this.isPlaying = false;
- }
+ };
+
+
+
+
+
+
+
}
```
Now, let's expand on this example to give callers the ability to apply attributes to the audio element with an `Element`:
-```typescript {data-filename="app/components/audio-player.ts" data-diff="+10"}
+```gts { data-filename="app/components/audio-player.gts" data-diff="+11,-26,+27" }
import Component from '@glimmer/component';
import { tracked } from '@glimmer/tracking';
-import { action } from '@ember/object';
+import playWhen from 'my-app/modifiers/play-when';
+import { on } from '@ember/modifier';
interface AudioPlayerSignature {
Args: {
@@ -162,32 +168,31 @@ interface AudioPlayerSignature {
export default class AudioPlayer extends Component {
@tracked isPlaying = false;
- @action
- play() {
+ play = () => {
this.isPlaying = true;
- }
+ };
- @action
- pause() {
+ pause = () => {
this.isPlaying = false;
- }
-}
-```
+ };
-```handlebars {data-filename="app/components/audio-player.hbs" data-diff="-1,+2"}
-
-
+
+
+
-
-
+
+
+
+}
```
We can also let the user provide a fallback for the case where the audio element does not load, using the default block. We have to name the default block explicitly in the new `Blocks` type we add to our signature. Since blocks yield out a list of items, we can use a [tuple type][tuple] to represent them. In this case, we will just yield out the same URL we loaded, to let the caller use it for the fallback.
-```typescript {data-filename="app/components/audio-player.ts" data-diff="+10,+11,+12"}
+```gts { data-filename="app/components/audio-player.gts" data-diff="+11,+12,+13,-29,+30,+31,+32" }
import Component from '@glimmer/component';
import { tracked } from '@glimmer/tracking';
-import { action } from '@ember/object';
+import playWhen from 'my-app/modifiers/play-when';
+import { on } from '@ember/modifier';
interface AudioPlayerSignature {
Args: {
@@ -203,52 +208,37 @@ interface AudioPlayerSignature {
export default class AudioPlayer extends Component {
@tracked isPlaying = false;
- @action
- play() {
+ play = () => {
this.isPlaying = true;
- }
+ };
- @action
- pause() {
+ pause = () => {
this.isPlaying = false;
- }
-}
-```
-
-```handlebars {data-filename="app/components/audio-player.hbs" data-diff="-1,+2,+3,+4"}
-
-
+ };
-
-
-```
+
+
+
-Let's go one step further and switch to supporting for two [named blocks][named-blocks]: an optional `title` block for a caption for the audio element, and a `fallback` block for the audio fallback where we previously used a `default` block.
+
+
+
+}
-```handlebars {data-filename="app/components/audio-player.hbs" data-diff="+1,+2,+3,+4,+5,-7,+8"}
-
- {{#if (has-block 'title')}}
- {{yield to='title'}}
- {{/if}}
+```
-
-
-
-
-```
+Let's go one step further and switch to supporting for two [named blocks][named-blocks]: an optional `title` block for a caption for the audio element, and a `fallback` block for the audio fallback where we previously used a `default` block.
To represent this, we will update the `default` block to be named `fallback` instead and add the `title` block. We do not yield anything to the `title` block, so we use an empty tuple, `[]`, to represent it.
-```typescript {data-filename="app/components/audio-player.ts" data-diff="-11,+12,+13"}
+```gts {data-filename="app/components/audio-player.gts" data-diff="-12,+13,+14,-31,-32,-33,+34,+35,+36,+37,+38,+39,+40,+41" }
import Component from '@glimmer/component';
import { tracked } from '@glimmer/tracking';
-import { action } from '@ember/object';
+import playWhen from 'my-app/modifiers/play-when';
+import { on } from '@ember/modifier';
interface AudioPlayerSignature {
Args: {
@@ -266,15 +256,30 @@ interface AudioPlayerSignature {
export default class AudioPlayer extends Component {
@tracked isPlaying = false;
- @action
- play() {
+ play = () => {
this.isPlaying = true;
- }
+ };
- @action
- pause() {
+ pause = () => {
this.isPlaying = false;
- }
+ };
+
+
+
+
+ {{#if (has-block 'title')}}
+ {{yield to='title'}}
+ {{/if}}
+
+
+
+
+
+
}
```
@@ -282,10 +287,11 @@ export default class AudioPlayer extends Component {
When working in JavaScript, we can provide the exact same information using JSDoc comments. Here is how our final component definition would look if it were written in JavaScript rather than TypeScript, and using comments for this documentation information:
-```js {data-filename="app/components/audio-player.js"}
+```gjs {data-filename="app/components/audio-player.gjs"}
import Component from '@glimmer/component';
import { tracked } from '@glimmer/tracking';
-import { action } from '@ember/object';
+import playWhen from 'my-app/modifiers/play-when';
+import { on } from '@ember/modifier';
/**
* @typedef AudioPlayerSignature
@@ -308,18 +314,30 @@ import { action } from '@ember/object';
/**
* @extends Component
*/
-export default class AudioPlayer extends Component {
+ export default class AudioPlayer extends Component {
@tracked isPlaying = false;
- @action
- play() {
+ play = () => {
this.isPlaying = true;
- }
+ };
- @action
- pause() {
+ pause = () => {
this.isPlaying = false;
- }
+ };
+
+
+
+ {{#if (has-block 'title')}}
+ {{yield to='title'}}
+ {{/if}}
+
+
+
+
+
+
}
```
@@ -460,7 +478,7 @@ Our helper will accept the same arguments, so we will use it like this:
```typescript {data-filename="app/helpers/format.ts"}
import Helper from '@ember/component/helper';
import { service } from '@ember/service';
-import type LocaleService from '../services/locale';
+import type LocaleService from ' my-app/services/locale';
interface FormatSignature {
Args: {
@@ -611,7 +629,7 @@ Given an `IntersectionObserverManager` service with an `observe` method, we migh
```typescript {data-filename="app/modifiers/did-intersect.ts"}
import Modifier from 'ember-modifier';
import { service } from '@ember/service';
-import type IntersectionObserverManager from '../services/intersection-observer-manager';
+import type IntersectionObserverManager from 'my-app/services/intersection-observer-manager';
interface DidIntersectSignature {
Args: {
@@ -648,7 +666,7 @@ Yielding back out the same type passed in will use generics, and providing an ap
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 {