Ember Octane Logout button disappears - ember.js

I am upgrading to Ember Octane and I modified the template HBS to call the component JS. When I use Ember Classic, the Logout button exists and works. But, when I convert to Octane then the Logout button disappears. o_O
What is the correct way to display a Logout button on the template HBS? Note: I do not have a component HBS file. Is this required in Ember Octane?
Classic Template HBS snippet:
<li><a href="#" onclick={{action "logout"}}>Logout</a></li>
Octane Template HBS snippet:
<li><a href="#" onclick={{on "submit" this.logout}}>Logout</a></li>
Octane Component JS (works with Classic Template, but not Octane Template):
import Component from '#ember/component';
import { inject as service } from '#ember/service';
import { action } from '#ember/object';
export default class Navigation extends Component {
#service session
#service currentClient
#action
logout(ev) {
ev.preventDefault();
this.session.invalidate();
}
}

The issue here is the improper usage of on modifier. The on modifier has to be used on the element space whereas in your snippet, the on modifier was used as a helper.
Should be used like:
<button {{on "click" this.logout}}> Logout </button>
This means, we are asking the framework to register the provided function this.logout for the click event.
and not as:
<button onclick={{on "click" this.logout}}> Logout </button>
This guide should help to migrate from classic event handling to the newest Octane way.

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.

EmberJS show router model in application.hbs

I am working with Ember.js, and I am trying to make my pages display the title of the page just below the navbar. To make this work I have tried to use model hook, and show it in the application.hbs file.
So far I have tried variations of this:
routes/contact.js
import Route from '#ember/routing/route';
export default class ContactRoute extends Route {
model() {
return 'Title of page';
}
}
templates/application.hbs
<div>
<NavBar />
<div class="pageTitle">
<h2>
<p>{{#model}}</p>
</h2>
</div>
<div class="body">
{{outlet}}
</div>
</div>
I've mostly tried to mess around with #model in application.hbs things like outlet.#model. But all of these attempts have resulted in empty titles or Template Compiler Errors.
Does anyone have a solution for this? Preferably one that does not involve jquery?
If I understand correctly what you are trying to accomplish, it is a good use case for services.
You need a couple parts. A service to keep track of the page title, and then you need to inject that service in the application controller so the template has access to the service, and also to inject the page title service in the routes so you can update the page title in the respective hooks.
Page service
import Service from '#ember/service';
import { tracked } from '#glimmer/tracking';
export default class extends Service {
#tracked title = "Your App"
}
Application controller and template
import Controller from '#ember/controller';
import { inject as service } from '#ember/service';
export default class ApplicationController extends Controller {
#service pageTitle;
}
<h1>Welcome to {{this.pageTitle.title}}</h1>
<br>
<br>
{{outlet}}
<LinkTo #route="my-route">my route</LinkTo>
<br>
<br>
MyRoute route updating the page title value in a model hook
import Route from '#ember/routing/route';
import { inject as service } from '#ember/service';
export default class extends Route {
#service pageTitle;
model() {
this.pageTitle.title = "My Route"
}
}
I have put it all together in an interactive Ember Twiddle demo.
Hope this helps!
Since you have created a new page (route) named contact, the UI part of the page has to be in the corresponding template file, i.e., templates/contact.hbs and not templates/application.hbs as the templates/contact.hbs file can only access the #model of routes/contact.js
ie., the below markup has to in templates/contact.hbs file and will be displayed when accessing the page at http://localhost:4200/contact
<div class="pageTitle">
<h2>
<p>{{#model}}</p>
</h2>
</div>
Also, note that the markup present in the templates/contact.hbs file will be rendered in the place of application template's {{outlet}} (see here)
For a detailed reference, check this twiddle

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

Send actions to the Application controller in EmberJS

I'm trying to toggle a global property on the application controller, by clicking a button on one of the templates. I've read some stuff on action bubbling but can't it to work.
Here's the property and action on the Application controller
export default Ember.Controller.extend({
isAuthenticated: true,
actions: {
logIn: function(){
this.toggleProperty('isAuthenticated');
}
}
});
And here's the action with a login.hbs template file (I'll turn this into a proper button soon)
<span {{action 'logIn'}}>
{{#link-to 'boards' class="btn-l bg-blue white db mtl link bn w-100"}}Login{{/link-to}}
</span>
How could I ensure the action toggles the property on the Application Controller?
In your login controller,you need to inject application controller.
import Ember from 'ember';
export default Ember.Controller.extend({
appCont:Ember.inject.controller('application')
});
and in login.hbs you need to specify which target will receive the method call.
<button {{action 'logIn' target=appCont}}> Login </button>
In this <button {{action 'logIn'}}> Login </button> , context of a template is login controller, actions used this way will bubble to login route when the login controller does not implement the specified action. Once an action hits a login route, it will bubble through the route hierarchy.
Reference: http://emberjs.com/api/classes/Ember.Templates.helpers.html#toc_specifying-a-target
EDIT: If you want to call functions available in Controller inside Component then you need to pass actions toComponent`
Login.hbs
{{component-a logIn=(action 'logIn') }}
component-a.hbs
<button {{action (action logIn)}}> LogIn</button>

Testing an Ember.js component using hasBlock

I have this component set up (stripped down to its minimum):
<a href={{href}}>{{text}}</a>
{{#if template}}
<button {{action "toggleSubmenu"}} class="global-nav-toggle-submenu">
<i class="caret-down"></i>
</button>
{{/if}}
And this test:
test('has a toggle button if a submenu is present', function(assert) {
var component = this.subject({
template: Ember.HTMLBars.compile('<ul class="global-nav-submenu"></ul>')
});
assert.ok(this.$().find('.global-nav-toggle-submenu').length);
});
This runs fine, but I get a deprecation notice from Ember:
Accessing 'template' in <app-name#component:global-nav-item::ember540> is deprecated. To determine if a block was specified to <app-name#component:global-nav-item::ember540> please use '{{#if hasBlock}}' in the components layout.
When I change the template to use hasBlock instead:
<a href={{href}}>{{text}}</a>
{{#if hasBlock}}
<button {{action "toggleSubmenu"}} class="global-nav-toggle-submenu">
<i class="caret-down"></i>
</button>
{{/if}}
The test fails. Logging this.$() to the console shows that the hbs template file seems to be ignoring the template I'm adding programmatically.
In the test, I've tried swapping out template with block, layout, hasBlock, content, etc., with no success. I've tried initializing the subject with hasBlock: true as well.
And the logic runs fine when loading a page in the regular development app that has a block applied:
{{#global-nav-item text="Hello" href="#"}}
Some Content
{{/global-nav-item}}
Is there some other way that I should be setting up my components in unit tests in order to test this logic?
In general you should use the "new" style component integration tests for this type of thing.
See the following resources:
Example from the ember-radio-button addon
Nice blog post - Ember component integration tests
Update: Based on the blog post linked from Robert's answer, here is the new test code in tests/integration/components/global-nav-item-test.js:
import hbs from 'htmlbars-inline-precompile';
import { moduleForComponent, test } from 'ember-qunit';
moduleForComponent('global-nav-item', 'Integration | Component | global-nav-item', {
integration: true
});
test('has a toggle button if a submenu is present', function(assert) {
this.render(hbs`
{{#global-nav-item text="Hello" href="/"}}
<ul class="le-global-nav-submenu"></ul>
{{/global-nav-item}}
`);
assert.ok(this.$('.global-nav-toggle-submenu').length);
});