Basic Ember app using an API i've written in Laravel. I have an index page showing all products, I generate edit links and when I visit the edit link none of the data is being returned from the model. Looking in the console it appears no XHR request is being made. If I force refresh then the XHR fires and the data appears.
// Init App
App = Ember.Application.create({
LOG_TRANSITIONS: true,
rootElement: '#myapp-ember'
});
// Declare routes
App.Router.map(function(){
this.resource("products",function(){
this.route('new');
this.route("edit", { path: "/:id/edit" });
});
});
// Model
App.Product = DS.Model.extend({
title: DS.attr('string'),
brand: DS.attr('string'),
category: DS.attr('string'),
type: DS.attr('string')
});
// Declare Data Store so Ember knows we're using Ember Data to handle our Model
App.Store = DS.Store.extend({
revision: 11
});
// set our API namespace
DS.RESTAdapter.reopen({
namespace: 'api/v1'
});
// forward from "/" to "/products"
App.IndexRoute = Ember.Route.extend({
redirect: function(){
this.transitionTo('products');
}
});
App.ProductsIndexRoute = Ember.Route.extend({
model: function(){
return App.Product.find();
}
});
App.ProductsEditRoute = Ember.Route.extend({
model: function(params){
return App.Product.find(params.id);
}
});
Main HTML file here:
<script type="text/x-handlebars" data-template-name="products">
<h3>All Products
{{#linkTo "products.new" classNames="btn btn-small pull-right"}}<i class="icon-plus"></i> Add Product{{/linkTo}}
</h3>
{{ outlet }}
</script>
<script type="text/x-handlebars" data-template-name="products/index">
<table class="table table-striped table-bordered">
<thead>
<tr>
<th>Title</th>
<th>Brand</th>
<th>Category</th>
<th>Type</th>
<th width="100">Actions</th>
</tr>
</thead>
<tbody>
{{#each product in controller }}
<tr>
<td>{{product.title}}</td>
<td>{{product.brand}}</td>
<td>{{product.category}}</td>
<td>{{product.type}}</td>
<td>
{{#linkTo "products.edit" product.id classNames="btn btn-small pull-info"}}edit{{/linkTo}}
</td>
</tr>
{{/each}}
</tbody>
</table>
</script>
<script type="text/x-handlebars" data-template-name="products/new">
<h2>new</h2>
</script>
<script type="text/x-handlebars" data-template-name="products/edit">
<h2>edit</h2>
{{ title }}
</script>
Like I said, index page (that redirects to /products) is showing all data as expected, links are created as they should be but when I land on the /products/ID/edit page I only see the <h2>edit</h2> until I refresh which causes the product title to appear
There are two reasons that the ajax request is not firing:
when using {{linkTo theRoute theObject}}, the model hook on theRoute is never called, because you already have the object to link to it! Instead, the routes model is set to be theObject
Even if the model hook were called and it called App.Product.find(params.id);, it would return the original object, because you have loaded it into the datastore with App.Product.find().
The solution is either:
Return all data in your list action, rather than just e.g. name and id. The list you get back from /products should have all the data for each item that you'd get from /products/1, products/2 etc, just all in a big list
Use a related model, e.g. ProductData, and use a relation to load in the product data when you need it.
With this in mind, your {{linkTo}} should have just product instead of product.id in it.
The last piece of the puzzle is in your route definition - you have used the param :id, instead of :product_id which is what ember expects. Either change it to :product_id, or add an appropriate serialize hook to your ProductsEditRoute
Related
Having the following code:
var Tasks = Backbone.Collection.extend({
url: 'http://localhost:5000/tasks'
});
var TaskView = Backbone.View.extend({
el: '.page',
render: function() {
var that = this;
var tasks = new Tasks();
tasks.fetch( {
success: function(tasks) {
var template = _.template($('#task-list-template').html(), {tasks: tasks.models});
that.$el.html(template);
}
})
}
});
var Router = Backbone.Router.extend({
routes: {
'' : 'home' // intentionally blank for the home page
}
});
// Display logic
var taskListView = new TaskView({ });
var router = new Router();
router.on('route:home', function() {
taskListView.render();
});
Backbone.history.start();
The following HTML:
<body>
<div class="container">
<h1>TODO app</h1>
<hr />
<div class="page"></div>
</div>
<script type="text/template" id="task-list-template">
<table class="table striped">
<thead>
<tr>
<th>Task</th>
<th></th>
</tr>
</thead>
<tbody>
<% _.each(tasks.tasks, function(task) { %>
<tr>
<td><%=task.get('task') %></td>
<td></td>
</tr>
<% }); %>
</tbody>
</table>
</script>
<script src="//cdnjs.cloudflare.com/ajax/libs/jquery/1.8.2/jquery.min.js"></script>
<script src="//cdnjs.cloudflare.com/ajax/libs/underscore.js/1.4.2/underscore-min.js"></script>
<script src="//cdnjs.cloudflare.com/ajax/libs/backbone.js/0.9.2/backbone-min.js"></script>
<script type="text/javascript" src="todoapp.js"></script>
</body>
and the following JSON return from an AJAX request:
{
"tasks": [
{
"id": 6314025183,
"task": "1"
}
]
}
I was wondering how to fill the Collection with the JSON data. I'm unable to fill my HTML table. I suspect my collection to not being filled properly.
How could I validate the content of the collection?
I'm I filling the collection the right way?
This code is based on this video from Thomas Davis available on youtube.
https://www.youtube.com/watch?v=FZSjvWtUxYk
You have two problems. One is code related and one is unfortunately API related.
The API problem can be solved in two ways, but I'll just lay it out first.
When a Collection requests data (from the url property) it expects an array of data. Unfortunately your API is returning an object:
{
"tasks": [
{
"id": 6314025183,
"task": "1"
}
]
}
This is pretty common in a lot of API design, and really speaks to a general misunderstanding of what makes APIs useful.
You'll notice the data you actually want is here in the tasks key of the object:
[
{
"id": 6314025183,
"task": "1"
}
]
It's an array of task objects, each with an id and - what I assume is - a task id.
Great, so you have two options here: you can fix the API so that a request to a collection route like /tasks returns the collection:
[
{
"id": 6314025183,
"task": "1"
}
]
Or, you can use Backbone's parse method to hack around the junk data.
From the documentation for Collection.parse:
Override this if you need to work with a preexisting API, or better namespace your responses.
Here's a quick example:
var Tasks = Backbone.Collection.extend({
'url': 'http://localhost:5000/tasks',
'parse': function( apiResponse ){
return apiResponse.tasks;
}
});
Note the information contained in that parse method that does not have a home. How do I know that the key of the response is tasks?
If I'm a new developer coming into this code, the fact is that I don't. It's tribal knowledge or knowledge I have to go searching for in the API raw response body. The better solution is to namespace the API response to return the collection as requested.
Your second problem is related to your code. In your code you have a Collection and a View and a template, but in your template, you're treating your tasks like a plain ol' javascript object, using underscore to loop over a key.
Instead, tell your collection how to represent it's data.
A collection is a set of related Models.
var Task = Backbone.Model.extend({});
var Tasks = Backbone.Collection.extend({
'url': 'http://localhost:5000/tasks',
'model': Task,
'parse': function( apiResponse ){
return apiResponse.tasks;
}
});
Now, when you hydrate your collection it will automatically create a model representing each set of discrete data.
You can change your view to look like this:
var TaskView = Backbone.View.extend({
'el': '.page',
'template': _.template($('#task-list-template').html()),
'render': function() {
var that = this;
var tasks = new Tasks();
tasks.fetch( {
success: function() {
that.$el.html( that.template( { 'tasks': tasks } ) );
}
})
}
});
Since all Backbone objects extend underscore in some way or another (see the docs for the details), you don't need to manually wrap the passed in collection in underscore. In fact, doing so will almost always create errors. Your template can look like this:
<html>
<body>
<div class="container">
<h1>TODO app</h1>
<hr />
<div class="page"></div>
</div>
<script type="text/template" id="task-list-template">
<table class="table striped">
<thead>
<tr>
<th>Task</th>
<th></th>
</tr>
</thead>
<tbody>
<% tasks.each( function( task ){ %>
<tr>
<td><%= task.get( 'task' ) %></td>
<td></td>
</tr>
<% }); %>
</tbody>
</table>
</script>
<script src="//cdnjs.cloudflare.com/ajax/libs/jquery/1.8.2/jquery.min.js"></script>
<script src="//cdnjs.cloudflare.com/ajax/libs/underscore.js/1.4.2/underscore-min.js"></script>
<script src="//cdnjs.cloudflare.com/ajax/libs/backbone.js/0.9.2/backbone-min.js"></script>
<script type="text/javascript" src="todoapp.js"></script>
</body>
</html>
The solution posted here is untested, but should allow you to make major debugging leaps even if it does not completely solve the problems
I'm having some major issues trying to do something that should be simple, which means that I'm obviously missing something simple.
I'm pulling articles via an API, and instead of having someone click the link to an article, I want a modal to popup with the content of the article on click, so that there wouldn't be any need for page changing or having to load the api multiple times. Naturally, the easiest way I thought of solving this was to use an action to set a property from false to true, and use that property on a bind-attr class to show the modal. However, no matter what I seem to do, I can't ever get the property value initially set or changed in the action, and logging the variable to check and see what it returns results in an error saying the variable is not defined. I would really like to see what the problem is here so I can also use this solution on my list/grid class toggling functions, because right now I resorted to using jQuery for that due to having similar problems.
Below is the code used for my articles listing and the action handling. The action does fire, which I confirmed with an alert(), but no luck on the property. Thanks for the help!
HTML
<script type="text/x-handlebars" data-template-name="articles">
<button {{action 'listStyle'}}>List</button>
<button {{action 'gridStyle'}}>Grid</button>
<section id="articles-category" class="grid">
<div class="row">
{{#each}}
<div class="col-xs-6 col-md-3 article-wrapper" {{action "openArticle"}}>
<div class="article">
<img src="http://placehold.it/300x270">
<div class="content">
<h3>{{title}}</h3>
</div>
</div>
</div>
<div {{bind-attr class=":article-modal isArticleActive:enabled" target="controller"}}>
<div class="article-close">X</div>
<div class="article-back">Back to Articles</div>
<div class="article-wrapper">
<div class="container">
<h2 class="article">{{title}}</h2>
{{{unescape html_body}}}
</div>
</div>
</div>
{{/each}}
</div>
</section>
</script>
app.js
// MAIN APP CONFIGURATION
App = Ember.Application.create();
DS.RESTAdapter.reopen({
host: 'http://mihair.herokuapp.com',
namespace: 'api'
});
Ember.Handlebars.helper('unescape', function(value) {
newValue = value.replace(/(?:\r\n|\r|\n)/g, '<br />');
return new Ember.Handlebars.SafeString(value);
});
// ROUTER
App.Router.map(function(){
this.resource('articles', {path: ':id'});
this.resource('article', {path: 'articles/:id_or_slug'});
});
App.ArticlesRoute = Ember.Route.extend({
model: function() {
return this.store.find('articles');
}
});
App.ArticleRoute = Ember.Route.extend({
model: function(params) {
return this.store.find('articles', params.id_or_slug)
}
});
// MODELS
App.Response = DS.Model.extend({
success: DS.attr('boolean', {defaultValue: false}),
message: DS.attr('string')
});
App.Articles = DS.Model.extend({
title: DS.attr('string'),
description: DS.attr('string'),
html_body: DS.attr('string')
});
App.Article = DS.Model.extend({
id: DS.attr('number'),
title: DS.attr('string'),
description: DS.attr('string'),
html_body: DS.attr('string')
});
// CONTROLLERS
App.ArticlesController = Ember.ArrayController.extend({
isArticleActive: false,
actions: {
listStyle: function() {
$('#articles-category').removeClass('grid').addClass('list');
},
gridStyle: function(){
$('#articles-category').removeClass('list').addClass('grid');
},
openArticle: function() {
this.set('isArticleActive', true);
}
}
});
isArticleActive lives on the ArticlesController (which is the collection), but when you use {{#each}} you change the context from the controller to an individual article. You'll want to work with an itemController in order to create properties for each item individual model instead of for the collection as a whole.
Ember.js Controller Does not work
I've been trying to figure this out for most of today and it's driving me insane, because I think i'm almost there, but just can't figure the last part out...
I have a route, called Map, which renders a sidebar, and within it has a named outlet for sidebar content:
map.hbs:
<div id="map-container">
{{render sidebar}}
<div id="map-canvas">
</div>
</div>
...
sidebar.hbs:
<div id="content-menu">
{{outlet sidebar-content}}
</div>
Each menu item in my sidebar has a custom action called loadModule, which performs a render of a named view into the sidebar-content outlet (using {{action 'loadModule' 'sidebar.module'}}):
var MapRoute = App.AuthenticatedRoute.extend({
actions: {
loadModule: function(module) {
//load a valid view template into the view
this.render(module,
{
into: 'sidebar',
outlet: 'sidebar-content'
});
}
}
});
module.exports = MapRoute;
Any action within the controller for that view works fine, I can trigger them from buttons etc, or by calling them in a didInsertElement in the SidebarModuleViews.
My issue is that I can't define a model for these views, so if I try and get data from my API in any of their Controllers, it won't render that data out to the templates.
I tried to use link-to, but I couldn't make the template append to the current viewport, rather than refreshing the entire page, which defeats the point of having a sidebar (I don't want the route to change)
var SidebarUserController = App.ApplicationController.extend({
actions: {
doSomething: function() {
alert('SOMETHING');
},
fetchUserProfile: function() {
//do something
var mod = this.store.find('profile', App.Session.get('uid'));
}
}
});
I can trigger either of those actions from the rendered template once it's rendered, however, although my store updates with the record, the handlebars helpers in the sidebar/user.hbs do not populate with the model data.
Here is my model:
var Profile = DS.Model.extend({
uid: DS.attr('string'),
firstName: DS.attr('string'),
lastName: DS.attr('string'),
gender: DS.attr('string'),
DOB: DS.attr('date'),
email: DS.attr('string')
});
module.exports = Profile;
and here is my sidebar/user.hbs:
<div class="container">
<button {{action 'doSomething'}}>Do A Thing</button>
<h1>{{firstName}} {{lastName}}</h1>
<h4>{{id}}</h4>
{{#if isAuthenticated}}
<a href="#" {{action 'logout'}}>Logout</a>
{{/if}}
</div>
In that template, the firstName, lastName and id fields do not populate, even though i'm pulling the data from the API and successfully storing it.
Additionally, if it helps, my router.map for sidebar/user looks like this:
this.resource('sidebar', function() {
this.route('user');
});
I believe that the fundamental issue here is that I can't work out how to set the model for the controller without triggering the route. Am I going about this wrong?
Ok so i've worked this out for my particular instance. It may not be the best way of doing it, but it's what I need:
In my MapRoute, I setup the model and controller for my additional sidebar menus in the setupController function. Doing this allows me to load critical data (such as user profile etc), on page load, and I can still retain the render function for each sidebar module in the Route, which will allow the intial data to load, and still allow me to update the model data for each sidebar module controller on subsequent functions:
map_route.js:
actions: {
loadModule: function(module) {
this.render(module, {into: 'sidebar', outlet: 'sidebar-content'});
}
},
setupController: function(controller, profile) {
var model = this.store.find('profile', App.Session.get('uid'));
var controller = this.controllerFor('sidebar.user');
controller.set('content', model);
},
...
I'm writing a small test app using Ember, in the form of a budget manager. I have a Budget object, which contains general properties like the monthly limit and a description. I also have a set of Expense objects, which contain a name, the amount spent, etc. Both are retrieved from a server using Ember Data's REST adapter.
HTML:
<body>
<script type="text/x-handlebars" data-template-name="budget">
<h2>{{name}} (€ {{amount}})</h2>
</script>
<script type="text/x-handlebars" data-template-name="expenses">
<ul id="expense-list">
{{#each model}}
{{render "expense" this}}
{{/each}}
</ul>
</script>
<!-- expense template -->
<script type="text/x-handlebars" id="expense">
<li>
<label>{{description}}</label>
<label class="subtle">{{formatDate time}}</label>
<label class="amount">{{amount}}</label>
</li>
</script>
</body>
</html>
JavaScript:
window.App = Ember.Application.create();
App.ApplicationAdapter = DS.RESTAdapter.extend({
host: 'http://localhost:5000',
namespace: 'outthepocket/api'
});
// Model
App.Expense = DS.Model.extend({
amount: DS.attr('number'),
description: DS.attr('string'),
time: DS.attr('date')
});
App.Budget = DS.Model.extend({
name: DS.attr('string'),
amount: DS.attr('number')
});
// Routes
App.Router.map( function() {
this.resource('budget');
this.resource('expenses');
});
App.ExpensesRoute = Ember.Route.extend({
model: function()
{
return this.store.find('expense');
}
});
App.BudgetRoute = Ember.Route.extend({
model: function()
{
return this.store.find('budget', 1);
}
});
Following the architecture I see in all the Ember tutorials, there is an ExpensesRoute with the list of expenses as its model, and a BudgetRoute with the selected budget as its model. This works great as long as I go through the proper URL to see each resource:
myapp.html#budget renders the budget template with data from the server.
myapp.html#expenses renders the expenses template with data from the server.
The problem I'm having now is that I want to display both templates, with their data, on one page (the index page). I've tried two solutions for this:
Solution 1: Have separate routes and templates and call {{render budget}} and {{render expenses}} in the main application template. This renders both templates, but without any data.
Solution 2: Have just an IndexRoute and return both budget and expenses from its model property, rendering them into the index template. This more or less works, but seems counter to Ember's otherwise nice separation of different resources, routes and controllers.
Any thoughts? I've been through five or six tutorials and Ember's official guide, but none of those have made clear how to assemble a one-page web app with multiple templates backed by multiple resources without having to link to different pages/routes.
You can use Ember.RSVP.hash to load more than one model, in a single object:
App.IndexRoute = Ember.Route.extend({
model: function()
{
return Ember.RSVP.hash({
expenses: this.store.find('expense'),
budget: this.store.find('budget', 1)
})
}
});
And in the template you can access each resolved promise by the key:
{{expenses}} will return the result from this.store.find('expense') promise and {{budget}} the result from this.store.find('budget', 1) promise.
So in your index template you will able to do:
<script type="text/x-handlebars" id="index">
{{render "expenses" expenses}}
{{render "budget" budget}}
</script>
I want to implement a system that shows me the newest posts. For this I do not want to use the index action from the user as this is already taken for another post function but a "newest" action. It is showed on the index route with a {{ render "postNewest" }} call. I would prefer to load the data in the PostNewestController or PostNewestView instead of the route for abstraction reasons.
I tried two ideas to achieve this, but none worked so far:
create a custom adapter and add a findNewest() method: the findNewest() method is sadly not found when trying to call in the init method of the controller.
write the request directly into the init method and then update with store.loadMany(payload): data is successful request. However, I do not know how to access the data from the template and set the content of the controller.
Is there any way for this?
EDIT:
Here is the source code to better understand the problem:
PostModel.js
App.Post.reopenClass({
stream: function(items) {
var result = Ember.ArrayProxy.create({ content: [] });
var items = [];
$.getJSON("/api/v1/post/stream?auth_token=" + App.Auth.get("authToken"), function(payload) {
result.set('content', payload.posts);
});
return result;
}
});
PostStreamController.js
App.PostStreamController = Ember.ArrayController.extend({
init: function() {
this.set("content", App.Post.stream());
},
});
index.hbs
{{# if App.Auth.signedIn}}
{{ render "dashboard" }}
{{else}}
{{ render "GuestHeader" }}
{{/if}}
{{ render "postStream" }}
postStream.hbs
{{#each post in model}}
<li>{{#linkTo 'post.show' post data-toggle="tooltip"}}{{post.name}}{{/linkTo}}</li>
{{else}}
Nothing's there!
{{/each}}
PostShowRoute.js
App.PostShowRoute = Ember.Route.extend({
model: function(params) {
return App.Post.find(params.post_id);
},
setupController: function(controller, model) {
controller.set('content', model);
},
});
I Had this issue too. Just add init in your controller, and define the model you want to get there.
In your Controller
App.PostRecentController = Ember.ArrayController.extend({
init: function() {
return this.set('content', App.Post.find({
recent: true
}));
}
});
In your template
{{#each post in content}}
{{post.body}}
{{/each}}
I would recommend you check EMBER EXTENSION, it will give you a good idea of the naming, and see if everything is missing.
I figured out the problem. The Post model has a belongsTo relationship to another model. This relationship is not loaded by Ember so in the stream() method I have to load this manually. Then everything works as expected.