I have an api endpoint at /api/pin that returns the following JSON:
{
"num_results": 4,
"objects": [
{
"id": 1,
"image": "http://placekitten.com/200/200/?image=9",
"title": "Test"
},
{
"id": 2,
"image": "http://placekitten.com/200/200/?image=9",
"title": "test"
},
{
"id": 3,
"image": "www.test.com",
"title": "test"
}
],
"page": 1,
"total_pages": 1
}
I want to map this into a knockout observable array and display it in my page. Here's my js file:
define(['knockout', 'text!./pins.html'], function(ko, templateMarkup) {
function Pins(params) {
var self = this;
self.agents = ko.observableArray([]);
$.getJSON('/api/pin', function(data){
self.agents = ko.mapping.fromJS(data);
});
}
return { viewModel: Pins, template: templateMarkup };
});
My html:
<b data-bind="agents.num_results"> results </b>
<table>
<tbody data-bind="foreach: agents.objects">
<tr>
<td data-bind="text: image"></td>
<td data-bind="text: title"></td>
</tr>
</tbody>
</table>
I get nothing rendered, other than the word "results".
I know that I can create a view model that represents the JSON data and push it into the array during the getJSON (and I've done that successfully). But I thought the whole point of the knockout mappings library was so that you didn't have to do that. I guess I'm having trouble wrapping my head around what exactly I'm doing wrong here. Seems like I must be missing something super obvious, but I'm pulling my hair out trying to figure out what's wrong.
So I figured it out. Basically I had to mock up a PinViewModel like this:
define(['knockout', 'text!./pins.html'], function(ko, templateMarkup) {
function PinVewModel (){
this.objects = ko.observableArray();
}
function Pins(params) {
var self = this;
self.agents = new PinVewModel();
$.getJSON('/api/pin', function(data){
ko.mapping.fromJS(data, {}, self.agents);
});
}
return { viewModel: Pins, template: templateMarkup };
});
And if anyone is interested in the POST part...
function Pin(data){
this.image = ko.observable(data.image);
this.title = ko.observable(data.title);
}
this.createPins = function(formElement){
formPin = new Pin({image: formElement.pin_url.value, title: formElement.pin_name.value});
$.ajax("/api/pin", {
data: ko.toJSON(formPin),
type: "post", contentType: "application/json",
success: function(result) {
self.pins.objects.push(formPin);
}
});
};
There's probably redundancy I'm doing in here, but it works and achieves the desired results.
Related
I'm trying to learn Backbone and can't seem to match data from the fetch function into my Underscore template. How can can I get the children array in my JSON and match it to the template?
The Backbone.View looks like this:
var Projects = Backbone.Collection.extend({
url: '/tree/projects'
});
var Portfolio = Backbone.View.extend({
el: '.page',
render: function () {
var that = this;
var projects = new Projects();
projects.fetch({
success: function (projects) {
var template = _.template($('#projects-template').html());
that.$el.html(template({projects: projects.models}));
}
})
}
});
At the url: http://localhost:3000/portfolio/api/tree/projects
The JSON returned looks like this:
{
id:"projects",
url:"http://localhost:8888/portfolio/projects",
uid:"projects",
title:"Projects",
text:"",
files:[
],
children:[
{
id:"projects/example-1",
url:"http://localhost:8888/portfolio/projects/example-1",
uid:"example-1",
title:"Example 1",
images:"",
year:"2017",
tags:"Website",
files:[
],
children:[
]
},
{
id:"projects/example-2",
url:"http://localhost:8888/portfolio/projects/example-2",
uid:"example-2",
title:"Example #"2
text:"Example 2's text",
year:"2016",
tags:"Website",
files:[
{
url:"http://localhost:8888/portfolio/content/1-projects/4-example-2/example_ss.png",
name:"example_ss",
extension:"png",
size:244845,
niceSize:"239.11 kB",
mime:"image/png",
type:"image"
}
],
children:[
]
},
]
}
My Underscore file looks like this:
<script type="text/template" id="projects-template">
<h4>tester</h4>
<div>
<% _.each(projects.children, function (project) { %>
<div>
<div><%= project.get('year') %></div>
<div><%= project.get('title') %></div>
<div><%= project.get('tags') %></div>
</div>
<% }); %>
</div>
</script>
You can define a parse method on the collection:
var Projects = Backbone.Collection.extend({
url: '/tree/projects',
parse: function(response){
/* save other data from response directly to collection if needed.
for eg this.title = response.title; */
return response.children; // now models will be populated from children array
}
});
Do not use parse
While I usually agree with TJ, using parse on the collection is more like a hack than a definite solution. It would work only to get the children projects of a project and nothing more.
The parse function shouldn't have side-effects on the collection and with this approach, changing and saving fields on the parent project wouldn't be easily possible.
It also doesn't deal with the fact that it's a nested structure, it's not just a wrapped array.
This function works best when receiving wrapped data:
{
data: [{ /*...*/ }, { /*...*/ }]
}
Models and collections
What you have here are projects that have nested projects. A project should be a model. You also have files, so you should have a File model.
Take each resource and make a model and collection classes with it. But first, get the shared data out of the way.
var API_ROOT = 'http://localhost:8888/';
File
var FileModel = Backbone.Model.extend({
defaults: {
name: "",
extension: "png",
size: 0,
niceSize: "0 kB",
mime: "image/png",
type: "image"
}
});
var FileCollection = Backbone.Collection.extend({
model: FileModel
});
Project
var ProjectModel = Backbone.Model.extend({
defaults: function() {
return {
title: "",
text: "",
files: [],
children: []
};
},
getProjects: function() {
return this.get('children');
},
setProjects: function(projectArray, options) {
return this.set('children', projectArray, options);
},
getFiles: function() {
return this.get('files');
},
getSubProjectUrl: function() {
return this.get('url');
}
});
var ProjectCollection = Backbone.Collection.extend({
model: ProjectModel,
url: API_ROOT + '/tree/projects'
});
Project view
Then, make a view for a project. This is a simple example, see the additional information for tips on optimizing the rendering.
var ProjectView = Backbone.View.extend({
template: _.template($('#projects-template').html()),
initialize: function(options) {
this.options = _.extend({
depth: 0, // default option
}, options);
// Make a new collection instance with the array when necessary
this.collection = new ProjectCollection(this.model.getProjects(), {
url: this.model.getSubProjectUrl()
});
},
render: function() {
this.$el.html(this.template(this.model.toJSON()));
this.$projectList = this.$('.list');
// use the depth option to avoid rendering too much projects
if (this.depth > 0) this.collection.each(this.renderProject, this);
return this;
}
renderProject: function(model) {
this.$projectList.append(new ProjectView({
model: model,
depth: depth - 1
}).render().el);
}
});
With a template like this:
<script type="text/template" id="projects-template">
<h4><%= title %></h4>
<span><%= year %></span><span><%= tags %></span>
<p><%= text %></p>
<div class="list"></div>
</script>
Using the view:
var model = new ProjectModel({ id: "project" });
model.fetch({
success: function() {
var project = new ProjectView({
model: model,
depth: 2
});
}
});
Additional info
Nested models and collections
Efficiently rendering a list
I have an issue with Knockout binding to a model here is my code. The code fires and returns a JSON object but the table is empty. Any suggestions would be appreciated.
HTML
<table style="border: double">
<thead>
<tr>
<td>jobId</td>
</tr>
</thead>
<!--Iterate through an observableArray using foreach-->
<tbody data-bind="foreach: Jobs">
<tr style="border: solid" data-bind="click: $root.getselectedjob" id="updtr">
<td><span data-bind="text: $data.jobId "></span></td>
</tr>
</tbody>
</table>
Javascript
var JobViewModel = function () {
//Make the self as 'this' reference
var self = this;
//Declare observable which will be bind with UI
self.jobId = ko.observable("");
self.name = ko.observable("");
self.description = ko.observable("");
//The Object which stored data entered in the observables
var jobData = {
jobId: self.jobId,
name: self.name,
description: self.description
};
//Declare an ObservableArray for Storing the JSON Response
self.Jobs = ko.observableArray([]);
GetJobs(); //Call the Function which gets all records using ajax call
//Function to Read All Employees
function GetJobs() {
//Ajax Call Get All Job Records
$.ajax({
type: "GET",
url: "/Client/GetJobs",
contentType: "application/json; charset=utf-8",
dataType: "json",
success: function (data) {
debugger;
self.Jobs(data); //Put the response in ObservableArray
},
error: function (error) {
alert(error.status + "<--and--> " + error.statusText);
}
});
//Ends Here
}
//Function to Display record to be updated. This will be
//executed when record is selected from the table
self.getselectedjob = function (job) {
self.jobId(job.jobId),
self.name(job.name),
self.description(job.description)
//,
//self.DeptName(employee.DeptName),
//self.Designation(employee.Designation)
};
};
ko.applyBindings(new JobViewModel());
C# Method to get jobs
public ActionResult GetJobs(string AccountIDstr)
{
//parse this as parameter
int AccountID = Convert.ToInt32(AccountIDstr);
AccountID = 1;
var jobs = (from c in db.jobs
select c).OrderByDescending(m => m.jobId).ToList();
//"Business logic" method that filter jobs by the account id
var jobsFilter = (from e in jobs
where (AccountID == null || e.accountId == AccountID)
select e).ToList();
var jobsresult = from jobrows in jobsFilter
select new
{
jobId = jobrows.jobId.ToString(),
name = jobrows.name,
description = jobrows.description
};
return Json(new
{
Jobs = jobsresult
},
JsonRequestBehavior.AllowGet);
}
JSON Object
{"Jobs":[{"jobId":"5","name":"Job 5 ","description":"Job 5 description"},{"jobId":"1","name":"Job 1 ","description":"Job 1 description"}]}
Your Jobs is an observableArray, but the data is wrapped in an object. When you set the value in GetJobs, you should be doing
self.Jobs(data.Jobs);
Here's a runnable snippet that works. You should be able to run this using your ajax function to populate data. If it doesn't work, examine what you're getting back.
var JobViewModel = function() {
//Make the self as 'this' reference
var self = this;
//Declare observable which will be bind with UI
self.jobId = ko.observable("");
self.name = ko.observable("");
self.description = ko.observable("");
//The Object which stored data entered in the observables
var jobData = {
jobId: self.jobId,
name: self.name,
description: self.description
};
//Declare an ObservableArray for Storing the JSON Response
self.Jobs = ko.observableArray([]);
GetJobs(); //Call the Function which gets all records using ajax call
//Function to Read All Employees
function GetJobs() {
//Ajax Call Get All Job Records
var data = {
"Jobs": [{
"jobId": "5",
"name": "Job 5 ",
"description": "Job 5 description"
}, {
"jobId": "1",
"name": "Job 1 ",
"description": "Job 1 description"
}]
};
setTimeout(function() {
self.Jobs(data.Jobs);
}, 500);
}
//Function to Display record to be updated. This will be
//executed when record is selected from the table
self.getselectedjob = function(job) {
self.jobId(job.jobId),
self.name(job.name),
self.description(job.description)
//,
//self.DeptName(employee.DeptName),
//self.Designation(employee.Designation)
};
};
ko.applyBindings(new JobViewModel());
<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.7.1/jquery.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/knockout/2.1.0/knockout-min.js"></script>
<table style="border: double">
<thead>
<tr>
<td>jobId</td>
</tr>
</thead>
<!--Iterate through an observableArray using foreach-->
<tbody data-bind="foreach: Jobs">
<tr style="border: solid" data-bind="click: $root.getselectedjob" id="updtr">
<td><span data-bind="text: $data.jobId "></span>
</td>
</tr>
</tbody>
</table>
heres my code
var readAll = function () {
$.ajax(
{
url: _spPageContextInfo.webServerRelativeUrl +
"/_api/web/lists/getByTitle('PhoneBook')/items/" +
"?$select=Id, Title, pb_FirstName, pb_PhoneNumber" +
"&$orderby=Title,pb_FirstName, pb_PhoneNumber",
type: "GET",
headers: {
"accept": "application/json;odata=verbose",
},
success: function (data) {
console.log(data);
},
error: function (err) {
alert(JSON.stringify(err));
}
}
);
};
$(document).ready(function () {
readAll();
});
data = {
"d": {
"results": [{
"__metadata": {
"id": "b4d773a6-f31e-442d-8974-38c535d491d6",
"uri": "mysite:6555",
"etag": "\"1\"",
"type": "SP.Data.LST_x005f_PhoneBookListItem"
},
"Id": 1,
"Title": "name11",
"pb_FirstName": "name",
"pb_PhoneNumber": "1234",
"ID": 1
}]
}
}
function readList(data) {
var html = [];
html.push("<table><thead><tr><th>ID</th><th>First Name</th>" +
"<th>Last Name</th><th>Phone</th></tr></table></thead>");
data = data.d.results;
for (var i = 0; i < results.length; i++) {
html.push("<tr><td>");
html.push(results[i].ID);
html.push("</td><td>");
html.push(results[i].Title);
html.push("</td><td>");
html.push(results[i].pb_FirstName);
html.push("</td><td>");
html.push(results[i].pb_PhoneNumber);
html.push("</td><tr>");
}
html.push("</table>"`enter code here`);
$('.table').html(html.join(''));
}
so i get an in the console a json array with the data. im trying to bring the data in my html table. but i dont know how. so i need to bring my data object and render it in the right html
hope u can help me
Replace
console.log(data);
with the strangely named
readList(data);
You should use $.get for get data and handlebars for render html with data, its easy.
// Link handlebars
var template = "<table>\
<thead>\
<tr>\
<th>ID</th>\
<th>Title</th>\
<th>First Name</th>\
<th>Phone</th>\
</tr>\
</thead>\
<tbody>\
{{#objects}}\
<tr>\
<td>{{ Id }}</td>\
<td>{{ Title }}</td>\
<td>{{ pb_FirstName }}</td>\
<td>{{ pb_PhoneNumber }}</td>\
</tr>\
{{/objects}}\
</tbody>\
</table>";
$.get('someurl')
.success(function(response){
//here render data to html
// We can use Handlebars, is a engine of templates.
$("#box").html(Handlebars.compile(template)({'objects' : response['d']['results']}));
})
.error(function(err){
alert(JSON.stringify(err));
});
I'm having trouble accessing the nested JSON using the ng-repeat directives. I know it is working because the not nested part of the JSON object is displaying.
Here is a plunker of my code: http://plnkr.co/edit/V2iURMa8t7vG9AqDFMOf?p=preview
JavaScript:
var app = angular.module("app", [ ]);
app.controller("AppTest", function($scope){
$scope.currentInfo=[{"id":0,"article":"JavaScript Conquers the World","publisher":"Daily Times","meta_data":{"comments":4}},
{"id":1,"article":"The Problem with Foobar","publisher":"New York Times","meta_data":{"comments":27}}];
$scope.tableInfo=[{"name":"id","dataType":"Integer","isEditable":false},
{"name":"article","dataType":"String","isEditable":true},
{"name":"publisher","dataType":"String","isEditable":false},
{"name":"meta_data.comments","dataType":"Integer","isEditable":false}];
});
HTML:
<body ng-app="app" ng-controller="AppTest as app"
<table>
<tbody ng-repeat="anotherItem in currentInfo">
<tr>
<td ng-repeat="item in tableInfo">{{anotherItem[item.name]}}</td>
</tr>
</tbody>
</table>
</body>
Another solution that is better is to add function in the controller that will resolve the value for you. The issue with your solution is that you need Angular to resolve meta_data.comments, but it is treating it as the string that is used in the array lookup since it has already resolved item.name.
$scope.resolveLookup = function(object, lookup) {
var depth = lookup.split('.').length;
var currentObj = object;
for(var x=0; x<depth; x++) {
currentObj = currentObj[lookup.split('.')[x]];
}
return currentObj;
};
Then change the HTML to look like:
<td ng-repeat="item in tableInfo">{{resolveLookup(anotherItem,item.name)}}</td>
Here is the Plunker: http://plnkr.co/edit/RVd2ncwstyQtCtdhcC9U?p=preview
The issue is that it is putting 'metadata.comments' in the [] and it doesn't realize that it needs to be resolved again by angular. I can't think of fix without changing the data structure of your 'tableInfo' object.
Here is how I would do it.
Change table info to:
$scope.tableInfo = [{
"name": ["id"],
"dataType": "Integer",
"isEditable": false
}, {
"name": ["article"],
"dataType": "String",
"isEditable": true
}, {
"name": ["publisher"],
"dataType": "String",
"isEditable": false
}, {
"name": ["meta_data", "comments"],
"dataType": "Integer",
"isEditable": false
}];
Change your HTML to:
<td ng-repeat="item in tableInfo" ng-if="item.name.length==1">{{anotherItem[item.name[0]]}}</td>
<td ng-repeat="item in tableInfo" ng-if="item.name.length==2">{{anotherItem[item.name[0]][item.name[1]]}}</td>
Here is the Plunker: http://plnkr.co/edit/q9lHZ2TD7WZ74b2f6Ais?p=preview
I'm dealing with pretty big amounts of json and the data is something like this:
{
"name": "John Smith",
"age": 32,
"employed": true,
"address": {
"street": "701 First Ave.",
"city": "Sunnyvale, CA 95125",
"country": "United States"
},
"children": [
{
"name": "Richard",
"age": 7,
"field": {
"field": "value"
}
}
]
}
Whenever I change anything I get a new response which is somewhat similar to the previous data, but where new properties might have been added, stuff might have been removed and so on.
My testcode is something like this (don't mind the infinite amount of bad practices here):
<div data-viewmodel="whatevz">
<span data-bind="text: stuff['nested-thingy']"></span>
</div>
<script>
function vm() {
var self = this;
this.stuff = ko.observable();
require(["shop/app"], function (shop) {
setTimeout(function () {
self.stuff(shop.stuff);
}, 1200);
});
}
ko.applyBindings(new vm(), $("[data-viewmodel]")[0]);
</script>
I want stuff['nested-thingy'] to be updated whenever stuff is updated. How do I do this without all kinds of mapping and making everything observable?
You should only have to update your biding:
<div data-viewmodel="whatevz">
<span data-bind="text: stuff()['nested-thingy']"></span>
</div>
You have to access the value of the observable with the (). That returns your object and then you can access it. The content of the binding is still dependent on the observable stuff therefore it should update whenever stuff is updated.
At least my fiddle is working that way: http://jsfiddle.net/delixfe/guM4X/
<div data-bind="if: stuff()">
<span data-bind="text: stuff()['nested-thingy']"></span>
</div>
<button data-bind="click: add1">1</button>
<button data-bind="click: add2">2</button>
Note the data-bind="if: stuff(). That is necessary if your stuff's content is empty at binding time or later...
function Vm() {
var self = this;
self.stuff = ko.observable();
self.add1 = function () {
self.stuff({'nested-thingy': "1"});
};
self.add2 = function () {
self.stuff({'nested-thingy': "2"});
};
}
ko.applyBindings(new Vm());
Any reason you can't use the mapping plugin to deal with the mapping for you? You can use the copy option for the properties that you don't want to be made observables:
var mapping = {
'copy': ["propertyToCopy"]
}
var viewModel = ko.mapping.fromJS(data, mapping);