Skip to content

Commit

Permalink
Merge pull request #20728 from emberjs/cleanup-action-modifier
Browse files Browse the repository at this point in the history
  • Loading branch information
kategengler authored Aug 7, 2024
2 parents 85fa73f + 9cb8cf5 commit f971719
Show file tree
Hide file tree
Showing 7 changed files with 48 additions and 156 deletions.
97 changes: 33 additions & 64 deletions packages/@ember/-internals/glimmer/lib/component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -205,8 +205,8 @@ declare const SIGNATURE: unique symbol;
default for older editions of Ember (pre 3.15).
Below is the documentation for Classic components. If you are looking for the
API documentation for Template-only or Glimmer components, it is
[available here](/ember/release/modules/@glimmer%2Fcomponent).
API documentation for Template-only or Glimmer components, it is [available
here](/ember/release/modules/@glimmer%2Fcomponent).
## Defining a Classic Component
Expand Down Expand Up @@ -404,9 +404,9 @@ declare const SIGNATURE: unique symbol;
<div id="ember1" class="ember-view empty"></div>
```
If you want to add a class name for a property which evaluates to true and
and a different class name if it evaluates to false, you can pass a binding
like this:
If you want to add a class name for a property which evaluates to true and and
a different class name if it evaluates to false, you can pass a binding like
this:
```app/components/my-widget.js
import Component from '@ember/component';
Expand Down Expand Up @@ -467,9 +467,9 @@ declare const SIGNATURE: unique symbol;
### Other HTML Attributes
The HTML attribute section of a component's tag can be set by providing an
`attributeBindings` property set to an array of property names on the component.
The return value of these properties will be used as the value of the component's
HTML associated attribute:
`attributeBindings` property set to an array of property names on the
component. The return value of these properties will be used as the value of
the component's HTML associated attribute:
```app/components/my-anchor.js
import Component from '@ember/component';
Expand All @@ -488,8 +488,8 @@ declare const SIGNATURE: unique symbol;
<a id="ember1" class="ember-view" href="http://google.com"></a>
```
One property can be mapped on to another by placing a ":" between
the source property and the destination property:
One property can be mapped on to another by placing a ":" between the source
property and the destination property:
```app/components/my-anchor.js
import Component from '@ember/component';
Expand Down Expand Up @@ -522,9 +522,9 @@ declare const SIGNATURE: unique symbol;
<a id="ember1" class="ember-view" href="http://bing.com"></a>
```
Note that the `href` attribute is ultimately set to `http://bing.com`,
despite it having attribute binidng to the `url` property, which was
set to `http://google.com`.
Note that the `href` attribute is ultimately set to `http://bing.com`, despite
it having attribute binidng to the `url` property, which was set to
`http://google.com`.
Namespaced attributes (e.g. `xlink:href`) are supported, but have to be
mapped, since `:` is not a valid character for properties in Javascript:
Expand Down Expand Up @@ -662,27 +662,33 @@ declare const SIGNATURE: unique symbol;
## Handling Browser Events
Components can respond to user-initiated events in one of three ways: passing
actions with angle bracket invocation, adding event handler methods to the
component's class, or adding actions to the component's template.
There are two ways to handle user-initiated events:
### Passing Actions With Angle Bracket Invocation
### Using the `on` modifier to capture browser events
For one-off events specific to particular instance of a component, it is possible
to pass actions to the component's element using angle bracket invocation syntax.
In a component's template, you can attach an event handler to any element with the `on` modifier:
```handlebars
<MyWidget {{action 'firstWidgetClicked'}} />
<MyWidget {{action 'secondWidgetClicked'}} />
<button {{on 'click' this.doSomething}} />
```
In this case, when the first component is clicked on, Ember will invoke the
`firstWidgetClicked` action. When the second component is clicked on, Ember
will invoke the `secondWidgetClicked` action instead.
This will call the function on your component:
```js
import Component from '@ember/component';
export default class ExampleComponent extends Component {
doSomething = (event) => {
// `event` is the native click Event
console.log('clicked on the button');
};
});
```
Besides `{{action}}`, it is also possible to pass any arbitrary element modifiers
using the angle bracket invocation syntax.
See the [Guide on Component event
handlers](https://guides.emberjs.com/release/components/component-state-and-actions/#toc_html-modifiers-and-actions)
and the [API docs for `on`](../Ember.Templates.helpers/methods/on?anchor=on)
for more details.
### Event Handler Methods
Expand Down Expand Up @@ -752,43 +758,6 @@ declare const SIGNATURE: unique symbol;
* `dragEnd`
* `drop`
### `{{action}}` Helper
Instead of handling all events of a particular type anywhere inside the
component's element, you may instead want to limit it to a particular
element in the component's template. In this case, it would be more
convenient to implement an action instead.
For example, you could implement the action `hello` for the `person-profile`
component:
```app/components/person-profile.js
import Component from '@ember/component';
export default Component.extend({
actions: {
hello(name) {
console.log("Hello", name);
}
}
});
```
And then use it in the component's template:
```app/templates/components/person-profile.hbs
<h1>{{@person.name}}</h1>
<button {{action 'hello' @person.name}}>
Say Hello to {{@person.name}}
</button>
```
When the user clicks the button, Ember will invoke the `hello` action,
passing in the current value of `@person.name` as an argument.
See [Ember.Templates.helpers.action](/ember/release/classes/Ember.Templates.helpers/methods/action?anchor=action).
@class Component
@extends Ember.CoreView
@uses Ember.TargetActionSupport
Expand Down
1 change: 1 addition & 0 deletions packages/@ember/-internals/glimmer/lib/helpers/action.ts
Original file line number Diff line number Diff line change
Expand Up @@ -275,6 +275,7 @@ export const ACTIONS = new WeakSet();
```
@method action
@deprecated
@for Ember.Templates.helpers
@public
*/
Expand Down
27 changes: 2 additions & 25 deletions packages/@ember/component/template-only.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,32 +34,9 @@ import { type Opaque } from '@ember/-internals/utility-types';
import { templateOnlyComponent as glimmerTemplateOnlyComponent } from '@glimmer/runtime';

/**
* Template-only components have no backing class instance, so this in their
* Template-only components have no backing class instance, so `this` in their
* templates is null. This means that you can only reference passed in arguments
* via named argument syntax (e.g. `{{@arg}}`):
*
* ```hbs
* {{!--
* This does not work, since `this` does not exist
* --}}
* <label for="title">Title</label>
* <Input @value={{this.value}} id="title" />
* ```
*
* Additionally, the mut helper generally can't be used for the same reason:
*
* ```hbs
* {{!-- This does not work --}}
* <input
* value={{this.value}}
* onkeyup={{action (mut this.value) target="value"}}
* />
* ```
*
* Since Octane, a template-only component shares a subset of features that are
* available in `@glimmer/component`. Such component can be seamlessly
* "upgraded" to a Glimmer component, when you add a JavaScript file alongside
* the template.
* (e.g. `{{@arg}}`).
*/
// eslint-disable-next-line @typescript-eslint/no-empty-interface
export interface TemplateOnlyComponent<S = unknown> extends Opaque<S> {}
Expand Down
71 changes: 11 additions & 60 deletions packages/@ember/object/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,31 +54,21 @@ export default EmberObject;
```js
import Component from '@ember/component';
import { action, set } from '@ember/object';
import { tracked } from '@glimmer/tracking';
import { action } from '@ember/object';
export default class Tooltip extends Component {
@tracked isShowing = false;
@action
toggleShowing() {
set(this, 'isShowing', !this.isShowing);
this.isShowing = !this.isShowing;
}
}
```
```hbs
<!-- template.hbs -->
<button {{action this.toggleShowing}}>Show tooltip</button>
{{#if isShowing}}
<div class="tooltip">
I'm a tooltip!
</div>
{{/if}}
```
Decorated actions also interop with the string style template actions:
```hbs
<!-- template.hbs -->
<button {{action "toggleShowing"}}>Show tooltip</button>
<button {{on "click" this.toggleShowing}}>Show tooltip</button>
{{#if isShowing}}
<div class="tooltip">
Expand All @@ -90,27 +80,10 @@ export default EmberObject;
It also binds the function directly to the instance, so it can be used in any
context and will correctly refer to the class it came from:
```hbs
<!-- template.hbs -->
<button
{{did-insert this.toggleShowing}}
{{on "click" this.toggleShowing}}
>
Show tooltip
</button>
{{#if isShowing}}
<div class="tooltip">
I'm a tooltip!
</div>
{{/if}}
```
This can also be used in JavaScript code directly:
```js
import Component from '@ember/component';
import { action, set } from '@ember/object';
import { tracked } from '@glimmer/tracking';
import { action } from '@ember/object';
export default class Tooltip extends Component {
constructor() {
Expand All @@ -121,37 +94,15 @@ export default EmberObject;
document.addEventListener('click', this.toggleShowing);
}
@tracked isShowing = false;
@action
toggleShowing() {
set(this, 'isShowing', !this.isShowing);
this.isShowing = !this.isShowing;
}
}
```
This is considered best practice, since it means that methods will be bound
correctly no matter where they are used. By contrast, the `{{action}}` helper
and modifier can also be used to bind context, but it will be required for
every usage of the method:
```hbs
<!-- template.hbs -->
<button
{{did-insert (action this.toggleShowing)}}
{{on "click" (action this.toggleShowing)}}
>
Show tooltip
</button>
{{#if isShowing}}
<div class="tooltip">
I'm a tooltip!
</div>
{{/if}}
```
They also do not have equivalents in JavaScript directly, so they cannot be
used for other situations where binding would be useful.
@public
@method action
@for @ember/object
Expand Down
3 changes: 0 additions & 3 deletions packages/@ember/routing/hash-location.ts
Original file line number Diff line number Diff line change
Expand Up @@ -146,9 +146,6 @@ export default class HashLocation extends EmberObject implements EmberLocation {
Given a URL, formats it to be placed into the page as part
of an element's `href` attribute.
This is used, for example, when using the {{action}} helper
to generate a URL based on an event.
@private
@method formatURL
@param url {String}
Expand Down
2 changes: 1 addition & 1 deletion packages/@ember/routing/history-location.ts
Original file line number Diff line number Diff line change
Expand Up @@ -246,7 +246,7 @@ export default class HistoryLocation extends EmberObject implements EmberLocatio
}

/**
Used when using `{{action}}` helper. The url is always appended to the rootURL.
Formats url to be placed into href attribute.
@private
@method formatURL
Expand Down
3 changes: 0 additions & 3 deletions packages/@ember/routing/none-location.ts
Original file line number Diff line number Diff line change
Expand Up @@ -110,9 +110,6 @@ export default class NoneLocation extends EmberObject implements EmberLocation {
Given a URL, formats it to be placed into the page as part
of an element's `href` attribute.
This is used, for example, when using the {{action}} helper
to generate a URL based on an event.
@private
@method formatURL
@param {String} url
Expand Down

0 comments on commit f971719

Please sign in to comment.