How can I get the Id from the URL in an Ember Route? - ember.js

I have a two panel display where I show a list of items on the left, then detail about a selected item on the right (using nested route).
My route looks like this:
Router.map(function() {
this.route('authenticated', {path: '/'}, function() {
this.route('bakery', function() {
this.route('cakes', function() {
this.route('detail', { path: '/:id' });
});
});
});
});
My URL looks like
http://localhost:3333/bakery/cakes/e34b3ce3
When an item is selected, it is set to "active" (temporary property on the model - default is false) and highlighted via an action on the bakery/cakes route. The detail is then shown on the right.
If I refresh the page, the item is no longer highlighted - but the detail is still shown.
Ideally I'd like to use the afterModel() hook in the bakery/cakes route to set that item back to active again, but I've been unable to get the Id to be able to do this.
I've tried the following:
Accepted answer from here
This question doesn't help me as the model will have reloaded and my "active" property will be false so I can't just select where active = true.
I'm using ember 2.5.0. Thanks.

I wonder if it'd be better to architect your structure a bit differently (from what I assume you're doing).
First, load all of the cakes on the authenticated.bakery.cakes route;
import Ember from 'ember';
export default Ember.Route.extend({
model() {
return this.store.findAll('cakes');
}
});
Secondly, show your "full width" cakes list on the authenticated.bakery.cakes.index template (the cake models will be inherited);
<div class="full width cake list">
{{#each model as |cake|}}
{{#link-to "authenticated.bakery.cakes.detail" cake.id}}
{{!-- cake photo --}}
{{cake.name}}
{{!-- other cake details... --}}
{{/link-to}}
{{/each}}
</div>
Next, on your authenticated.bakery.cakes.detail route, load the specific cake along with the list of cakes;
import Ember from 'ember';
export default Ember.Route.extend({
model(params) {
let cakes= this.modelFor('authenticated.bakery.cakes');
return Ember.RSVP.hash({
cakes: cakes,
cake: cakes.findBy('id', params.id)
});
}
});
Finally on the authenticated.bakery.cakes.detail template, show the condensed/narrow list of cakes along with the specific cake details. And using {{link-to}}, the 'active' class will automatically be applied;
<div class="narrow width cake list">
{{#each model.cakes as |cake|}}
{{#link-to "authenticated.bakery.cakes.detail" cake.id}}
{{cake.name}}
{{/link-to}}
{{/each}}
</div>
<div class="cake details">
{{model.cake.name}}
</div>

As another option, change your model active flag on the proper route hooks should work. (I think anyway, haven't done this myself.) On your authenticated.bakery.cakes.detail route;
import Ember from 'ember';
export default Ember.Route.extend({
model(params) {
return this.store.findRecord('cakes', params.id);
},
afterModel(cake) {
cake.set('active', true);
},
actions: {
willTransition() {
this.get('controller.model').set('active', false);
}
}
});

Related

Filtering an array in ember

Ok so I'm fairly new to programing, I know how to run a filter on a JSON Array but I cant seem to figure it out when I'm pulling the data from firebase and viewing it in an Ember app.
this is my route.js code:
import Ember from 'ember';
export default Ember.Route.extend({
model: function() {
return this.store.findAll('user');
}
});
This is my template.hbs code the href="#!" is the generic from materialize for the dropdown buttons:
<div class="list-wrapper col-xs-10 col-xs-offset-1">
<div class="col-xs-12 button-div">
{{#md-btn-dropdown text='Filter' class="col-xs-2" belowOrigin=true}}
<li>Female</li>
<li>Male</li>
<li>All</li>
{{/md-btn-dropdown}}
</div>
{{#each model as |info|}}
<div class="col-xs-3 user-card">
<div class="card-info">
<ul>
<li>Last Name- {{info.lastName}}</li>
<li>First Name- {{info.firstName}}</li>
<li>Gender- {{info.gender}}</li>
<li>{{info.email}} </li>
</ul>
</div>
</div>
{{/each}}
</div>
{{outlet}}
This is my controller.js code which I no is all wrong:
import Ember from 'ember';
export default Ember.Controller.extend({
customFilter: function(gender) {
return function(el) {
var r = el.user;
return r.gender === gender;
};
}
});
and this is my model:
import DS from 'ember-data';
import Ember from 'ember';
export default DS.Model.extend({
lastName: DS.attr('string'),
firstName: DS.attr('string'),
gender: DS.attr('string'),
email: DS.attr('string')
});
I've searched high and low and I'm sure I'm just missing something basic and stupid. What I want is for the dropdown menu to be able to filter and display only female, male or all. Again I'm new to this stuff so I apologize if this is a pretty basic thing. Thank You
What your missing is an action that updates your controller when an item in the dropdown is actually selected.
Some helpful reading:
Triggering changes with actions
Computed Properties
Here's how to put actions in your dropdown component
{{#md-btn-dropdown text='Filter' class="col-xs-2" belowOrigin=true}}
<li><a {{action "filterUpdated" "female"}}>Female</a></li>
<li><a {{action "filterUpdated" "male"}}>Male</a></li>
<li><a {{action "filterUpdated"}}>All</a></li>
{{/md-btn-dropdown}}
In your controller you then need to handle this action like so:
import Ember from 'ember';
export default Ember.Controller.extend({
// the people property is an alias of the model object
// which essentially makes people a synonym for model
// read more http://emberjs.com/api/classes/Ember.computed.html#method_alias
people: Ember.computed.alias('model'),
// holds the currently selected gender, e.g., "female". A null value indicates there is no filter.
currentFilter: null,
/*
filteredPeople is a computed array containing people models.
The array is recomputed every time the model changes or the currentFilter changes,
see the .property() bit at the end.
read more: http://emberjs.com/api/classes/Ember.computed.html#method_filter
*/
filteredPeople: Ember.computed.filter('people', function(person/*, index, array*/) {
// this function is passed each item in the model array, i.e., the person argument
// there's no need to use the index nor array arguments, so I've commented them out
if(this.get('currentFilter') === null) {
// when the filter is null, we don't filter any items from the array
return true;
} else {
// otherwise only return true if the gender matches the current filter
return person.gender === this.get('currentFilter');
}
}).property('people', 'currentFilter'),
actions: {
filterUpdated: function (value) {
if (Ember.isEmpty(value)) {
// an empty value means we should clear the filter
this.set('currentFilter', null);
}
else {
this.set('currentFilter', value);
}
}
}
});
Finally, edit your template to change
{{#each model as |info|}}
to
{{#each filteredPeople as |info|}}
Also at a meta level, don't apologize for asking questions! Everyone is new at something at somepoint, and often asking is the best way to learn. That's what stackoverflow is all about :)
Something like this would work:
gender: 'All',
filteredModel: Ember.computed.filter('model', function(person) {
return person.gender === this.get('gender');
}).property('gender'),
this assumes that it starts on all, and then when the dropdown changes the value of gender, then the filteredModel will get updated. You can then in your hbs file change the result to:
{{#each filteredModel as |info|}}

Update ember component on model change

Using Ember 1.13
I have two nested resources, one of which renders a component based off the model returned by a dynamic route
something like
Router.map(function() {
this.resource('maps', function () {
this.resource('map', { path: '/:map_id' });
});
});
and a template for a map which renders a component
map.hbs
{{some-component model=model}}
{{#each maps as |map|}}
{{#link to 'map' map}}{{map.name}}{{/link-to}}
{{/each}}
when I first hit
/maps/1
the component renders
when I hit one of the links and go to
/maps/2
it appears as if the route never gets hit and the component never updates
is this a result of using link-to or is it true the route is not getting hit because just changing the model inside of a route does cause the same lifecyle hooks to go off?
What is the best way to force this component to rerender?
You're probably doing something wrong.
Here's a basic working example:
<h3>maps.index</h3>
<ul>
{{#each model as |item|}}
<li>
{{link-to item.name 'maps.map' item}}
</li>
{{/each}}
</ul>
<h3>maps.map</h3>
{{link-to "Back to index" 'maps.index'}}
<hr>
{{x-map map=model}}
<h4>components/x-map</h4>
<p>Id: {{map.id}}</p>
<p>name: {{map.name}}</p>
App.Router.map(function() {
this.route('maps', function () {
this.route('map', { path: '/:map_id' });
});
});
App.MapsIndexRoute = Ember.Route.extend({
model: function () {
return this.store.findAll('map');
}
});
App.MapsMapRoute = Ember.Route.extend({
model: function (params) {
return this.store.findRecord('map', params.mapId);
}
});
App.Map = DS.Model.extend({
name: DS.attr('string')
});
Demo: http://emberjs.jsbin.com/voquba/4/edit?html,js,output
Note that instead of passing the whole record into the child route:
{{link-to item.name 'maps.map' item}}
You can pass only its ID:
{{link-to item.name 'maps.map' item.id}}
This is useful when you know the ID but don't have the whole record at hand. Ember Data will look up the record of given ID in the store. If it's not there, it'll fetch it via the route's model hook.

How does one access model data in a router/controller?

Bear with me please, I'm new.
Been breaking my head over this problem and sort of here as last resort. It's about how to access a model's data when that route loads. For instance, when /meals/2 loads, I want a function to run that sets the background of the document using that model's background-image string property. Or when /meals loads, the a function that uses a property of the collection's first item.
Any help on 'the ember way' to do this would be much appreciated.
Menu.hbs
{{#each meal in model}}
<span {{action 'mealSelected' meal.image_large}}>
{{#link-to 'menu.meal' meal tagName="li" class="meal-block" href="view.href"}}
[...]
{{/link-to}}
</span>
{{/each}}
<div id="meal-info-wrapper">
{{outlet}}
</div>
Model:
export default DS.Model.extend({
name: DS.attr('string'),
image: DS.attr('string')
});
Router.js
export default Router.map(function() {
this.route('about');
this.route('menu', { path: '/' }, function() {
this.route('meal', { path: '/meal/:id/:slug' });
});
});
routes/menu.js
export default Ember.Route.extend({
model: function() {
return this.store.find('menu');
},
afterModel: function() {
Ember.$(document).anystretch('temp-images/bg-1.png');
}
});
What I want to do in routes/menu.js for instance would be to have that image url be supplied by the model.
afterModel will run only once the model has been resolved, and the model is passed as an argument. So, based on my understanding of your app, you can adjust your routes/menu example to:
export default Ember.Route.extend({
model: function() {
return this.store.find('menu');
},
afterModel: function(model) {
Ember.$(document).anystretch(model.get('firstObject.image'));
}
});
Correct me if I misunderstood something, what you want to do is:
Change the background image of a DOM element based on a property found
in each Model's record.
Model loading is an async operation, you want to do the image swaping once you are sure the data is loaded. You used the afterModel hook to guarantee that, but that is not enough.
You want to modify the DOM inside your template, but you need to make sure that the template has been rendered. So, the DOM manipulation logic, instead of placing it in afterModel, it belongs to the didInsertElement event that Views have.
I suggest you use a component (its a view too), something like:
// your template
{{#each meal in model}}
{{meal-component content=meal}}
{{/each}}
// the meal-component
didInsertElement: function() {
var imgURLProperty = this.get('content.imgURLProperty');
Ember.$(document).anystretch(imgURLProperty);
}
Of course, you can't copy paste any of that. It just shows you the main mechanic of how you can modify a template based on the properties of a model.

Global CRUD Ember.js

I was wondering if someone could give me brief direction. I'm making an app that I want to be able to take notes from anywhere I'm at in the app (CRUD). I'm rendering my presentations in my application controller using {{render}} but I'm not sure how to put the full crud operations there as well. This is what I have so far:
-- Presentation Controller
import Ember from 'ember';
var PresentationController = Ember.ObjectController.extend({
actions: {
edit: function () {
this.transitionToRoute('presentation.edit');
},
save: function () {
var presentation = this.get('model');
// this will tell Ember-Data to save/persist the new record
presentation.save();
// then transition to the current user
this.transitionToRoute('presentation', presentation);
},
delete: function () {
// this tells Ember-Data to delete the current user
this.get('model').deleteRecord();
this.get('model').save();
// then transition to the users route
this.transitionToRoute('presentations');
}
}
});
export default PresentationController;
-- Presentations Controller
import Ember from 'ember';
var PresentationsController = Ember.ArrayController.extend({
actions: {
sendMessage: function ( message ) {
if ( message !== '') {
console.log( message );
}
}
}
});
export default PresentationsController;
-- Model
import DS from 'ember-data';
var Presentation = DS.Model.extend({
title: DS.attr('string'),
note: DS.attr('string')
});
-- Presentations Route
import Ember from 'ember';
var PresentationsRoute = Ember.Route.extend({
model: function() {
return this.store.find('presentation');
}
});
export default PresentationsRoute;
-- Presentation Route
import Ember from 'ember';
var PresentationRoute = Ember.Route.extend({
model: function (params) {
return this.store.find('presentation', params.id);
}
});
export default PresentationRoute;
-- Application Route
import Ember from 'ember';
export default Ember.Route.extend({
model: function () {
return this.store.find('category');
},
setupController: function (controller, model) {
this._super(controller, model);
controller.set('product', this.store.find('product'));
controller.set('presentation', this.store.find('presentation'));
}
});
-- Application HBS
<section class="main-section">
<div id="main-content">
{{#link-to "presentations.create" class="create-btn expand" tagName="button"}} Add presentation {{/link-to}}
{{render 'presentations' presentation}}
{{outlet}}
</div>
</section>
-- Presentations HBS
{{#each presentation in controller}}
{{#link-to 'presentation' presentation tagName='li'}}
{{presentation.title}}
{{/link-to}}
{{/each}}
{{outlet}}
-- Presentation HBS
{{outlet}}
<div class="user-profile">
<h2>{{title}}</h2>
<p>{{note}}</p>
<div class="btn-group">
<button {{action "edit" }}>Edit</button>
<button {{action "delete" }}>Delete</button>
</div>
</div>
Basically what you're describing is a modal of sorts. It'll be accessible no matter what page (route) you're viewing, and you will be able to perform actions within this modal (creating notes, editing notes, deleting notes, etc) without leaving or affecting the current page being displayed in the background. Essentially, what this means is that you should leave the router alone, since the router is what controls the current page, and you don't want to affect that. You're not going to want to have any {{#link-to}} or transitionTo or transitionToRoute calls, nor any presentation-related routes or outlets.
Instead, you're going to have to handle everything at the controller and view level. This is where components really come in handy, as they're great for encapsulation if you use them correctly. Inside of presentations.hbs, I'd use components to represent each of the presentations:
{{#each presentation in controller}}
{{individual-presentation presentationModelBinding="presentation"}}
{{/each}}
Note that you'll need a corresponding IndividualPresentationComponent object that extends Ember.Component. Going further, inside of individual-presentation.hbs, I'd have code similar to what you have now, but with allowances for various CRUD operations:
{{#if editing}}
{{input value=presentationModel.title}}
{{textarea value=presentationModel.note}}
{{else}}
<h2>{{title}}</h2>
<p>{{note}}</p>
{{/if}}
<div class="btn-group">
{{#if editing}}
<button {{action "save" }}>Save</button>
{{else}}
<button {{action "edit" }}>Edit</button>
{{/if}}
<button {{action "delete" }}>Delete</button>
</div>
Note that the context for a component's template is the component itself, not some other controller. Similarly, actions fired inside of a component's template are direct to the component's actions hash. So your IndividualPresentationComponent will need to look like this somewhat:
IndividualPresentationComponent = Ember.Component.extend({
classNames: ['user-profile'],
actions: {
save: function () {
this.sendAction('save', this.get('presentationModel'));
this.set('editing', false);
},
edit: function () {
this.set('editing', true);
},
delete: function () {
this.sendAction('delete', this.get('presentationModel'));
}
}
});
Notice I'm using sendAction here. This is how components communicate with the outside world. To get this to work, go back your presentations.hbs and intercept the actions like so:
{{#each presentation in controller}}
{{individual-presentation presentationModelBinding="presentation"
save="savePresentation"
delete="deletePresentation"}}
{{/each}}
Here you're basically saying that if the component sends the "save" action, you want to handle it with your controller's "savePresentation" action, and if the component sends the "delete" action, you want to handle it with your controller's "deletePresentation" action. So your presentations-controller.js will need to implement those actions:
var PresentationsController = Ember.ArrayController.extend({
actions: {
savePresentation: function (presentationModel) {
...
},
deletePresentation: function (presentationModel) {
...
},
}
});
And you can delete PresentationController, since all of its functionality is now handled directly by your IndividualPresentationComponent and your PresentationsController.

Accordion wizard with multiple steps in ember

I am using ember to built a "wizard accordion".
Basically what I want is:
an accordion which is always shown
the accordion contains all the steps
one step is active but it is also possibly to change the header of previous steps
each step has its own model (e.g. Selecting from countries in first step, selecting from products in second)
it should be possible to jump back and force between steps
out of all the selections a central model is built which is sent to the server after the final step
The number of steps is dynamic and sometimes certain steps are skipped
The architecture i thought about is have 3 outlets in the application template:
{{previousSteps}}
{{outlet}}
{{nextSteps}}
and always rendering 3 templates in each Route. Each step would have it's own route and controller and the create action of each step would save the shared model to application controller and transition to the next step.
But I guess this solution is by far not the best. Does somebody have a better architecture for this?
Especially how to structure the routes, controller and templates
Thank you
Update:
I am doing it the following way now. I appreciate any comments on my solution.
App.Router.map(function() {
this.resource('steps', { path: '/' });
});
App.StepsRoute = Ember.Route.extend({
model: function() {
return [
{ controllerName: 'countries', templateName: 'countries', items: this.store.find('country') },
{ controllerName: 'products', templateName: 'products', items: this.store.find('product') }
]
}
});
App.StepsController = Ember.ArrayController.extend({
currentStep: "countries",
transitionToStep: function(step) {
this.set('currentStep', step);
},
lookupItemController: function(modelObject) {
return modelObject.controllerName;
}
});
App.CountriesController = Ember.ObjectController.extend({
needs: ['steps'],
currentStep: Ember.computed.oneWay('controllers.steps.currentStep'),
isExpanded: function() {
return "countries" === this.get('currentStep');
}.property('currentStep')
});
Steps.handlebars:
{{#each step in controller}}
<div {{bind-attr class=":step controller.isExpanded:expanded"}}>
{{view Ember.View templateName=step.templateName}}
</div>
{{/each}}
I had to use ObjectController for Countries controller since using an ArrayController as itemController did not work
Based on the way you laid out the UI, I think I'd do something like this in the router:
App.Router.map(function() {
this.resource('wizard', function() {
this.route('step1');
this.route('step2');
this.route('step3');
});
});
Then, have a wizard template that looks like this:
{{#link-to 'wizard.step1'}}Step 1{{/link-to}}
<div {{bind-attr class='step1Expanded}}>{{outlet 'step1'}}</div>
{{#link-to 'wizard.step2'}}Step 2{{/link-to}}
<div {{bind-attr class='step2Expanded}}>{{outlet 'step2'}}</div>
{{#link-to 'wizard.step3'}}Step 3{{/link-to}}
<div {{bind-attr class='step3Expanded}}>{{outlet 'step3'}}</div>
Then, inside each of the step routes, you would need to override renderTemplate so that it will render in the appropriate outlet, like this:
App.WizardStep1Route = Ember.Route.extend({
renderTemplate: function() {
this.render({outlet: 'step1'});
}
});
Finally, inside the WizardController, you would need to add computed properties to handle the step1Expanded logic being applied to the classes so you can know which one is displaying at any given time.
All this will allow you to load different models per step and will also allow you to handle model validation logic inside your routes. For example, if you cannot proceed to step3 until step1 and step2 are completed, you can add logic to handle that inside the step1 and step2 routes.