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
Related
How do I get pagination with ng-table-dynamic and $http working?
HTML specification of the table is
<table class="table-bonds table table-bordered table-hover table-striped"
export-csv="csv"
separator=","
show-filter="true"
ng-table-dynamic="bondsTable.bondsDataParams with bondsTable.bondsDataCols">
<tr ng-repeat="row in $data">
<td class="hand"
ng-repeat="col in $columns">{{::row.node[col.field]}}</td>
</tr>
The table creation code is:
self.bondsDataParams = new NgTableParams({
page: 1, // show first page
count: 5 // count per page
}, {
filterDelay: 0,
total: 0,
getData: function (params) {
return $http(bondsDataRemote).then(function successCallback(response) {
// http://codepen.io/christianacca/pen/mJoGPE for total setting example.
params.total(response.data.nodes.length);
return response.data.nodes;
}, function errorCallback(response) {
});
}
});
AngularJS 1.5.8
This is an excellent directive for pagination have a look at it . It has lots of options and its easy to use.
The main problem was mixing up loading the data via ajax and not supporting the filtering/pagination on the server side of the request.
Either provide all the data up-front so that the table can filter, or fully support the pagination, sorting and filtering on the server side.
Option 1. Load the data before hand. I used this option because my dataset is not that big and it seemed like the easiest way to allow people to use all the permutations of filtering sorting and downloading.
No total value is required here. The data is all loaded.
var Api = $resource('/green-bonds.json');
// Or just load all the data at once to enable in-page filtering, sorting & downloading.
Api.get({page: "1", count: "10000"}).$promise.then(function (data) {
self.bondsDataParams = new NgTableParams({count: 25}, {
dataset: data.results
})
});
Or fully support the lazy loading data API and set total. Uses getData: rather than just setting dataset.
var Api = $resource('/green-bonds.json');
this.bondsDataParams = new NgTableParams({}, {
getData: function (params) {
return Api.get(params.url()).$promise.then(function (data) {
params.total(data.count);
return data.results;
});
}
});
Note 1: By default $resource expects an object .get() for object, .query() for array. Also see isArray:. I didn't get this to work.
Note 2: params.url() provides $resource with the ng-table params. e.g. {page: "1", count: "10"}
Hello I try to make a angularjs application so i retrieve a data from a json folder but it's display lik this ["adventure","sci-fi"]
how can I please remove [" "] from this ["adventure","sci-fi"]
this is my json folder
[
{
"title": "Interstellar",
"genre": [
"adventure",
"sci-fi"
],
"watched": false
},
{
"title": "Inception",
"genre": [
"action",
"mystery",
"sci-fi"
],
"watched": true
}
]
and this my service.js
var app = angular.module('appMovies', [] );
app.service('moviesService', function($http,$q){
var deferred =$q.defer();
$http.get('movies.json').then(function (data)
{
deferred.resolve(data);
});
this.getPlayers = function ()
{
return deferred.promise;
}
})
and this my controller
app.controller('appcontrolles', function($scope,moviesService){
var promise = moviesService.getPlayers();
promise.then(function(data)
{
$scope.players =data.data;
console.log($scope.players);
});
})
and this is my index.html
<table class="table table-striped">
<thead>
<tr>
<th>title</th>
<th>genre</th>
<th>watched</th>
</tr>
</thead>
<tbody>
<tr ng-repeat="movie in players | filter : genre |filter: search.genre |filter : watched ">
<td>{{movie.title}}</td>
<td>{{movie.genre}}</td>
<td><input type="checkbox" name="vu", ng-model="movie.watched",value="true"></td>
</tr>
</tbody>
</table>
thanks for your help
Since movie.genre is an array, when you just put {{movie.genre}}, Angular outputs the value as a string representing an array: ["adventure","sci-fi"].
If you want a simple comma-delimited list of values, you could use the .join() function of an array to create a string with a specify delimiter, like ", ", like so:
<td>{{movie.genre.join(", ")}}</td>
Alternatively, if you want more complex DOM around it, then you could ng-repeat over that array:
<td><span ng-repeat="genre in movie.genre">{{genre}}</span></td>
Off Topic:
You don't need to use $q.defer with something that already returns a promise, like $http - you can just return that promise, and so your service could be simplified to:
app.service('moviesService', function($http){
this.getPlayers = function()
{
return $http.get('movies.json');
}
});
$q.defer is used when you are trying to convert a non-promise async function of some third-party service, for example, which uses on-success and on-error handlers.
I have a factory, which goes into a controller, and I am trying to get data from that display on an HTML page. I am having trouble specifying an Object's pathway however.
My Factory:
app.factory('APIMethodService', function() {
var Head = "api.example.com";
return {
apis:
[{
accounts: [
{
v1: [
{
uri: Head+"/v1/accounts/",
item1: "AccountNumber",
item2: "MoneyInAccount"
}],
v2: [
{
uri: Head+"/v2/accounts/",
item1: "AccountNumber",
item2: "MoneyInAccount"
}]
}
],
customers: [
{
v1: [
{
uri: Head+"/v1/customers/",
item1: "CustomerName",
item2: "CustomerID",
item3: "CustomerEmail"
}]
}
]
}]
};
});
My Controller:
app.controller('APIController', function($scope, APIMethodService) {
$scope.title = "API";
$scope.apiList = APIMethodService;
$scope.accountList = $scope.apiList.accounts.v1;
$scope.accountList2 = $scope.apiList[0][0];
});
My HTML
<div ng-controller="APIController">
<div id="api" class="row">
<div class="col-xs-12">
<div class="row" style="font-size:20px">
{{title}} Page!
<table class="table table-striped">
<tr ng-repeat="api in apiList | orderBy:'uri' | filter:search">
<td>{{api.uri}}</td>
<td>{{api.item1}}</td>
<td>{{api.item2}}</td>
</tr>
</table>
</div>
</div>
</div>
</div>
The errors I get are in regards to the Controller trying to parse out the individual objects I wish to grab, like accounts or customers, and then any version v#, they may have.
So it will say something such as
TypeError: Cannot read property 'v1' of undefined
I just need some help specifying the proper pathways into my factory service.
You have a few problems. First, you are referring to the object returned from the factory incorrectly. APIMethodService is the factory that you're injecting, so you need to first reference the object that that factory is returning like this:
APIMethodService.apis
This will give you your entire JSON object.
From there, the rest of your object is made up of arrays of objects, so referring to 'v1' won't do you any good. You need to specify an index instead. If you want v1, you'll need:
APIMethodService.apis[0].accounts[0].v1
This will give you the v1 array, which again is an array of objects.
Customers would be:
APIMethodService.apis[0].customers[0].v1
The first problem you have is that the factory returns an object with a single property called apis. So basically this $scope.apiList.accounts.v1 should be $scope.apiList.apis.accounts.v1. Bu that's not all as this won't either work since dotting(.) into apis is an array you'd have to use the index. In this case it would be $scope.apiList.apis[0] and then you could .accounts[0].v1 which is also an array containing a single object.
Now if you can I would suggest to you that you'd change how you represent this data structure.
This is how you could do it.
app.factory('APIMethodService', function() {
var Head = "api.example.com";
return {
accounts: {
v1: {
uri: Head+"/v1/accounts/",
items: ["AccountNumber","MoneyInAccount"]
},
v2: {
... // skipped for brevity
}
},
customer: {
... // code skipped for brevity
}
};
});
And then it's just a matter of dotting into your APIMethodService-object like APIMethodService.accounts.v1.items[0] if you want the AccountNumber method name.
Constructing your url could then be done like this.
var baseUrl = APIMethodService.accounts.v1.uri; // 'api.example.com'
var url = baseUrl + APIMethodService.accounts.v1.items[0]; // 'AccountNumber'
// url = "api.example.com/v1/accounts/AccountNumber"
Again, this is one way you could do it but this can be further enhanced upon. The examples I provided are simply for demo purposes and this is not in any way the only way to do it.
Expanding upon recieved comments/questions your service (and data representation) could now look like this.
app.factory('APIMethodService', function() {
var Head = "api.example.com";
return {
accounts: {
v1: {
uri: Head+"/v1/accounts/",
items: [
{
name:'AccountNumber',
description:'Show the account number'
},
{
name:'AccountOwner',
description:'Show information about the owner of the account'
},
{
name:'MoneyInAccount',
description:'Show money in the Account'
}
]
},
v2: {
... // skipped for brevity
}
},
customer: {
... // code skipped for brevity
}
};
});
// Get descriptions
var accountNumberDescription = APIMethodService.accounts.v1.items[0].description; // 'Show the account number'
var accountOwnerDescription = APIMethodService.accounts.v1.items[1].description; // 'Show information about the owner of the account'
var moneyInAccountDescription = APIMethodService.accounts.v1.items[2].description; // 'Show money in the Account'
By using objects with properties like this it's alot easier to understand what you are trying to do. With arrays with indexes you'd have to know or take a look at the source to see what's going on. Here, someone viewing your code they can instantly understand that it is the description you are getting.
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
Hi I'm trying to wrap my head around backbone.js for some days now but since this is my first MVC Framework, it's pretty hard.
I can easily get my Collections to work, fetching data from the server etc, but it all depends on first "logging" in per API-Key. I just don't know how to model this with a good MVC approach. (btw: I can't use the Router/Controller because it's a Chrome Extension)
The Flow looks like this:
Start Extension
Is there an API-Key in localStorage?
No => Display a input field and a save button which saves the key to localStorage; Yes => proceed with the Application:
App......
The only way I could think of it is putting it all together in a big View... but I guess since I am fairly new to this there are surely some better approaches.
You can create a model that maintains the state of the user's login status and a view that renders a different template depending on that status. The view can show the "input field" template if the user is not logged in and a different template if the user is. I would keep all access to localStorage in the Model because the View should not be concerned with persistence. The view should probably not be concerned with the API Key as well, and that's why I have my view binding to the model's loggedIn change ('change:loggedIn') instead of apiKey change...although I am showing the API key in one of my templates for demonstration purpose only. Here's my very simplified sample. Note that I didn't bother with validating the API Key, but you'll probably want to:
Templates:
<script id="logged_in" type="text/html">
You're logged in. Your API key is <%= escape('apiKey') %>. Let's proceed with the application...
</script>
<script id="not_logged_in" type="text/html">
<form class="api_key">
API Key: <input name="api_key" type="text" value="" />
<button type="sumit">Submit</button>
</form>
</script>
Backbone Model and View:
var LoginStatus = Backbone.Model.extend({
defaults: {
loggedIn: false,
apiKey: null
},
initialize: function () {
this.bind('change:apiKey', this.onApiKeyChange, this);
this.set({'apiKey': localStorage.getItem('apiKey')});
},
onApiKeyChange: function (status, apiKey) {
this.set({'loggedIn': !!apiKey});
},
setApiKey: function(apiKey) {
localStorage.setItem('apiKey', apiKey)
this.set({'apiKey': apiKey});
}
});
var AppView = Backbone.View.extend({
_loggedInTemplate: _.template($('#logged_in').html()),
_notLoggedInTemplate: _.template($('#not_logged_in').html()),
initialize: function () {
this.model.bind('change:loggedIn', this.render, this);
},
events: {
'submit .api_key': 'onApiKeySubmit'
},
onApiKeySubmit: function(e){
e.preventDefault();
this.model.setApiKey(this.$('input[name=api_key]').val());
},
render: function () {
if (this.model.get('loggedIn')) {
$(this.el).empty().html(this._loggedInTemplate(this.model));
} else {
$(this.el).empty().html(this._notLoggedInTemplate(this.model));
}
return this;
}
});
var view = new AppView({model: new LoginStatus()});
$('body').append(view.render().el);