Start Date Release Date Release Versions PR link Tracking Link Stage Teams
2/13/2024
Accepted
  • CLI
  • Data
  • Framework
  • Learning
  • Steering
  • TypeScript

Deprecate (action) template helper and {{action}} modifier.

Summary

The (action) template helper and {{action}} modifier was common pre-Octane. Now that we have native classes and the {{on}} modifier, we no longer need to use (action) or {{action}}

Motivation

Remove legacy code with confusing semantics.

This is a part of Deprecating Ember Classic (pre-Octane).

Transition Path

This was written in the Octave vs Classic cheatsheet

that content here

Before (pre-Octane)

// parent-component.js
import Component from '@ember/component';

export default Component.extend({
  count: 0
});
{{!-- parent-component.hbs --}}
{{child-component count=count}}
Count: {{this.count}}
// child-component.js
import Component from '@ember/component';

export default Component.extend({
  actions: {
    plusOne() {
      this.set('count', this.get('count') + 1);
    }
  }
});
{{!-- child-component.hbs --}}
<button type="button" {{action "plusOne"}}>
  Click Me
</button>

After (post-Octane)

// parent-component.js
import Component from '@glimmer/component';
import { tracked } from '@glimmer/tracking';
import { action } from '@ember/object';

export default class ParentComponent extends Component {
  @tracked count = 0;

  @action plusOne() {
    this.count++;
  }
}
{{!-- parent-component.hbs --}}
<ChildComponent @plusOne={{this.plusOne}} />
Count: {{this.count}}
{{!-- child-component.hbs --}}
<button type="button" {{on "click" @plusOne}}>
  Click Me
</button>

But what we could put in the deprecation app:

Scenario: action is passed a string

Before:

<button type="button" {{action "plusOne"}}>
  Click Me
</button>

After

<button type="button" {{on 'click' this.plusOne}}>
  Click Me
</button>

or, if plusOne is passed in as an argument

<button type="button" {{on 'click' @plusOne}}>
  Click Me
</button>

If the plusOne action is in an actions object, it needs to move out:

Before:

import Component from '@glimmer/component';

export default class Demo extends Component {
    actions = {
        plusOne() {
           /* ... */ 
        }
    }
}

or

import Component from '@ember/component';

export default class Demo extends Component {
    actions = {
        plusOne() {
           /* ... */ 
        }
    }
}

or

import Component from '@ember/component';

export default Component.extend({
    actions: {
        plusOne() {
           /* ... */ 
        }
    }
})

After:

import Component from '@glimmer/component';
import { action } from '@ember/object';

export default class Demo extends Component {
    @action
    plusOne() {
       /* ... */ 
    }
}

Note that @action is completely different from (action) or {{action}} (and is partly a motivator for deprecating (action) and {{action}}, to reduce ambiguity).

@action is binds the this on the method to the instance of the class.

Scenario: action is passed a function reference

Before:

<SomeComponent @update={{action this.plusOne}} />

After

<SomeComponent @update={{this.plusOne}} />

Scenario: action is passed parameters

Before:

<SomeComponent @update={{action this.plus 1}} />

After:

<SomeComponent @update={{fn this.plus 1}} />

Scenario: action is used with mut

Before:

<SomeComponent @update={{action (mut @value.property)}} />

After:

// parent.js
import Component from '@glimmer/component';
import { action } from '@ember/object';

export default class SomeComponent extends Component {
    @action
    handleUpdate(value) {
        this.args.property = value; 
    }
}
{{! parent.hbs }}
<SomeComponent @update={{this.handleUpdate}} />

Related, Combining function arguments with action functions

When removing (action) or {{action}} with a string name, you'll also need to verify that there are no send calls with that same string.

How We Teach This

The guides already cover how to invoke functions in the modern way.

Remove: https://api.emberjs.com/ember/5.6/classes/Ember.Templates.helpers/methods/action?anchor=action

Drawbacks

Older code will stop working once the deprecated code is removed.

Alternatives

  • adding an import so folks can keep using action in gjs. I don't think we should do this because we want to clean up antiquated patterns, rather than encourage their continued existence.

Unresolved questions

  • Could there be a codemod? Potentially for action usage that references this.properties. For string actions, it's impossible.