How to have two different models within a route and its subroute? - ember.js

I'm making a simple web chat system with Ember.
I have a route /chatrooms that lists a few chatrooms, and then I also have /chatrooms/:chatroom_id that should show the actual chatroom with messages.
The second route is within the first one, like this:
this.resource('chatrooms', function() {
this.route('show', {
path: ':chatroom_id'
});
});
When I access /chatrooms, a call is made to the server (/api/chatrooms) is a list of rooms is returned and displayed, like expected.
When I click a room, the application transitions to /chatrooms/id, but no call is made to retrieve the messages (available at /api/chatrooms/id), even when I try to define a model.
I have a similar scenario with the users. A list of users is retrieved, then displayed. When a name is clicked, the profile is shown. No second call is made, but that's okay since Ember knows everything about the user already.
In my current case, when a list is first returned, it includes all the information except the messages. I believe that would be too much otherwise (10 chatrooms * 100 last messages = 1000 elements in my JSON for each request). So I want to call the server for messages only when a chatroom is selected.
Do you know how to do it, or maybe there's something wrong I'm doing in the first place?
Updates
Template code from app/templates/chatrooms.hbs
<h1>Chatrooms</h1>
<ul class="sub-menu nobullet flex mas">
{{#each chatroom in model}}
<li class="mrs">{{link-to chatroom.name "chatrooms.show" chatroom class="pat"}}</li>
{{/each}}
</ul>
{{outlet}}
In this case, model is an array of chatrooms.
My routes:
app/routes/chatrooms.js
export default Ember.Route.extend({
model: function() {
return this.store.find('chatroom');
}
});
app/routes/chatrooms/show.js
export default Ember.Route.extend({
model: function(params) {
return this.store.get('chatroom', params.chatroom_id);
},
actions: {
send: function() {
...
}
}
});

As discussed in this thread, when you link-to a route and the model is already loaded, the model hook of the route is not fired because there’s no need to reload the data.
If you transition to a route and all the context objects -the objects which will serve as models to templates- are passed in, the beforeModel and model hooks will not be called.
Later in the thread balint corrects:
In fact, the beforeModel hook still gets called in that case, it is only the model hook that does not.
If you want to force the model to be reloaded, you can change your link to use the ID instead of the model:
{{link-to chatroom.name "chatrooms.show" chatroom.id class="pat"}}
You could also load the data in the beforeModel or afterModel hooks, or setupController.
Also, in the chatrooms/show route, you are getting the already-loaded model from the Ember Data store rather than loading it from the server. Try this:
return this.store.find('chatroom', params.chatroom_id);

I ended up adding a links property to the JSON response for chatrooms. When the content of a chatroom has to be displayed, the link is used and the messages retrieved. It only requires two requests, and there's not need to preload all the messages from all the chatrooms and no need to make a request for each message.

Related

How does Ember Data write over instances of stored model data

Ok, so I'm new to Ember so bear with me.
I have an Ember application that is communicating with an API that does not adhere to JSONAPI standards, thus I have begun writing my own serializers in order to use Ember Data. However I am finding that when I make multiple requests to the same resource, the data is having trouble writing to the store. Consecutive requests to the same resource always responds with the following error:
TypeError: Cannot convert object to primitive value
Which from my limited understanding, implies that the data I am sending to the store is being treated like a string.
In my Application route I have written a findAll to my model 'listing-item' like so:
model: function() {
return this.store.findAll('listing-item');
},
In a nested 'user' route, when I do any type of request for the listing-item data that returns an array response (query, findAll) for the listing-item data, I get:
TypeError: Cannot convert object to primitive value
at EmptyObject.SETTER_FUNCTION [as title] (ember.debug.js:20672)
at assign (<anonymous>)
at InternalModel.setupData (internal-model.js:244)
at Class._load (store.js:1728)
at Class._pushInternalModel (store.js:2055)
at Class._push (store.js:1995)
at finders.js:141
at Backburner.run (ember.debug.js:720)
at Class._adapterRun (store.js:2253)
at finders.js:139
(Title is a field in my listing item model).
As I mentioned earlier, my API does not adhere to JSONAPI standards, so I've written a listing-item serializer like so:
import DS from 'ember-data';
export default DS.RESTSerializer.extend({
normalizeArrayResponse(store, primaryModelClass, payload) {
payload.data = [];
payload.listing_item._data.forEach(this.formatListingItemArray, payload.data);
delete payload.listing_item;
return payload;
},
formatListingItemArray(listingItem) {
this.push({
type: "listing-item",
id: listingItem.id,
attributes: {
title: listingItem.title,
description: listingItem.description,
buy_now_price: listingItem.buy_now_price,
created_at: listingItem.created_at,
category_id: listingItem.category_id,
subcategory_id: listingItem.subcategory_id,
identity_id: listingItem.identity_id,
listing_number: listingItem.listing_number,
brand_new: listingItem.brand_new,
sold: listingItem.sold,
},
});
},
});
So I suppose my question is, what is Ember Data doing with my data object for this error to occur, and what might I be doing wrong in formatting my data for Ember data to consume.
UPDATES:
It appears as though only the top 3 fields are causing this error to occur. If I comment out the attributes 'title', 'description' and 'buy_now_price' in my serializer, I don't get this error. Also, it appears this only occurs when I navigate to the route, If I am in the /user route when the application loads, both requests work as expected.
Ok, so I've been crawling through ember-data code and found that in the internal-model.js file there is a setup function that looks at the current attributes in the store and compares them to the data being passed from the serializer. It then does an assign() to copy the serializers new data over to the stores object. However for some reason it seems that my stores object has a set of 'getter' and 'setter' function that come back from the store for the problematic fields (title, description and buy_now_price). What I need to know now is why are these functions coming along for the ride and what have I done to cause this?
Picture of getters/setters on ember-data object
Thanks in advance, let me know if there's any more information I need to provide in order to give better context.
So I found the solution to my problem for anyone that experiences a similar issue.
The symptom was as stated above, it seemed as though ember-data was returning getter and setter functions back from the store and having problems placing my new values in their stead. However the clue was that it was only the fields that I was rendering in my template.
The problem seems to be that I was using an Ember Concurrency task to perform the loading of my data, then passing the task straight in to an internal component from the model template. Something like this (not tested code):
The WRONG pattern (one I was using to experience the issue).
//route file - application.js
export default Ember.Route.extend({
model: function() {
return {
listings: this.get('getListings').perform(),
};
},
getListings: task(function * () {
return yield this.store.findAll('listing-item');
}),
})
//application template - application.hbs
{{listing-index listings=model.listings}}
//listing index component template.hbs
{{#if listings.value}}
{{#each listings.value.content as |listing|}}
{{listing._data.title}}
{{listing._data.description}}
{{listing._data.buy_now_price}}
{{/each}}
{{/if}}
The CORRECT pattern seems to be as thus:
//route file - application.js
export default Ember.Route.extend({
model: function() {
return {
listings: this.get('getListings').perform(),
};
},
getListings: task(function * () {
return yield this.store.findAll('listing-item');
}),
})
//application template - application.hbs
{{#if listings.value}}
{{listing-index listings=model.listings.value}}
{{/if}}
//listing index component template.hbs
{{#each listings as |listing|}}
{{listing.title}}
{{listing.description}}
{{listing.buy_now_price}}
{{/each}}
So the problem seems to lie in this pattern of passing the task rather than the content of the task in to my component. When I looped over the listings in the model template then passed the listings straight in to the component, it seemed to have solved my issues. I gather this is something to do with the use of these _data properties. A further explanation would be appreciated but i'll mark this as resolved for now.

When creating a new data object in Ember that relies on another object how do you pass it along?

I am on a page where I can see a specific customer, part of my router.js is:
this.route('customers');
this.route('customer', {path: "customers/:customer_id"});
this.route('customer.order.create', { path: "customers/:customer_id/order/create" });
customer.order.create needs to load in my main view and so is not nested. An order 'has a' customer.
I've setup my /customer/order/create controller to have
needs: "customer"
I want to access the customer in my /customer/order/create.hbs template like this:
<h3>New Order for {{controllers.customer.name}}</h3>
When I end up creating the order I will also want to set newOrder.customer = customer.
customer.hbs links like so
<div>
{{#link-to 'customer.order.create' model}}Create Order{{/link-to}}
</div>
Currently {{controllers.customer.name}} renders nothing, what piece of the puzzle am I missing to get to the customer in my order/create route?
Or putting it more generally, what route/controller/etc code do I need when I have a parent object which belongs to my child object in a /parentObject/parent_id/childObject/create type scenario.
There are many points to fix:
1) {{controllers.customer}} is Controller Object, {{controllers.customer.name}} it's name property. I think you want {{controllers.customer.model.name}}.
2) "..newOrder.customer = customer.." should be
newOrder.set('customer', this.get('controllers.customer.model'));
3) your customer.order.create route model hook shoudn't be empty, since you are using dynamic segment customer_id:
//route
model: function(params) {
return this.find('customer', params.customer_id);
}
4) Your routes are not nested, so {{controllers.customer.model.name}} would be empty if your customer route is not activated. So you should use: {{model.name}} instead of {{controllers.customer.model.name}}
When you click link you passes model directly, and model hook is not fired, so all looks good. When you visit url directly, model hook is fired. If it is empty you will see nothing.
General concept: it is dependancy injection concept, you could read here: http://guides.emberjs.com/v1.12.0/understanding-ember/dependency-injection-and-service-lookup/
You should be able to get the customer from the store. Give the following code a try:
The route:
export default Ember.Route.extend({
model: function(params) {
return Ember.RSVP.hash({
customer: this.store.find('customer', params.customer_id)
});
}
});
The controller:
export default Ember.Controller.extend({
customer: Ember.computed.alias('model.customer')
});
And it should be directly accessible as customer in your template, like so:
<h3>New order for {{customer.name}}</h3>
I changed needs: "customer" to needs: ["customer"] and then used {{model.name}} in my template. Seems that needs requires an array of strings and not just a string, after I fixed that Ember took compare of the rest without the need to create a /customers/order/create.js route.
For a more complete answer see Artych's answer if you don't want everything taken care of.

How to implement a search feature?

I have a search feature that allows the user to filter out by products. Typically, I would have the following:
// Routes
import Ember from 'ember';
export default Ember.Route.extend({
model: function(params) {
return Ember.RSVP.hash({
keyword: params.keyword,
products: this.store.find('product', { name: params.keyword, status: 'available' })
});
},
setupController: function(controller, model) {
controller.set('content', model);
}
});
In the template:
// Template
{{#if products.length}}
<ul>
{{#each products}}
<li>
Name: {{name}}
</li>
{{/each}}
</ul>
{{else}}
<p>No products</p>
{{/if}}
Which works. However, what if I wanted a search API endpoint to handle filtering of products...
How will the model hook in the routes look like? Should I create an Ember search model? Is that the right approach?
If not, should I be using Ember.$.ajax instead? Other approach?
Ember.js provides facilities to deal with _query_ing data at resource level. Here is a suggested solution followed by some other options that are closer to your expectations.
Expected Use Case
In Ember, you don't have to map your "representation" of data to your server endpoint. You don't have to be RESTful with your Ember routes (as in URLs).
In Ember, one "representation" of data (say a Controller and View set) can consult multiple data sources.
Use "q" parameter for full text search on your resource.
The point behind all of this is, to decrease complexity of your backend API. If you build a search and query system for one resource, you can reuse it for all other resources of your backend.
Create a search URL
// in your router
this.route('search')
Create a search Route to fetch required data
Ember.RSVP.hash({
stores: this.store.find('store', {
rate_minimum: 4.0,
near: 'lat:long',
q: 'local store name'
}),
products: this.store.find('product', {
price_maximum: '$50',
near: 'lat:long',
q: 'famous shoe brand'
})
});
Fast Response Way
Use Store#pushPayload
Download data with Ember.$.ajax from your search endpoint. Keep in mind as long as your data has a name, you can use Store#pushPayload to load multiple unrelated data types into Ember Data's Store.
In your Route's model hook, use this.store.all('product') to return all products in memory without sending a request. At some point in future Ember will update this array. You can use Ember.RSVP to "represent" more than one resource.
In your Controller, use array operations like filterBy to provide real time update to users' queries beside issuing a new request to API for more relevant data.
This results in immediate response while waiting for backend to reply and immediately updating "representation" of data with backend response.
Ember Data-less Way
Create a Search Route
Use Ember.$.ajax to get data
Pass it as your Route's model hook
Send another ajax request whenever user changes search criteria
I don't think you would do anything different. When you execute a store.find with query parameters, in your case { name: params.keyword, status: 'available' }, the result that is return by the query is what will show in the controller; assuming you are using an ArrayController. Your model and controller should not need to change, you just control what you want based on how you structure your query.
// display all models that are in the store
store.find('someModel')
// display a single model
store.find('someModel', 123) // will display a single model
// display only models that match the query
store.find('someModel', { name: params.keyword, status: 'available' }
Now if you are dealing with endpoints that don't conform to the norm, ie. your search endpoints are not just at /products?name=some-name&status=available, then you may have to modify your adapter slightly to deal with the differences. Doing this in the adapter means you still have a simple controller/route/model and you are hiding the complexity which is very specific to the endpoint in the adapter.

Proper way to set multiple models on route; depending on user authentication?

I'm currently working on an Ember app and it is coming along fine but since I am new to MVC applications in general there are a lot of concepts that don't come naturally to me.
I am currently trying to return two models for my index route. I referred to another SO question (EmberJS: How to load multiple models on the same route?) for the correct method and it has worked great.
My problem is now that I need to only set one of the two models only if the user is authenticated. I am using ember-simple-auth, and currently this is what I've got:
// app/routes/index.js
import Ember from 'ember';
export default Ember.Route.extend({
model: function() {
if (this.get('session.isAuthenticated')) {
var _this = this;
this.get('store').find('user', this.get('session.uid')).then(function(user) {
_this.set('model.entries', user.get('entries'));
});
}
return Ember.RSVP.hash({
newEntry: this.get('store').createRecord('entry', {
body: 'Write here ...'
})
});
}
});
For some reason, this does not work. After my route is loaded, the model only has the 'newEntry' property and not an 'entries' property, although the promise does get fulfilled (I put console.logs inside to prove it).
What could be happening? And is this the best way to accomplish this?
There is a set of data that you always want to load, for every user. Do that in the model hook, that is actually the data for the route.
There is another piece of info that you want to add only if a condition is met (authentication). Do that in the afterModel hook.
...is provided the route's resolved model...
http://emberjs.com/api/classes/Ember.Route.html#method_afterModel
So, now you can append or remove data from the model. Or take any relevant action depending on the data that you received.

Ember Routing - Model hook not called on page refresh

I am trying to use nested routing to render a collection first. Once the collection is rendered, clicking on an item (using link-to) renders that particular item in the outlet. This all works fine so far.
I am having a problem where refreshing the page doesn't call the 'model' hook of my nested resource though.
From Ember's website at http://emberjs.com/guides/routing/specifying-a-routes-model/:
What happens if the user visits your application directly with a URL
that contains a dynamic segment? For example, they might reload the
page, or send the link to a friend, who clicks on it. At that point,
because we are starting the application up from scratch, the actual
JavaScript model object to display has been lost; all we have is the
ID from the URL.
Luckily, Ember will extract any dynamic segments from the URL for you
and pass them as a hash to the model hook as the first argument
Here is my code:
Admin.Workqueues.App.Router.map(function () {
this.resource('delinquencies', function () {
this.resource('delinquency', {
path: '/:id'
});
});
});
Admin.Workqueues.App.DelinquenciesRoute = Ember.Route.extend({
model: function () {
// Does XHR here and fetches a collection of items to render.
// Returns a promise
}
});
Admin.Workqueues.App.DelinquencyRoute = Ember.Route.extend({
model: function (params) {
debugger; // This doesn't get called
}
});
So with this code, going to /delinquencies lists the entire collection. Clicking on an item opens a delinquency object at /delinquencies/3 but now refreshing the page doesn't call the model hook of delinquency route.
I am not sure what I am missing. Any ideas? If it matters, I am using:
Ember : 1.2.0
Ember Data : 1.0.0-beta.7+canary.f482da04
Handlebars : 1.1.1
You shouldn't define the resource as a child resource but as a separate one.
Admin.Workqueues.App.Router.map(function () {
this.resource('delinquencies', function () {});
this.resource('delinquency', { path: '/:delinquency_id' });
});
The reason your code works right now is because the link-to helper already provides the context so the model hook never gets called.
For the model hook in the DelinquencyRoute you should use something like this:
return this.store.find('delinquency', params.delinquency_id);
For more info, have a look at the guides and getting started tutorial:
http://emberjs.com/guides/routing/defining-your-routes/ (the dynamic segments section answers your question)