How to get Ember to detect a checkbox on change along with a glimmer tracked? - ember.js

In ember js version 3.2.6, how do we get a checkbox to do extra logic on value changed?
Example, I have a checkbox (a toggle true/false) for updateServer.
The existing code is using a glimmer #tracked and this work fine for showing some instant UI modification as can be seen on the application.hbs.
Value of update server:
{{updateServer}}
But then I need to add some logic on value change, see onCheckboxChange. That function is called, but it seem the value of updateServer is not the one after click. It's the older one, before click. See picture.
How do I get the latest value of updateServer?
application.js
import Controller from '#ember/controller';
import { tracked } from '#glimmer/tracking';
import { action } from '#ember/object';
export default class ApplicationController extends Controller {
#tracked updateServer = false;
#action
onCheckboxChange() {
//some extra logic
console.log(`at controller, updateServer is: ${this.updateServer}`);
}
}
application.hbs
{{page-title 'EmberSimple'}}
<label>
<Input #type='checkbox' #checked={{this.updateServer}} {{on 'change' onCheckboxChange}}/>
Update Server</label>
<br />
Value of update server:
{{updateServer}}
<br />
{{outlet}}

One simple solution is to just not use <Input but a primitive <input. This could look like this:
<input type="checkbox" checked={{this.updateServer}} {{on "click" this.onCheckboxChange}} />
However then you manually need to assign the new value to the tracked property:
#action
onCheckboxChange(event) {
this.updateServer = event.target.checked;
console.log(`at controller, updateServer is: ${this.updateServer}`);
}

Related

Add Extra Actions to LinkTo in Octane

I have a dropdown menu with links, when the links are clicked I'd like the menu to close.
Something like (a11y/i18n truncated):
<ul class="menu">
<li>
<LinkTo #route="myprofile">
Profile
</LinkTo>
</li>
<li>
<LinkTo #route="logout">
Logout
</LinkTo>
</li>
</ul>
I'd like to add an additional click handler to the link to, something like:
<ul class="menu">
<li>
<LinkTo #route="myprofile" {{on "click" this.closeMenu}}>
Profile
</LinkTo>
</li>
<li>
<LinkTo #route="logout" {{on "click" this.closeMenu}}>
Logout
</LinkTo>
</li>
</ul>
However this makes the LinkTo useless as it reloads the page as if following a link instead of transitioning to a new route. We're currently doing this using hember-link-action, but I'd love to find a more idiomatic way to approach this problem.
If you need to perform additional logic, you may implement redirect in an action instead of using the LinkTo helper. To do so, you need to inject RouterService into your component and then call its transitionTo method. Something like:
export default class ExampleComponent extends Component {
#service router;
#action
navigate(route) {
this.menuExpanded = false;
this.router.transitionTo(route);
}
}
Note that there also exist the transitionTo() method from Route and transitionToRoute() from Controller that behave like the LinkTo helper. But those methods are deprecated now, and using RouterService is a recommended idiomatic way of doing transitions in js code.
I've written a component to mostly handle this, but I'm quite certain there are more edge cases in LinkTo then I've covered (for example it doesn't cover a passed model or list of models). I called this <LinkToWithAction /> and it looks like:
<a href={{this.href}} class={{if this.isActive "active"}} {{on "click" this.navigate}} >
{{yield}}
</a>
import Component from '#glimmer/component';
import { inject as service } from '#ember/service';
import { action } from '#ember/object';
export default class LinkToWithActionComponent extends Component {
#service router;
get href() {
return this.router.urlFor(this.args.route, {
queryParams: this.queryParams,
});
}
get isActive() {
return this.router.isActive(this.args.route);
}
get queryParams() {
return this.args.queryParams ?? {};
}
#action
navigate(evt) {
evt.preventDefault();
this.args.action();
this.router.transitionTo(this.args.route, {
queryParams: this.queryParams,
});
}
}
and it is called as:
<LinkToWithAction
#route="mymaterials"
#action={{set this.isOpen false}}
#queryParams={{hash course=null}}
>
{{t "general.myprofile"}}
</LinkToWithAction>
This is made more annoying by this issue with transitionTo that adds unset queryParams to the URL when called which effects the public router service. The built in component uses the private internal router where this behavior doesn't exist, and it may be worth using that private service, but for now we're going to live with passing the query params.

How to programatically add component via controller action in Ember 3.x

I want to add add multiple copies of a component via js and pass different params in it. The code should execute via controller action of click on a button in the template. The solution for 2.x doesn't work in Ember version 3.x(pre-octane). Can anybody please help. I can't render plain html as I am using other addon of ember in the component.
Basically you want an array with the params in the js, and then a {{#each loop invoking the components. Something like this:
#tracked componentParams = new TrackedArray();
#action showThem() {
this.componentParams.push({ value="foo" });
this.componentParams.push({ value="bar" });
this.componentParams.push({ value="baz" });
}
{{#each this.componentParams as |params|}}
<MyComponent #value={{params.value}} />
{{/each}}
<button type="button" {{on "click" this.showThem}}>Show Them</button>
(this uses TrackedArray from tracked-built-ins).

Getting error after migrating to Ember octane

I am using Ember 3.16.3 and i am getting this error on the following code :
Error: Assertion Failed: You must pass a function as the second
argument to the on modifier
//login.hbs
<form {{on "submit" this.login}}>
<Input type="email" placeholder="email" #value={{this.email}} />
<button type="submit">login</button>
</form>
.
//login.js
import Route from '#ember/routing/route';
import { tracked } from '#glimmer/tracking';
import { action } from '#ember/object';
export default class LoginRoute extends Route {
#tracked email = '';
#action
login(event) {
event.preventDefault();
// do some operations ...
}
}
As specified in the error, the on modifier should receive a valid function to execute. As mentioned in the guides,
If you add the {{action}} helper to any HTML DOM element, when a user clicks the element, the named event will be sent to the template's corresponding component or controller.
This holds good for on modifier or any values that are used in the template. You can think of routes to be a part where we fetch data for the corresponding page. Any other backing property or computation has to be defined inside a controller or component to be used in the template.
Hence, moving your login action to a Controller will solve this issue. Additionally, you need to move email to the controller as well, or you won't see updates to it work correctly.

Ember component call an action in a route or controller

I have a component the main purpose of which is to display a row of items.
Every row has a delete button to make it possible to delete a row. How is possible to pass an action from a template to the component which will trigger an action in a router ?
Here is the template using the component:
#templates/holiday-hours.hbs
{{#each model as |holidayHour|}}
{{holiday-hour holiday=holidayHour shouldDisplayDeleteIcon=true}}
{{/each}}
Here is the component template:
# templates/components/holiday-hour.hbs
...
div class="col-sm-1">
{{#if shouldDisplayDeleteIcon}}
<button type="button" class="btn btn-danger btn-sm mt-1" {{action 'deleteHoliday' holiday}}>
<span class="oi oi-trash"></span>
</button>
{{/if}}
</div>
I'm using the same component to display a row and to create a new item (holiday-hour).
I'm using ember 3.1.2
Thank you
You have to send the actions up from the component to the route. The main way to do this is by adding actions to your component that "send" the action to the parent. Once the action is sent you have to tell the component what action on the route to trigger by passing in the action as a parameter. Below is an example of how to do this.
Component js
# components/holiday-hour.js
...
actions: {
deleteHoliday(){
this.sendAction('deleteHoliday');
}
}
Template for route
#templates/holiday-hours.hbs
...
{{#each model as |holidayHour|}}
{{holiday-hour holiday=holidayHour shouldDisplayDeleteIcon=true deleteHoliday='deleteHoliday'}}
{{/each}}
Route js
#routes/holiday-hours.js
...
actions: {
deleteHoliday(){
//code to delete holiday
}
}
I will try to give a general answer because your question is not giving enough/all info regarding the route actions etc. Long answer short, using closure functions. Assuming this is your route js file routes/holiday-hours.js
import Route from '#ember/routing/route';
export default Route.extend({
model(){ /*... some code */ },
setupController(controller){
this._super(controller);
controller.set('actions', {
passToComponent: function(param) { //.... function logic }
})
}
});
Note: in the above snippet, I'm using setupController to create actions. Alternatively, you can put the actions inside a controller file otherwise actions directly inside the route will throw an error.
So I want the action passToComponent to be called from the component. This is what you do to make it accessible inside the component.
{{#each model as |holidayHour|}} {{holiday-hour holiday=holidayHour shouldDisplayDeleteIcon=true callAction=(action 'passToComponent')} {{/each}}
Now we have passed the action to the component and here's how to call it from the component. Note: I have added a param just to show that it can take a param when called within the component.
import Component from '#ember/component';
export default Component.extend({
actions: {
deleteHoliday: ()=> {
this.get('callAction')() /*Pass in any params in the brackets*/
}
}
});
You will also see demonstrations using sendAction which is rather old and acts more of an event bus that is not very efficient. Read more from this article

Ember integration test fails after click event

I have this integration test:
test('can change chord text', function(assert) {
this.render(hbs`{{chart-editor-chord chord=chord}}`);
this.$().click();
assert.ok(!!this.$('.chord-input').length);
});
but the assertion fails, the component template looks like this:
<div {{action 'changeChord'}} class="measure-chord chord-big">
{{#if chord.editing}}
<input type="text" value="{{chord.name}}" class="chord-input">
{{else}}
{{chord.name}}
{{/if}}
</div>
and the component code:
import Ember from 'ember';
export default Ember.Component.extend({
store: Ember.inject.service(),
actions: {
changeChord() {
this.chord.set('editing', true);
}
}
});
I'm updating the chord model in the changeChord() action and it does work if I test in the browser, but the integration test fails. So, does this change in the model have to be rendered synchronously to the template? I tried using wait() in the test but that doesn't make a difference. So how should I test this?
While I'm trying to create a twiddle for you, I found three things:
Where do you create chord mock in your test?
You are not sending event to the correct html component. Use this.$('.measure-chord') or this.$('.chord-big').
Instead of this.chord.set you should use this.get('chord').set. Actually Ember.set(this, 'chord.isEditing', ...) is even better.
And bonus: You don't need a div wrapper, component does this for you.
twiddles:
working copy
without div
It looks like your click helper is clicking the div that your component.js controls instead of the initial div in your template. If you specify the div in your click helper it should work:
this.$('.measure-chord').click();