How to implement a search feature? - ember.js

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.

Related

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.

Accessing store in another route

I have this model:
App.Game = DS.Model.extend({
name: attr(),
uri: attr()
});
and this route:
App.GamesRoute = Ember.Route.extend({
model: function() {
return this.store.find('game');
}
});
This works fine, calls the backend server, and stores elements in the store (I've checked with Ember inspector). This is the json I return:
{"games":[{"id":"TicTacToe","name":"TicTacToe","uri":"http://localhost:10000/games/TicTacToe"}]}
Now I have this template for 'games' (snipped):
{{#each game in model}}
{{#link-to 'games.matchlist' game.id}}{{game.uri}}{{/link-to}}
This shows the URI for each game. Now in the games.matchlist route what I would like to do is to search in the store by the game_id received param and get the game URI. The reason is that the server doesn't follow RESTAdapter conventions, and I would like to make a custom AJAX query to that URI myself.
This doesn't work:
App.GamesMatchlistRoute = Ember.Route.extend({model: function(params) {
var store = this.store;
var game = store.find('game', params.game_id)
console.log(game);
console.log("URI: " + game.uri);
at this point, game is an object but it's not an instance of my model. It doesn't have a uri attribute. What am I doing wrong? I'm feeling that I'm missing something obvious.
If you want to get records without hitting the server and you know you already have it in the store, use this.store.getById('game', ID).
I'm on my mobile, but you need to create a GameAdapter and customize I believe the fetch function. Checkout the docs for adapters on the ember site and you should have your answer.
Your other option is to fetch the data from your server and use this.store.pushPayload(data).
Docs here: http://emberjs.com/api/data/classes/DS.Store.html#method_pushPayload
And the adapter docs here: http://emberjs.com/guides/models/customizing-adapters/

How to have two different models within a route and its subroute?

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.

How to structure a multi-record Ember app with named outlets?

I'm trying to build a Tweetdeck-like UI to arrange items from a central library into categories. I really need help wrapping my head around the canonical way of using Ember's router.
Essentially, I have a search UI, which allows the user to open zero or more categories simultaneously. The categories show a list of items, which the user can add to from a central library on the right. By completely ignoring the router and the URL, I have managed to hack together a semi-working proof of concept. Now I want to go back and try to do it the Ember way. Below is a high level sketch of what I am trying to accomplish:
If I understand correctly, the desired URL scheme would be a comma-separate list of model IDs that are currently open. I got a good idea of how to approach that from another question, How to design a router so URLs can load multiple models?.
Unfortunately, there are a few concepts I do not understand:
How do I construct my templates and router, such that the library is displayed with its own model and controller? I assume a named {{outlet}} is the way to go, but I am completely lost when it comes to the renderTemplate configuration. Or perhaps I should use {{render}} instead? In either case, I do not understand the router's role in this situation.
EDIT 1/28: I've added an updated fiddle that includes a standalone library route/template and documents my attempts to render it into the categories template. How does Ember expect me to give the library template its model when I try to embed it into another route? I've tried both {{outlet}} with renderTemplate and {{render}}, but in both cases, I am stuck when it comes to specifying the model.
Using renderTemplate:
App.CategoriesRoute = Ember.Route.extend({
renderTemplate: function() {
this.render('categories');
this.render("library", {
into: "categories",
outlet: "library",
controller: "library",
});
},
});
When my controller receives a request to open a category, how do I communicate that to the router? How is the hash path updated? Who is responsible for loading the appropriate model(s)? I assume I should start with transitionTo or transitionToRoute, but I do not understand the router's role here either. Specific questions:
How do I de-serialize multiple, comma-separated models from the URL? Do I just split on the comma or is there a better way?
Once I get the IDs from the URL, how do I make my model hook return multiple records? Do I just shove them all into an Ember array?
When the controller gets the ID of a new record to open, how do I communicate that to the router?
I've tried to work this out on my own and have read the Ember documentation many times, but I am afraid it is simply over my head. I put together a minimal (currently non-functional) fiddle to outline my thoughts and point out where I am stuck. I would appreciate any help anyone could offer.
this.render does not accept a model parameter, but you could pass the model through the controller property instead, this makes sense to do since the Controller is really a proxy for the model at any rate
App.IndexRoute = Ember.Route.extend({
var self = this,
notesController = self.controllerFor('notes').set('content', self.store.find('notes'));
renderTemplate: function() {
this.render('notes', {
controller: notesController,
into: 'index',
outlet: 'notes'
});
}
});
You could also try something like this from this link.
App.IndexRoute = Ember.Route.extend({
model: function() {
return Ember.RSVP.hash({
books: this.store.findAll('book'),
genres: this.store.findAll('genre')
});
},
setupController: function(controller, model) {
controller.set('books', model.books);
controller.set('genres', model.genres);
}
});
Here, they load multiple models into one route using Ember.RSVP.hash and then using setupController they set each model (Rails: instance variable?) individually.
I'm assuming using this method that you could load as many models as you needed.

How do I use an object controller in Ember.js with an API that only returns all objects?

I have a two page Ember.js application using ember-data to wrap a simple RESTful API. The main page is a list of products, and the other page is a product details page for one product. The data is loaded via an API that only has an "index" request, /api/products.
The above works fine when navigating the site via the main page, however I'm not sure how best to handle navigating directly to the product details page. I need ember-data to request all products and keep these products client-side so that as the user navigates the simple site it doesn't make any more requests back to the API for products. However, the ProductIndexView and ProductIndexController in my application should preferably only see the one record.
Is there a good way to handle this in Ember.js? I know that I could add a computed property to the controller that filters down the full list and then pass that into the view template. However, I'd rather the view and controller not know about the full list.
You need to nest all your routes in a resources that fetches all products.
Something like this:
App.Route.map(function() {
this.resource('products', { path: '/' }, function() {
this.route('index');
this.resource('product', { path:'/:product_id'} );
});
});
App.ProductsRoute = Ember.Route.extend({
model: function() {
return App.Product.find({});
}
});
App.ProductsIndexRoute = Ember.Route.extend({
model: function() {
return this.modelFor('products');
};
});
Use the products/index template to display all products.
Use the product template to display a product detail.
Notice the {} I included in App.Product.find({}). This enforces ember-data to return a promise. This is necessary to make the product route wait for all products to arrive from the server before being called.