From 98e463052a0bcf988c9f80e5082052f0a37bf985 Mon Sep 17 00:00:00 2001 From: Tommy Carter Date: Tue, 15 Jul 2025 12:01:49 -0500 Subject: [PATCH 1/9] Updated autotracking in depth to use gjs --- .../in-depth-topics/autotracking-in-depth.md | 145 ++++++++++++------ 1 file changed, 98 insertions(+), 47 deletions(-) diff --git a/guides/release/in-depth-topics/autotracking-in-depth.md b/guides/release/in-depth-topics/autotracking-in-depth.md index 0ff6f514ce..925b2f4da8 100644 --- a/guides/release/in-depth-topics/autotracking-in-depth.md +++ b/guides/release/in-depth-topics/autotracking-in-depth.md @@ -9,11 +9,7 @@ When Ember first renders a component, it renders the initial _state_ of that component - the state of the instance, and state of the arguments that are passed to it: -```handlebars {data-filename=app/components/hello.hbs} -{{this.greeting}}, {{@name}}! -``` - -```js {data-filename=app/components/hello.js} +```gjs {data-filename=app/components/hello.gjs} import Component from '@glimmer/component'; export default class HelloComponent extends Component { @@ -29,11 +25,19 @@ export default class HelloComponent extends Component { return 'Hola'; } } + + } ``` -```handlebars {data-filename=app/templates/application.hbs} - +```gjs {data-filename=app/templates/application.gjs} +import '../components/hello.gjs'; + + ``` When Ember renders this template, we get: @@ -59,11 +63,12 @@ Trackable values are values that: We can do this by marking the field with the `@tracked` decorator: -```js {data-filename=app/components/hello.js} +```gjs {data-filename=app/components/hello.gjs data-diff="+2,-5,+6"} import Component from '@glimmer/component'; import { tracked } from '@glimmer/tracking'; export default class HelloComponent extends Component { + language = 'en'; @tracked language = 'en'; get greeting() { @@ -76,6 +81,10 @@ export default class HelloComponent extends Component { return 'Hola'; } } + + } ``` @@ -95,20 +104,10 @@ Tracked properties can be updated like any other property, using standard JavaScript syntax. For instance, we could update a tracked property via an action, as in this example component. -```handlebars {data-filename=app/components/hello.hbs} -{{this.greeting}}, {{@name}}! - - -``` - -```js {data-filename=app/components/hello.js} +``` gjs { data-filename=app/components/hello.gjs data-diff="+3,+18,+19,+20,+21,+25,+26,+27,+28,+29,+30" } import Component from '@glimmer/component'; import { tracked } from '@glimmer/tracking'; -import { action } from '@ember/object'; +import { on } from '@ember/modifier'; export default class HelloComponent extends Component { @tracked language = 'en'; @@ -124,10 +123,19 @@ export default class HelloComponent extends Component { } } - @action - updateLanguage(event) { + updateLanguage = (event) => { this.language = event.target.value; - } + }; + + } ``` @@ -140,10 +148,10 @@ Another way that a tracked property could be updated is asynchronously, if you're sending a request to the server. For instance, maybe we would want to load the user's preferred language: -```js +``` gjs { data-filename=app/components/hello.gjs data-diff="+6,+7,+8,+9,+10,+11,+12,+13,+14,+15" } import Component from '@glimmer/component'; import { tracked } from '@glimmer/tracking'; -import { action } from '@ember/object'; +import { on } from '@ember/modifier'; export default class HelloComponent extends Component { constructor() { @@ -168,6 +176,20 @@ export default class HelloComponent extends Component { return 'Hola'; } } + + updateLanguage = (event) => { + this.language = event.target.value; + }; + + } ``` @@ -180,16 +202,26 @@ app. So far we've only shown tracked properties working through getters, but tracking works through _methods_ or _functions_ as well: -```js +``` gjs { data-diff="+17,+18,+19,+20,+21,+24,+25,+26,+27" } import Component from '@glimmer/component'; import { tracked } from '@glimmer/tracking'; -import { action } from '@ember/object'; +import { on } from '@ember/modifier'; export default class HelloComponent extends Component { + constructor() { + super(...arguments); + + fetch('/api/preferences') + .then(r => r.json()) // convert the response to a JS object + .then(response => { + this.language = response.preferredLanguage; + }); + } + @tracked language = 'en'; @tracked supportedLanguages = ['en', 'de', 'es']; - isSupported(language) { + isSupported = (language) => { return this.supportedLanguages.includes(language); } @@ -207,6 +239,20 @@ export default class HelloComponent extends Component { return 'Hola'; } } + + updateLanguage = (event) => { + this.language = event.target.value; + }; + + } ``` @@ -246,27 +292,29 @@ export default class ApplicationRoute extends Route { } ``` -```js {data-filename=app/controllers/application.js} -import Controller from '@ember/controller'; -import { action } from '@ember/object'; - -export default class ApplicationController extends Controller { - @action - updateName(title, name) { - this.model.title = title; - this.model.name = name; - } +```gjs {data-filename=app/templates/application.gjs} +import Component from '@glimmer/component'; +import { on } from '@ember/modifier'; +import { fn } from '@ember/helper'; + +// TODO: Q: Is this the right way to do stateful route components??? +// Do we still need to use the RouteTemplate wrapper from 'ember-route-template'? +export default class ApplicationRouteComponent extends Component { + updateName = (title, name) => { + this.args.model.title = title; + this.args.model.name = name; + }; + + } ``` -```handlebars {data-filename=app/templates/application.hbs} -{{@model.fullName}} - - -``` - As long as the properties are tracked, and accessed when rendering the template directly or indirectly, everything should update as expected @@ -362,7 +410,8 @@ you cache (or "memoize") a getter by simply marking it as `@cached`. With this in mind, let's introduce caching to `aspectRatio`: -```js +``` js { data-diff="-1,+2,+10,-22,+23,-28,+29" } +import { tracked } from '@glimmer/tracking'; import { cached, tracked } from '@glimmer/tracking'; let count = 0; @@ -383,11 +432,13 @@ let photo = new Photo(); console.log(photo.aspectRatio); // 1.5 console.log(count); // 1 console.log(photo.aspectRatio); // 1.5 +console.log(count); // 2 console.log(count); // 1 photo.width = 800; console.log(photo.aspectRatio); // 2 +console.log(count); // 3 console.log(count); // 2 ``` From 0518d16855212b21550f9bd7467b68ae88d1793f Mon Sep 17 00:00:00 2001 From: Tommy Carter Date: Fri, 18 Jul 2025 07:30:07 -0500 Subject: [PATCH 2/9] Updated patterns for components to use gjs --- .../patterns-for-components.md | 283 +++++++++++------- 1 file changed, 169 insertions(+), 114 deletions(-) diff --git a/guides/release/in-depth-topics/patterns-for-components.md b/guides/release/in-depth-topics/patterns-for-components.md index 077e61a07c..7fa29a4b98 100644 --- a/guides/release/in-depth-topics/patterns-for-components.md +++ b/guides/release/in-depth-topics/patterns-for-components.md @@ -13,7 +13,7 @@ provided. For instance, if you wanted to create a tooltip icon that had a standard icon and class, you could do it like so: -```javascript {data-filename=app/components/tooltip.js} +```gjs {data-filename=app/components/tooltip.gjs} import Component from '@glimmer/component'; export default class TooltipComponent extends Component { @@ -24,20 +24,24 @@ export default class TooltipComponent extends Component { get tooltipClass() { return this.args.tooltipClass + ' tooltip'; } -} -``` -```handlebars {data-filename=app/components/tooltip.hbs} -
- - {{@content}} -
+ +} ``` Now when called like so: -```handlebars - +```gjs +import Tooltip from '../components/tooltip.gjs'; + + ``` The result will be: @@ -65,14 +69,16 @@ The positioning of `...attributes` matters, with respect to the other attributes in the element it is applied to. Attributes that come _before_ `...attributes` can be overridden, but attributes that come _after_ cannot: -```handlebars -

- ... -

+```gjs + ``` There is one exception to this, which is the `class` attribute. `class` will get @@ -80,10 +86,12 @@ merged, since its more often the case that users of the component want to _add_ a class than completely override the existing ones. For `class`, the order of `...attributes` will determine the order of merging. Putting it before: -```handlebars -

- Hello {{@friend}}, I'm {{this.name}}! -

+```gjs + ``` Results in: @@ -96,10 +104,12 @@ Results in: And putting it after: -```handlebars -

- Hello {{@friend}}, I'm {{this.name}}! -

+```gjs + ``` Results in: @@ -117,134 +127,179 @@ The most frequently used of these is `aria-describedby` and `aria-labelledby`. In these cases, make sure to declare _all_ of the relevant values in the correct order. -```handlebars - +```gjs +import MyInput from '../components/my-input.gjs'; + + ``` To learn more about `aria` roles and accessibility in Ember, check out the [Accessibility Guide](../../reference/accessibility-guide/). -## Contextual Components +## Conditional Component Rendering -The [`{{component}}`](https://api.emberjs.com/ember/release/classes/Ember.Templates.helpers/methods/component?anchor=component) -helper can be used to defer the selection of a component to runtime. The -`` syntax always renders the same component, while using the -`{{component}}` helper allows choosing a component to render on the fly. This is -useful in cases where you want to interact with different external libraries -depending on the data. Using the `{{component}}` helper would allow you to keep -different logic well separated. +Sometimes you want to defer the selection of a component to runtime. Suppose you have a blog post model that contains a string `postType` indicating that the post is either a "root" or a "reply". +Below is an example of choosing different components for +displaying different kinds of posts. First, define your two components: -The first parameter of the helper is the name of a component to render, as a -string. So `{{component 'blog-post'}}` is the same as using ``. -The real value of [`{{component}}`](https://api.emberjs.com/ember/release/classes/Ember.Templates.helpers/methods/component?anchor=component) -comes from being able to dynamically pick the component being rendered. Below is -an example of using the helper as a means of choosing different components for -displaying different kinds of posts: +```gjs {data-filename=app/components/root-post.gjs} + +``` -```handlebars {data-filename=app/components/foo-component.hbs} -

Hello from foo!

-

{{this.post.body}}

+```gjs {data-filename=app/components/reply-post.gjs} + ``` -```handlebars {data-filename=app/components/bar-component.hbs} -

Hello from bar!

-
{{this.post.author}}
+Then, you can choose which to render based on the data: + +```gjs {data-filename=app/templates/index.gjs} +import RootPost from '../components/root-post.gjs'; +import ReplyPost from '../components/reply-post.gjs'; + +// returns either RootPost or ReplyPost (default: RootPost) +function getPostComponent(postType) { + return postType === 'reply' ? ReplyPost : RootPost; +} + + ``` -```handlebars {data-filename=app/templates/index.hbs} -{{#each this.myPosts as |post|}} - {{!-- either foo-component or bar-component --}} - {{component post.postType post=post}} -{{/each}} +This is great when `RootPost` and `ReplyPost` take the same arguments, like `author` and `body` in the above example. But what if the components accept different arguments? One way would be to move the selection conditional into the template, like so: + +```gjs {data-filename=app/templates/index.gjs} +import RootPost from '../components/root-post.gjs'; +import ReplyPost from '../components/reply-post.gjs'; + +const eq = (a, b) => a === b; + + ``` -or +## Contextual Components -```handlebars {data-filename=app/templates/index.hbs} -{{#each this.myPosts as |post|}} - {{!-- either foo-component or bar-component --}} - {{#let (component post.postType) as |Post|}} - - {{/let}} -{{/each}} -``` +The built-in [`{{component}}`](https://api.emberjs.com/ember/release/classes/Ember.Templates.helpers/methods/component?anchor=component) +helper allows us to partially apply some component arguments. + +The first parameter of the helper is a component to render. So `{{component BlogPost}}` is the same as using ``. Any named arguments are passed as arguments to the component, so `{{component BlogPost author="Sam"}}` is the same as ``. + +The `component` helper is often used when yielding components to blocks. For example the layout for a SuperForm component might be implemented as: -Or, for example the layout for a SuperForm component might be implemented as: +```gjs {data-filename=app/components/super-form.gjs} +import SuperInput from './super-input.gjs'; +import SuperTextarea from './super-input.gjs'; +import SuperSubmit from './super-input.gjs'; -```handlebars {data-filename=app/components/super-form.hbs} -
- {{yield (hash - Input=(component "super-input" form=this model=this.model) - Textarea=(component "super-textarea" form=this model=this.model) - Submit=(component "super-submit" form=this model=this.model) - )}} -
+ ``` And be used as: -```handlebars {data-filename=app/templates/index.hbs} - - - - - -``` +```gjs {data-filename=app/templates/index.gjs} +import SuperForm from '../components/super-form.gjs'; -When the parameter passed to `{{component}}` evaluates to `null` or `undefined`, -the helper renders nothing. When the parameter changes, the currently rendered -component is destroyed and the new component is created and brought in. + +``` -Picking different components to render in response to the data allows you to -have a different template and behavior for each case. The `{{component}}` helper -is a powerful tool for improving code modularity. +The `{{component}}` helper is a powerful tool for improving code modularity. ### Contextual helpers & modifiers We can even use helpers and modifiers in the same way. Let's extend the SuperForm component: -```handlebars {data-filename=app/components/super-form.hbs} -
- {{yield (hash +```gjs {data-filename=app/components/super-form.gjs} +import SuperInput from './super-input.gjs'; +import SuperTextarea from './super-input.gjs'; +import SuperSubmit from './super-input.gjs'; +import superIsValid from '../helpers/super-is-valid.js'; +import superErrorFor from '../helpers/super-error-for.js'; +import superAutoResize from '../modifiers/super-auto-resize.js'; - Input=(component "super-input" form=this model=this.model) - Textarea=(component "super-textarea" form=this model=this.model) - Submit=(component "super-submit" form=this model=this.model) + ``` And be used as: -```handlebars {data-filename=app/templates/index.hbs} - +```gjs {data-filename=app/templates/index.gjs} +import SuperForm from '../components/super-form.gjs'; + + ``` These APIs open the doors for the creation of new, more powerful UI abstractions. - -## Learn More - -To keep this guide concise, we built a separate site for [component patterns in Ember](https://emberjs-1.gitbook.io/ember-component-patterns/). -This project also addresses anti-patterns and accessibility for components. - -[![ember-component-patterns](/images/ember-component-patterns.png)](https://emberjs-1.gitbook.io/ember-component-patterns/) From 0b29b30be4026634ee98ca03a4881a7bec2d4205 Mon Sep 17 00:00:00 2001 From: Tommy Carter Date: Tue, 29 Jul 2025 09:47:12 -0500 Subject: [PATCH 3/9] Updated patterns for actions to use gjs --- .../in-depth-topics/patterns-for-actions.md | 580 +++++++++++------- 1 file changed, 370 insertions(+), 210 deletions(-) diff --git a/guides/release/in-depth-topics/patterns-for-actions.md b/guides/release/in-depth-topics/patterns-for-actions.md index d753a80d61..34e20b222b 100644 --- a/guides/release/in-depth-topics/patterns-for-actions.md +++ b/guides/release/in-depth-topics/patterns-for-actions.md @@ -12,27 +12,27 @@ to confirm in order to trigger some action. We'll call this the `ButtonWithConfirmation` component. We can start off with a normal component definition, like we've seen before: -```handlebars {data-filename=app/components/button-with-confirmation.hbs} - - -{{#if this.isConfirming}} -
- - -
-{{/if}} -``` - -```js {data-filename=app/components/button-with-confirmation.js} +```gjs {data-filename=app/components/button-with-confirmation.gjs} import Component from '@glimmer/component'; import { tracked } from '@glimmer/tracking'; export default class ButtonWithConfirmationComponent extends Component { @tracked isConfirming = false; + + } ``` @@ -45,92 +45,113 @@ more about tracked properties in the [Autotracking In-Depth](../autotracking-in- guide. Next, we need to hook up the button to toggle that property. We'll -do this with an _action_: +do this with an _action_. +An action is a function that is bound to the component instance. There are two common ways to bind a function to a component instance. The first way is to assign an arrow function to a property, like this: + +```js + // in component + myAction = () => { + // do something here + }; +``` + +The second way is to import and use the `@action` decorator, like this: + +```js +import { action } from '@ember/object'; +// ... -```js {data-filename=app/components/button-with-confirmation.js} + // in component + @action + myAction() { + // do something here + } +``` + +These two are functionaly equivalent and are used interchangably throughout these guides. + +So, to hook up the button to toggle the `isConfirming` property, we'll define a `launchConfirmDialog` action: + +```gjs {data-filename=app/components/button-with-confirmation.gjs} import Component from '@glimmer/component'; import { tracked } from '@glimmer/tracking'; -import { action } from '@ember/object'; +import { on } from '@ember/modifier'; export default class ButtonWithConfirmationComponent extends Component { @tracked isConfirming = false; - @action - launchConfirmDialog() { + launchConfirmDialog = () => { this.isConfirming = true; - } -} -``` - -```handlebars - + }; -{{#if this.isConfirming}} -
- - - -{{#if this.isConfirming}} -
- +```gjs {data-filename=app/components/button-with-confirmation.gjs} +import Component from '@glimmer/component'; -{{#if this.isConfirming}} -
- {{yield this.confirmValue}} +export default class ButtonWithConfirmationComponent extends Component { + submitConfirm = async() => { + if (this.args.onConfirm) { + // call `onConfirm` with a second argument + await this.args.onConfirm(this.confirmValue); + } - -