I am setting up a "saved item" feature for logged-in users. I have the modeling worked out already (it relates the saved item list to both the user and products). But I am having trouble with how to have a computed property on my saveditem model. The model:
// saveditem.js model
export default DS.Model.extend({
user: belongsTo('user'),
product: belongsTo('product'),
dateAdded: attr('string')
});
I am currently using the product id as the id of that model.
I need a computed property on that model because anytime a product is shown on the site, the UI needs to reflect whether the item is already in the saveditem ember-data store or not.
Example of if the item is not on the list (but can be added):
Example of an item that is on the list (but can be removed):
I was thinking that on my userprofile service which manages the user's data across the app, I could have a computed property that outputs an array of ids from the saveditem model:
savedItemsList: computed('WHAT.IS.DEPENDENT.KEY?.[]', function() {
return this.get('store').peekAll('saveditem').map(item => item.id);
}),
And then in the template I could use a composable helper to see if the item being displayed is in the list or not:
{{#if (contains product.id userprofile.savedItemsList)}}
...show already-saved button...
{{else}}
...show save button...
{{/if}}
The question: what would the dependent key be for this computed property? OR...is this method stinky and and there's a better way to do it?
I have tried:
savedItemsList: computed('store.saveditem.[]', function() {
(after of course injecting the store). Seems like the obvious one but it doesn't update when records are added or removed from the saveditem store. Also tried 'store.saveditems.[]', permutations with and without array brackets, and NO dependent key - all no worky.
Makes me wonder whether this is not possible for a good reason ;) I might be "fighting the framework" here. Any help appreciated!
You can introduce computed property which will return all the items in the store and use that property for your savedItemsList computed property.
allSavedItemsList: computed(function() {
return this.get('store').findAll('saveditem');
}),
savedItemsList: computed('allSavedItemsList.[]',function() {
return this.get('store').peekAll('saveditem').map(item => item.id);
}),
Related
I have a route that displays 5 categories. Each category is intended to only have one child. I created a child route and pass the selected category type id as queryparamter. In the child route model() I use the parameter with store.queryRecord() to query the backend to either return the record that matches that type OR nothing. This seems to work fine as long as a record exists. The problem I am running into is if I select a category that doesn’t have a child record. When nothing is returned from queryRecord the template continues to display the previous data. I can see the network request completing successfully and it returns an empty array. If I refresh the page the template correctly shows that there is no model data.
I’ve been struggling all day trying to find a way to refresh the template when the model no longer has a record. I have a feeling I am going at this backwards, I would be grateful for any pointers.
Parent:
export default Ember.Route.extend(AuthenticatedRouteMixin,{
user: Ember.inject.service('user'),
model() {
var user = this.get('user');
return this.store.findAll('strategic-priority',{ location: user.get('selectedLocationId'), year: user.get('selectedYearId') });
}
});
HBS
{{#each model as |strategic-priority|}}
{{#link-to 'priority-area.goal' (query-params priorityArea=strategic-priority.id) class="list-group-item"}} {{strategic-priority.label}} - {{strategic-priority.text}} {{/link-to}}
{{/each}}
Child:
export default Ember.Route.extend({
user: Ember.inject.service('user'),
queryParams: {
priorityArea: {
refreshModel: true,
replace: false,
}
},
model(params) {
Ember.Logger.debug(params); //I see this is in the console so I know this code is being called each time
var user = this.get('user');
return this.store.queryRecord('goal',{ location: user.get('selectedLocationId'), year: user.get('selectedYearId'),priority: params.priorityArea});
}
});
What you could try is to wrap the template that displays a category with {{#if hasModel}}. So something like
{{#if hasModel}}
... your template ...
{{/if}}
and then in the controller for your route
hasModel: Ember.computed.notEmpty('model')
I don’t know if this is the correct answer but my workaround was to change the model to use store.query rather than queryRecord. In my template I did a {{#each model as |xx|}} even though I only expected one record.
I was also able to avoid the situation in most cases by using hasMany. It is a bit cumbersome to gather the data on the backend (at least at my skill level with php and zf2) but in the end it seems to work pretty nice.
Thanks for advice.
My situation is I have a table where the user can add suppliers, and edit any existing ones (so there are potentially multiple records). Three of the fields (Type, Name, Version) come from a lookup object returned by the API (which is one table in the backend database).
Before clicking 'edit'
In edit mode
The thing is, I need these select elements to be "chained" (or cascading), but since they're populated from the same object, it's more like the selection of "Type" will filter the options available for "Name" and likewise selecting Name will further restrict the options available for Version.
However, since this is just one record being edited, these selects are in an {{#each supplier in suppliers}} block to generate the rows, and show selects if that record's isEditing property is true, so the value or selection is the per-record value, e.g. supplier.type and not a single property on the whole controller.
I've tried to come up with multiple ways to do this, but so far haven't found a solution to cascading dropdowns with multiple records since that means the value of any one select is dependent on the record.
I think I could get the option filtering to work if I knew how to reference the value of say the Type dropdown from within the controller, but then again it's conceivable that two records could be in edit mode at once, so modifying any property on the controller to populate the selects would affect the others too, and that's not good. I just really wanted to figure this out so I didn't have to pop up a modal dialog to edit the record.
You should use components to handle each row seperately.
Let's say that you have something like this:
{{#each suppliers as |supplier|}}
// .. a lot of if's, selects and others
{{/each}}
If you find yourself using {{#each}} helper and your block passed to that helper is more than one line, than it's a good sign you probably need a component there.
If you create a component named, let's say, SupplierRow you could make it as follow:
module export Ember.Component({
editing: Ember.computed.alias('model.isEditing'),
types: Ember.computed('passedTypes', function() {
// .. return types array for that exact supplier
}),
names: Ember.computed('passedNames', 'model.type', function() {
// .. return names array for that exact supplier based on possibleNames and model.type
}),
versions: Ember.computed('passedVersions', 'model.type', 'model.name', function() {
// .. return versions array for that exact supplier based on possibleVersions and model.type and model.name
}),
actions: {
saveClicked() {
this.sendAction('save', this.get('model'));
}
}
});
The template would basically look similiary to what you have currently in your {{#each}} helper. It would be rendered something like this:
{{#each suppliers as |supplier|}}
{{supplier-row model=supplier possibleTypes=types possibleNames=names possibleVersions=versions save="save"}}
{{/each}}
Seems like you are using an old version of Ember wich allows context switching in {{#each}} helpers. Assuming that, you can set itemController for each iteration and handle selectable values for each row separately:
{{#each suppliers itemController="supplierController"}}
// this == supplierController, this.model == supplier
{{/each}}
So inside the supplierController you can calculate select content for each single supplier. You can also access main controller from item controller by this.parentController property.
How could I prevent the itemView from being removed and re-rendered back in place when iterating over a controller's arrangedContent, if the observed model's property doesn't change value?
Short version below, using a blog post App.Post as example model:
Controller:
sortProperties: ['createdAt']
Template:
{{each arrangedContent}}
{{title}}
{{body}}
{{/each}}
objectAt(0).reload() causes the corresponding item to be removed and inserted back in the same place. The problem is that the itemView loses the previous state, so the result is some bad user interaction.
I've traced it to this sequence of calls:
1. retrieved record is pushed onto the store
2. notifyPropertyChange('data') onto the record
3. propertyWillChange('createdAt')
4. arrayWillChange() -> this causes the removal of the item, even though
the value of createdAt didn't change
5. arrayDidChange() -> reinserts the object, it gets re-rendered in the list
I'd need it to not to do the remove/insert sequence when the property hasn't changed actual value.
One thought was to queue the removes/inserts in arrayWillChange/arrayDidChange and not call them if the value didn't change. I suspect that this would cause additional sync problems.
You're probably using find or all for your collection which makes it a live filter (which removes/adds it to collections on demand. You can move the collection to a plain array, which will take away this logic. You can do this from the route, or controller
Route
App.FooRoute = Em.Route.extend({
model: function(){
return this.store.find('foo').then(function(foos){
return foos.toArray();
});
}
});
Controller
App.FooController = Em.ArrayController.extend({
simpleModel: function(){
return this.get('model').toArray();
}.property()
});
Then in your template
{{#each item in simpleModel}}
{{/each}}
Note: when you do this, if you add a new item to the store, you will need to manually add it to the collection.
I've got a fairly simple form but it should never carry any state with it. I started reading an older discussion about how you can use itemControllerClass to get a "singleton" class created each time you enter the route.
If I want to use this how would I plug this into the template and wire it up from the parent controller?
Here is what I'm guessing you would do from the javascript side
App.FooController = Ember.Controller.extend({
itemControllerClass: 'someclass_name'
});
The only "must have" is that I need to have access to the parent route params from the child singleton controller.
Any guidance would be excellent -thank you in advance!
Update
Just to be clear about my use case -this is not an ArrayController. I actually just have a Controller (as shown above). I don't need to proxy a model or Array of models. I'm looking for a way to point at a url (with a few params) and generate a new instance when the route is loaded (or the parent controller is shown).
I'm doing this because the template is a simple "blank form" that doesn't and shouldn't carry state with it. when the user saves the form and I transition to the index route everything that happened in that form can die with it (as I've cached the data in ember-data or finished my $.ajax / etc)
Anyone know how to accomplish this stateless behavior with a controller?
I'm betting you're talking about this discussion. It's one of my personal favorite discoveries related to Ember. The outcome of it was the itemController property of an ArrayController; I use it all the time. The basic gist of it is, when you're iterating over an array controller, you can change the backing controller within the loop. So, each iterating of the loop, it will provide a new controller of the type you specify. You can specify the itemController as either a property on the ArrayController, or as an option on the {{#each}} handlebars helper. So, you could do it like this:
App.FooController = Ember.ArrayController.extend({
itemController: 'someclass'
});
App.SomeclassController = Ember.ObjectController.extend({});
Or, like this:
{{#each something in controller itemController='someclass'}}
...
{{/each}}
Within the itemController, you can access the parent controller (FooController, in this case), like:
this.get('parentController');
Or, you can specify the dependency using needs, like you ordinarily would in a controller. So, as long as the params are available to the parentController, you should be able to access them on the child controller as well.
Update
After hearing more about the use case, where a controller's state needs to reset every time a transition happens to a particular route, It sounds like the right approach is to have a backing model for the controller. Then, you can create a new instance of the model on one of the route's hooks; likely either model or setupController.
From http://emberjs.com/api/classes/Ember.ArrayController.html
Sometimes you want to display computed properties within the body of an #each helper that depend on the underlying items in content, but are not present on those items. To do this, set itemController to the name of a controller (probably an ObjectController) that will wrap each individual item.
For example:
{{#each post in controller}}
<li>{{title}} ({{titleLength}} characters)</li>
{{/each}}
App.PostsController = Ember.ArrayController.extend({
itemController: 'post'
});
App.PostController = Ember.ObjectController.extend({
// the `title` property will be proxied to the underlying post.
titleLength: function() {
return this.get('title').length;
}.property('title')
});
In some cases it is helpful to return a different itemController depending on the particular item. Subclasses can do this by overriding lookupItemController.
For example:
App.MyArrayController = Ember.ArrayController.extend({
lookupItemController: function( object ) {
if (object.get('isSpecial')) {
return "special"; // use App.SpecialController
} else {
return "regular"; // use App.RegularController
}
}
});
The itemController instances will have a parentController property set to either the the parentController property of the ArrayController or to the ArrayController instance itself.
I might be using this all wrong, but:
I've got an ArrayController representing a collection of products. Each product gets rendered and there are several actions a user could take, for example edit the product title or copy the description from a different product.
Question is: how do you interact with the controller for the specific product you're working with? How would the controller know which product was being edited?
I also tried to create an Ember.Select with selectionBinding set to "controller.somevar" but that also failed.
I think the most important thing you need to do, is first move as much logic as you can away from the views, and into controllers.
Another thing that would be useful in your case, is to have an itemController for each product in the list. That way, you can handle item specific logic in this item controller.
I don't have enough information to understand your architecture, so I will make a few assumptions.
Given you have the following ProductsController:
App.ProductsController = Ember.ArrayController.extend();
You need to create a ProductController that will be created to wrap every single product on its own.
App.ProductController = Ember.ObjectController.extend();
You need to modify your template as follows:
{{#each controller itemController="product"}}
<li>name</li>
{{/each}}
Now every product in your list will have its own ProductController, which can handle one product's events and will act as the context for every list item.
Another option:
If you will only be handling one product at a time, you can use routes to describe which product you are working with:
App.Router.map(function() {
this.resource('products', { path: '/products' }, function() {
this.resource('product', { path: '/:product_id' }, function() {
this.route('edit');
});
});
});
And create a controller for editing a product:
App.ProductEditController = Ember.ObjectController.extend();
And your list items would link to that product route:
{{#each controller}}
<li>{{#linkTo "product.edit" this}}name{{/linkTo}}</li>
{{/each}}
If you define itemController on your ProductsController you don't need to specify that detail in your template:
App.ProductsController = Em.ArrayController.extend({
itemController: 'product',
needs: ['suppliers'],
actions: {
add: function() {
// do something to add an item to content collection
}
}
});
App.ProductController = Em.ObjectController.extend({
actions: {
remove: function() {
// do something to remove the item
}
}
});
Use a collection template like this:
<button {{action="add"}}>Add Item</button>
<ul>
{{#each controller}}
<li>{{name}} <button {{action="remove"}}>x</button></li>
{{/each}}
</ul>
The Ember documentation describesitemController here:
You can even define a function lookupItemController which can dynamically decide the item controller (eg based on model type perhaps).
The thing I found when rendering a collection wrapped in an ArrayController within another template/view is the way #each is used. Make sure you use {{#each controller}} as Teddy Zeeny shows otherwise you end up using the content model items and NOT the item controller wrapped items. You may not notice this until you try using actions which are intended to be handled by the item controller or other controller based content decoration.
When I need to nest an entire collection in another view I use the view helper as follows to set the context correctly so that any collection level actions (eg an add item button action) get handled by the array controller and not by the main controller setup by the route.
So in my products template I would do something like this to list the nested suppliers (assuming your route for 'product' has properly the 'suppliers' controller):
{{view controller=controllers.suppliers templateName="products/suppliers"}}
The suppliers template just follows the same pattern as the template I show above.