I'm having trouble with a computed property in Ember.
The problematic item is timeZones, which is set like this:
import Ember from 'ember';
export default Ember.Route.extend({
model(params) {
return Ember.RSVP.hash({
account: this.store.findRecord('account', params.id),
timeZones: this.store.findAll('time-zone'), <------------ timeZones
users: this.store.query('user', { by_account_id: params.id })
});
},
setupController(controller, model) {
this._super(controller, model.account);
controller.set('users', model.users);
controller.set('timeZones', model.timeZones);
}
});
Then I have something called selectedTimeZone which looks like this:
selectedTimeZone: Ember.computed('location.timezone', 'timeZones', function() {
console.log(this.get('timeZones'));
const timeZoneName = this.get('location.timezone');
var result;
this.get('timeZones').forEach(function(timeZone) {
if (timeZone.name === timeZoneName) {
console.log('yes'); // <------------------- never gets here
result = timeZone;
}
});
return result;
}),
The problem is that this.get('timeZones') isn't really accessible inside the component. timeZones makes it to the template just fine. I'm populating a dropdown with timeZones right now. But when I console.log it, it just comes through as Class.
How can I get my hands on timeZones inside this computed property?
actually most of this looks good, however some code is missing. Like is this a component or the controller, and if its a component how is the component invoked?
however one problem is obvious. timeZone obviously is a ember-data record, and so accessing timeZone.name is not a good idea. You should use embers .get(), either timeZone.get('name') or Ember.get(timeZone, 'name').
Next your dependency key is wrong. Because you use the name of each timeZone you should replace the dependency key timeZone with timeZone.#each.name.
Finally a bit a smaller version of your CP using findBy:
selectedTimeZone: Ember.computed('location.timezone', 'timeZones.#each.name', function() {
const timeZoneName = this.get('location.timezone');
return this.get('timeZones').findBy('name', timeZoneName);
}),
If this is not working you should verify that the data are loaded successfully into the store with the ember inspector and verify that you pass the timeZones into the component with timeZones=model.timeZones when calling the component from your controller.
Related
I've got a filter that works once, with whatever values are pre-initialized in, just at initial initiazation, but never updates. editstate is a service; in this case it just provides those exposed variables. So the idea is that when editstate.filterValue and editstate.filterField are changed, the filter should update.
But it doesn't.
I've tried having them be local computed values also, but no dice. The only programmatic thing that works so far is to unrender and rerender the entire component using a handlebars {{#if toggle.
export default Ember.Component.extend({
store: Ember.inject.service(),
editstate: Ember.inject.service('edit-state'),
filteredList: Ember.computed.filter('model', function(current, index, all) {
return current.get(Ember.get(this.get('editstate'), 'filterField')) == Ember.get(this.get('editstate'), 'filterValue');
}),
What am I missing? I don't see any API for forcing a filter to re-compute, nor to tell it more explicitly to watch these values.
Update: I found a terrible hacky way to accomplish my goal: In my service, I create but don't save a record in the model that I'm filtering, just to force updates. Whenever the filter parameters change, I update that record with the current time (millis). And of course it's always filtered out by the filter.
It's ugly, it's probably evil... is there a better way?
Define every variable (that you use in computed function) at computed definition:
export default Ember.Component.extend({
store: Ember.inject.service(),
editstate: Ember.inject.service('edit-state'),
filteredList: Ember.computed('model', 'editstate.filterField', 'editstate.filterValue', function() {
return this.get('model').filter((current) => {
return current.get(Ember.get(this.get('editstate'), 'filterField')) === Ember.get(this.get('editstate'), 'filterValue');
}
}),
...
or even more readable:
export default Ember.Component.extend({
store: Ember.inject.service(),
editstate: Ember.inject.service('edit-state'),
filteredList: Ember.computed('model', 'editstate.filterField', 'editstate.filterValue', function() {
let filterField = Ember.get(this, 'editstate.filterField');
let filterValue = Ember.get(this, 'editstate.filterValue');
return this.get('model').filterBy(filterField, filterValue);
}),
...
Why is it that when I click 'Random', the information in the template isn't reset and the data isn't update?
I have data that I want to display after a REST endpoint is successfully reached. The REST data that's returned is a random database record, so I don't need to worry about randomizing my request or anything. I only need to reach the server via that URL. In this case, the URL is: localhost:8000/api/verses/0
My handlebars template looks like this:
app/templates/verses.hbs
<div id="panel">
<h3>{{model.reference_number}}
<h3>{{model.body}}</h3>
<button {{action "getAnotherVerse"}}>Random</button>
</div>
{{outlet}}
So, when the 'Random' button is clicked, the following should be invoked:
app/controllers/verses.js
import Ember from 'ember';
import DS from 'ember-data';
export default Ember.ObjectController.extend({
actions: {
getAnotherVerse: function() {
this.get('model').reload();
// This is where the text should be reset to the new data.
}
}
});
app/routers/verses.js
import Ember from 'ember';
export default Ember.Route.extend({
model: function() {
return this.store.find('verse', '0');
}
});
When you fire getAnotherVerse you just take the current record(model) and simply reload it to fetch its latest data. I guess you want to call model method of your route once again, so model will be reset and you'll get brand new record from your server.
Move getAnotherVerse to your VersesRoute where you specify model for VersesController and try following code:
# app/routes/verses.js
model: function() {
return this.store.find('verse', '0');
},
actions: {
getAnotherVerse: function() {
this.refresh(); # beforeModel, model, afterModel, setupController will re-fire
}
}
If this still doesn't work, please try this:
# app/routes/verses.js
model: function() {
return this.store.fetch('verse', '0');
},
actions: {
getAnotherVerse: function() {
this.store.unloadAll('verse'); # I assume `verse` is your Model name
this.refresh(); # beforeModel, model, afterModel, setupController will re-fire
}
}
Your telling Ember Data to find the record with id = 0. Just guessing that your API endpoint is treating 0 as a special case and returning a record that does have an actual id.
Because Ember Data is using an identity map under the hood I'm guessing that when you call reload the data is creating a new record in the store. And therefore isn't triggering updates on the record that is being used for the model.
A better approach would be to just use
var that = this;
Ember.$.get('localhost:8000/api/verses/0')
.then(function(data) {
that.set('model', data);
});
You could push the data into the store too http://emberjs.com/guides/models/pushing-records-into-the-store/ and then it would be available if you need to find it by id later.
Another approach would be to create a custom adapter / serializer that could hide some of this, really depends on how your using ember data outside of this use case.
I'm trying to set up a shopping bag model in Ember. The shopping bag will be created on load of the page and saved in LocalStorage using the LocalStorage adapter. At any given time, there should only be one instance of a bag saved, as a user only needs to add products to one shopping bag. My question is this: it seems that I'm being hack-y with my methods of getting and setting data on my bag as Ember data caters to models with more than one instance. Is there a better way to structure/define my bag model that is better suited for one-instance models? Here's my model:
import DS from 'ember-data';
export default DS.Model.extend({
products: DS.hasMany('product', {async: true}),
productCount: function() {
return this.get('products.length');
}.property('products.length')
});
When I want to get the productCount in my template, the only way I can seem to get it to print is use an {{#each}} statement with {{productCount}} nested inside. As there is only one bag, this seems inefficient. In other parts of my code, I need to get the current instance of the bag and act on it. To get this to work, I'm finding all bags, then getting the firstObject, which also seems hack-y:
import Ember from "ember";
export default Ember.ArrayController.extend({
actions: {
addToBag: function(model) {
this.store.find('bag').then(function(bags) {
var bag = bags.get('firstObject');
bag.get('products').then(function(products) {
products.pushObject(model);
bag.save();
});
});
}
}
});
My application route uses the bag as its model, and sets up the controller:
import Ember from "ember";
var ApplicationRoute = Ember.Route.extend({
activate: function() {
var store = this.store;
store.find('bag').then(function(bags) {
var existing_bag = bags.get('firstObject');
// If there isn't already a bag instantiated, make one and save it
if(typeof existing_bag === 'undefined') {
var new_bag = store.createRecord('bag');
new_bag.save();
}
});
},
model: function() {
return this.store.find('bag');
},
setupController: function(controller,model) {
controller.set('content', model);
}
});
Any ideas here to make this more efficient? I don't want this to fester into code that is messy. Thanks so much in advance!
If that is your BagController above, it should be an ObjectController instead.
The reason why you're having to get the first object is because find fetches all items of that model type in your store. You may only have one, but find doesn't know that unless you provide an id and if this bag hasn't been stored in your database, it may not have one yet.
Instead of fetching the bag model from the store, I would link you Bag and Products controllers with needs, then simply access the model property of that controller. You can even set up an alias to be able to access it quickly.
For example:
import Ember from "ember";
export default Ember.ArrayController.extend({
needs: 'bag',
bag: Ember.computed.alias("controllers.bag.model").
actions: {
addToBag: function(model) {
this.get('bag').get('products').pushObject(model);
}
}
});
The ApplicationRoute is only fired once, when you're app first boots up so you don't need to check if there's already a bag model present. The only one that will be there is the one you create. You should do this in the model hook. You don't need to set content as the model. It'll be hooked up like that by default.
import Ember from "ember";
export default Ember.Route.extend({
model:function() {
return this.store.createRecord('bag');
}
});
If you may want the ability to have multiple shopping bags going forward you could try using the 'singleton' approach Discourse follows: https://github.com/discourse/discourse/blob/master/app/assets/javascripts/discourse/mixins/singleton.js
Basically it adds 'current' property that you can use as your single instance throughout your code.
However, if you won't have a need to have multiple instances this may not be the best choice.
I have a very basic route setup that allows me to first show "all" records for some object. Then if the user selects a dropdown they can filter this down using a date.
I recently upgraded to RC2 and realized that "needs" has replaced or will soon replace controllerFor.
I'm curious how I can use "needs" in the below situation where I need the nested / inner route for "records.date" to change the content for the parent "records" route when a date is selected.
What is missing from below is that inside the App.RecordsDateRoute I need to change the content of the "records" controller to be a new filter (by date this time) and everything I seem to do just dumps the handlebars template and show nothing -even when I try to use something simple like
this.controllerFor("records").set('content', App.Record.find(new Date(model.loaded)))
from within the setupController method of the RecordsDateRoute
App.Router.map(function(match) {
return this.resource("records", { path: "/" }, function() {
return this.route("date", { path: "/:date_loaded" });
});
});
App.RecordsController = Ember.ArrayController.extend({
selected: 0,
dates: Ember.computed(function() {
return App.Date.find();
}).property()
});
App.RecordsIndexRoute = Ember.Route.extend({
model: function() {
this.controllerFor("records").set("selected", 0);
return App.Record.find();
}
});
App.RecordsDateRoute = Ember.Route.extend({
model: function(params) {
//the controllerFor below seems to be working great ... but what about needs?
this.controllerFor("records").set("selected", params.date_loaded);
return App.Date.create({ loaded: params.date_loaded });
}
});
With rc2, instances of other controllers can be retrieved via "controllers.controllerName", in you case it would be this.get('controllers.records').
The "needs" declaration makes the referencing controller sort of import the reference to the other controller; in your case, the date controller would be:
App.RecordsDateRoute = Ember.Route.extend({
needs: ['records'],
model: function(params) {
this.get("controllers.records").set("selected", params.date_loaded);
return App.Date.create({ loaded: params.date_loaded });
}
});
Regarding App.Record.find(new Date(model.loaded)), find() expects an id or an object whose keys and values will be used to filter the collection of models, but you're giving it a Javascript date.
Did you mean App.Record.find(new App.Date(model.loaded)), or maybe something like App.Record.find({ loaded: model.loaded }) /* assuming it's already a Date */?
There is also an initController(controller, model) method in the route called , maybe you could use that instead of "overloading" the model() method with too many responsibilities. http://emberjs.com/api/classes/Ember.Route.html#method_setupController
I recently upgraded to RC2 and realized that "needs" has replaced or will soon replace controllerFor.
To access another controller from route hooks you should continue to use controllerFor. Controller.needs is for communication between controllers, it replaces the now deprecated use of controllerFor method on controllers. AFAIK there is no plan to deprecate controllerFor on ember Routes.
I'm curious how I can use "needs" in the below situation where I need the nested / inner route for "records.date" to change the content for the parent "records" route when a date is selected.
For this use case it would be best to stick with controllerFor. It is possible to use needs this way, by specifying that App.RecordsDateController needs = ['records'] you could access the records controller via controller.get('controllers.records') from within your route's setupController hook.
What is missing from below is that inside the App.RecordsDateRoute I need to change the content of the "records" controller to be a new filter (by date this time) and everything I seem to do just dumps the handlebars template and show nothing -even when I try to use something simple like this.controllerFor("records").set('content', App.Record.find(new Date(model.loaded))) from within the setupController method of the RecordsDateRoute
App.RecordsDateRoute = Ember.Route.extend({
model: function(params) {
return App.Date.create({ loaded: params.date_loaded });
},
setupController: function(controller, model) {
var recordsController = this.controllerFor("records");
// Moved this from model hook, since here you are 'setting up a controller'
recordsController.set("selected", model.date_loaded);
// Set query based on current route's model
var query = { loaded: model.loaded };
recordsController.set("content", App.Record.find(query));
}
});
So, I'm having some issues with Ember's new router. I'm trying to save and later return to the current path for a given dynamic segment, so my urls might look like
#/inventory/vehicle/1001
Which can then branch off into
#/inventory/vehicle/1001/details
#/inventory/vehicle/1001/photos
#/inventory/vehicle/1001/description
etc. I need a way to return to the most recent route. The Ember guides have a method for this here:
http://emberjs.com/guides/routing/redirection/
The problem with this method is that by creating the "/choose" route and assigning it to "/", this overwrites the standard "/inventory/vehicle/1001" route. For instance, if I were to try to create a linkTo a vehicle like so:
{{#linkTo "vehicle" vehicle}}
Then Ember will throw an error because the "vehicle" route no longer exists. Instead, it must be set to:
{{#linkTo "vehicle.choose" vehicle}}
Which works, activates the VehicleChooseRoute and everything. Except, since "vehicle.choose" is technically a child of "vehicle", the #linkTo ONLY has an active class applied when the current route is
#/inventory/vehicle/1001
Which instantaneously redirects to the latest filter, and so it's basically never on. So basically I'm trying to figure out a way around this. I tried changing the path of "vehicle.choose" to be the standard path (#/inventory/vehicle/1001/choose) so it doesn't overwrite the "vehicle" route, and then setting up VehicleRoute like so:
Case.Router.map(function() {
this.resource('inventory', function(){
this.route('review');
this.route('sheets');
this.resource('vehicle', { path: '/vehicle/:vehicle_id' }, function(){
this.route('choose');
this.route('details');
this.route('consignor');
this.route('additional');
this.route('price');
this.route('dmv');
this.route('expenses');
this.route('description');
this.route('tasks');
});
});
});
App.VehicleRoute = Ember.Route.extend({
model: function(params) {
return Case.Vehicle.find(params.vehicle_id);
},
setupController: function(controller, model) {
model.set('active', true);
},
redirect: function() {
this.transitionTo('vehicle.choose');
}
})
Case.VehicleChooseRoute = Ember.Route.extend({
redirect: function() {
var lastFilter = this.controllerFor('vehicle').get('lastFilter');
this.transitionTo('vehicle.' + (lastFilter || 'details'));
}
});
But the problem that arises from this (aside from feeling rather hacked together) is that redirect replaces the entire template that would normally be rendered by "vehicle" so I only get the subview. So that's not an option.