ko.observableArray and JSON data massaging - javascript

I'm using web services to load data to client side. For binding purposes I need to expand on data that I get. I.e. I don't want to massage all data on server side.
For example, object Trip { Id: "123", Status: "P" }
In HTML I bind table to observableArray and want to display "Pending" instead of "P".
I'm coming from Silverlight/MVVM and usually you would use converter or just add new R/O property to object.
Not sure how this scenario should be handled in knockout.js

You may find here all you need :
http://net.tutsplus.com/sessions/knockout-succinctly/
Have a good read.

If you are just looking for a converter, computed observables are a good candidate.
var Tip = function(data) {
var self = this;
self.id = data.id;
self.status = ko.observable(data.status);
//You may prefer fullStatus, or statusName
self.statusConverter = ko.computed(function() {
return self.statusMap[self.status()];
});
};
Tip.prototype.statusMap = {
P: "Pending",
O: "Open",
C: "Closed"
};
which you can bind to like this:
<td data-bind="text: statusConverter"></td>
You can see it in this fiddle

Related

Oracle JET - Creating chart with JSON-data

I'm trying to populate a pie chart with some JSON-data. My datasource is restcountries.eu/rest/v2/all. I retrieve the data with a $.getJSON and I create a temporary array which in turn is used as a datasource. I then bind the source to the pie chart. That what I think I'm doing atleast..
The error I'm getting is following
my-piechart-viewModel.js:25 Uncaught (in promise) ReferenceError: data is not
defined
at new myPieChartModel (my-piechart-viewModel.js:25)
at Object.CreateComponent (ojcomposite.js:1808)
at ojcustomelement.js:385
My code looks like this
HTML
<oj-chart id="pieChart1" aria-label= 'TestPieChart'
type="pie"
series='[[datasource]]'
style="max-width:500px;width:100%;height:350px;">
</oj-chart>
JS
function myPieChartModel() {
var self = this;
self.data = ko.observableArray();
$.getJSON("https://restcountries.eu/rest/v2/all").
then(function(countries) {
var tempArray = [];
$.each(countries, function() {
tempArray.push({
name: this.name,
population: this.population
});
});
self.data(tempArray);
});
self.datasource = ko.observableArray(data);
}
return myPieChartModel;
What am I doing wrong? I'm very new to Oracle's JET, and I have very little experience with JSON overall.
If you defined something as self.data you cannot later access it by calling just data. So you need to change your last line to:
self.datasource = ko.observableArray(self.data);
Even if you do do that, you'll get this error:
The argument passed when initializing an observable array must be an array, or null, or undefined.
That is, you cannot pass an observableArray into an observableArray. self.data should just be a normal JS array.
self.data = [];
But a normal JS array does not fire any events when its values are changed, so you'll need to update the observableArray datasource again. Your full code can be like this:
function myPieChartModel() {
var self = this;
self.data = [];
self.datasource = ko.observableArray(self.data);
$.getJSON("https://restcountries.eu/rest/v2/all").
then(function(countries) {
$.each(countries, function() {
self.data.push({
name: this.name,
population: this.population
});
});
self.datasource(self.data);
});
}
return myPieChartModel;
Let me know if it works. I have a feeling that your JSON data will also need to be modified like this:
self.data.push({name: this.name,
items: [this.population]
});
Why? Because that's how Oracle JET expects it. Here's the documentation.

Backbone trying a put instead of a post

For some odd reason, Backbone is trying to put my model object instead of posting it to the server.
The error is an HTTP 500 error because Backbone is trying to put a model with no id (because I have made it undefined):
PUT /api/teams/undefined 500 285ms - 135b
Here's my code:
this.model.id = undefined;
this.model._id = undefined;
teamCollection.add(this.model);
this.model.save(null,
{
success: function (model) {
$('.top-right').notify({
message: {text: ' New team added! '},
type: 'info',
fadeOut: {
delay: 3500
}
}).show();
window.userHomeMainTableView.render();
}
,
error: function (model) {
teamCollection.remove(model);
$('.top-right').notify({
message: {text: ' Error adding team :( '},
type: 'danger',
fadeOut: {
delay: 3500
}
}).show();
}
});
even after "forcing" the model.id and model._id to be undefined, Backbone still tries to do an HTTP PUT. How can this be?
The syncing process internally uses Model#isNew to decide if it should PUT or POST. isNew is very simple minded:
isNew model.isNew()
[...] If the model does not yet have an id, it is considered to be new.
and that check is done using:
!this.has(this.idAttribute)
The has method is just a simple wrapper around attr:
has: function(attr) {
return this.get(attr) != null;
}
so these are irrelevant:
this.model.id = undefined;
this.model._id = undefined;
when Backbone is deciding to PUT or POST, those don't really remove the id attribute, they break your model.
I think you'd be better off copying the model (without the id) and saving the copy:
var data = m.toJSON();
delete data.id;
var copy = new Model(data);
Or better, create a whole new model, populate it with data, and then save the new model instance.
I think you'd need to unset the id attribute and manually remove the id property to make what you're trying to do work.
that's strange it looks like it should do a POST from your code. It looks like you can ovverride it though. From the second answer here
fooModel.save(null, {
type: 'POST'
});
use the following code snippet to set the id to undefined.
this.model.set('id',undefined');
although if it's a new model you don't need to do this. it will be undefined by default.
if you have used the idAttribute for defining the model and say it's value is userId then you need to do something like this.
this.model.set('userId',undefined');

Knockout load another AJAX url to combine to model

I have some complex question.
My first JSON url has this properties, ID, Name and Parameter. Then when I call the JSON, I want to go to retrieve another JSON URL based on the ID to get the child ID.
Then I want to output as Child ID, Parent Name and Parent Parameter.
Here is the jsFiddle http://jsfiddle.net/c3L7gr4w/1/
And here is the model url 1
[
{
"ParemeterValues": "Actual,2011,SYS.LoadCompanies,y,y",
"ID": "1771cdf7-7a73-49e4-8538-0d0cad965226",
"Name": "EXEC.Data.PLandBS.FromFMISMultipleCompanies",
},
{
"ParemeterValues": "Actual,2012",
"ID": "19439ce4-240c-4f2a-98ee-47cb1708b339",
"Name": "Data.BS.BringForwardOpeningBalances",
}
]
and the model url2
{
"ID": "1771cdf7-7a73-49e4-8538-0d0cad965226",
"Name": "EXEC.Data.PLandBS.FromFMISMultipleCompanies",
"TM1.ChoreProcessesProcess":
[
{
"Name": "EXEC.Data.PLandBS.FromFMISMultipleCompanies",
"ID": "dd1acc0b-51ff-4844-b6c4-c67640b326c4",
}
]
}
I think you need to adjust your dataMappingOptions.key so that KO can return a different key when you load your child model in on top of your original.
I have a working fiddle here, I think - http://jsfiddle.net/sifriday/c3L7gr4w/4/
dataMappingOptions now looks like:
var dataMappingOptions = {
key: function(data) {
var ChildID = "";
if (data["TM1.ChoreProcessesProcess"] != undefined)
ChildID = data["TM1.ChoreProcessesProcess"][0].ID
return data.ID + ChildID;
},
create: function(options) {
return new Person(options.data);
}
};
The problem with this is that when you load your updatedData KO will now use the mapping to destroy your original people and create new ones. However, updatedData is missing the ParemeterValue, so you need to merge this in from your original JSON. This will be OK even when you are using Ajax, because you can save the original JSON in a variable somewhere:
loadUpdatedData: function() {
// You need to merge in paremeterValues from your orig JSON
updatedData[0].ParemeterValues = data[0].ParemeterValues;
// Now good to go...
ko.mapping.fromJS(updatedData, dataMappingOptions, viewModel.people);
}
(I've also done it so that ChildID is appended as an extra property, but you should be able to see from this how to make it replace the original ID in both the People view-model and in the dataMappingOptions.key)

Is it bad to add JSON on HTML data attribute?

Since HTML data attribute allows adding any custom data, I wonder if it is a good idea to include a set of JSON list as a data attribute? Then, the corresponding JSON can be easily accessed by JavaScript events with getAttribute("data-x").
In fact, my question is that: Is it standard, efficient, and reasonable to add a large set of data to an HTML attribute?
For example
<div data-x="A LARGE SET OF JSON DATA" id="x">
Or a large set of JSON data must be stored within <script> tag, and an HTML attribute is not a right place for a large set of data, even for data attribute.
Instead of storing everything in the data attribute you could use an identifier to access the data.
So for example you could do this :
var myBigJsonObj = {
data1 : { //lots of data},
data2 : { //lots of data}
};
and then you had some html like so :
<div data-dataId="data1" id="x">
You can use jquery to get the data now like so :
var dataId = $('#x').attr('data-dataId');
var myData = myBigJsonObj[dataId];
This is the best approach imho.
Say you want to save the object var dataObj = { foo: 'something', bar: 'something else' }; to an html data attribute.
Consider first stringifying the object such that we have
var stringifiedDataObj = JSON.stringify(dataObj);
Then you can use jQuery to set the stringifiedDataObj as the data attribute e.g. with the jQuery.data() API
While there's nothing to stop you embedding a long string of JSON in a data attribute, arguably the more "correct" way of doing it would be to add one data attribute per property of the JSON data. eg:
Javascript:
var dataObj = { foo: 'something', bar: 'something else' }
HTML:
<div data-foo="something" data-bar="something else"></div>
This way each piece of data in the JSON object corresponds to a separate, independently-accessible piece of data attached to the DOM element.
Bear in mind that either way you'll need to escape the values you're inserting into the HTML - otherwise stray " characters will break your page.
Technically you can, and I have seen several sites do this, but another solution is to store your JSON in a <script> tag and put a reference to the script tag in the data attribute. This is a better solution than just storing the data as a JS object in an actual script if the data is rendered to the page server-side, but there are CSP restrictions on inline script tags, for example.
HTML
<div data-data-id="#MyScriptData" id="x"></div>
<script type="application/json" id="MyScriptData">
{
"fruit": "apple",
...
}
</script>
JS
$(function () {
var dataId = $("#x").data("data-id");
var dataTag = $(dataId);
var dataJson = dataTag.html(); // returns a string containing the JSON data
var data = JSON.parse(dataJson);
...
});
You could make use of Map. Where your element will be the key, and the value at that key could be an object in which you store wanted data. Something like this (not tested though):
(function(global) {
const map = new Map();
global.CustomData = {
add(element, key, data) {
if (!map.has(element)) {
map.set(element, {});
}
map.get(element)[key] = data;
return map.get(element);
},
get(element, key) {
if (!map.has(element)) {
return null;
}
if (key !== undefined) {
return map.get(element)[key];
}
return map.get(element)
},
remove(element, key) {
if (!map.has(element)) {
return false;
}
delete map.get(element)[key];
if (Object.keys(map.get(element)).length === 0) {
map.delete(element);
}
return true;
},
clear(element) {
if (!map.has(element)) {
return false;
}
map.delete(element);
return true;
}
}
})(window);

When do I need a model in backbone.js?

I'm new to Backbone.js, and someone who comes out of the 'standard' model of JS development I'm a little unsure of how to work with the models (or when).
Views seem pretty obvious as it emulates the typical 'listen for event and do something' method that most JS dev's are familiar with.
I built a simple Todo list app and so far haven't seen a need for the model aspect so I'm curious if someone can give me some insight as to how I might apply it to this application, or if it's something that comes into play if I were working with more complex data.
Here's the JS:
Todos = (function(){
var TodoModel = Backbone.Model.extend({
defaults: {
content: null
}
});
var TodoView = Backbone.View.extend({
el: $('#todos'),
newitem: $('#new-item input'),
noitems: $('#no-items'),
initialize: function(){
this.el = $(this.el);
},
events: {
'submit #new-item': 'addItem',
'click .remove-item': 'removeItem'
},
template: $('#item-template').html(),
addItem: function(e) {
e.preventDefault();
this.noitems.remove();
var templ = _.template(this.template);
this.el.append(templ({content: this.newitem.val()}));
this.newitem.val('').focus();
return this;
},
removeItem: function(e){
$(e.target).parent('.item-wrap').remove();
}
});
self = {};
self.start = function(){
new TodoView();
};
return self;
});
$(function(){
new Todos(jQuery).start();
});
Which is running here: http://sandbox.fluidbyte.org/bb-todo
Model and Collection are needed when you have to persist the changes to the server.
Example:
var todo = new TodoModel();
creates a new model. When you have to save the save the changes, call
todo.save();
You can also pass success and error callbacks to save . Save is a wrapper around the ajax function provided by jQuery.
How to use a model in your app.
Add a url field to your model
var TodoModel = Backbone.Model.extend({
defaults: {
content: null
},
url: {
"http://localhost";
}
});
Create model and save it.
addItem: function(e) {
e.preventDefault();
this.noitems.remove();
var templ = _.template(this.template);
this.el.append(templ({content: this.newitem.val()}));
this.newitem.val('').focus();
var todo = new TodoModel({'content':this.newitem.val()});
todo.save();
return this;
},
Make sure your server is running and set the url is set correctly.
Learning Resources:
Check out the annotated source code of Backbone for an excellent
explanation of how things fall into place behind the scenes.
This Quora question has links to many good resources and sample apps.
The model is going to be useful if you ever want to save anything on the server side. Backbone's model is built around a RESTful endpoint. So if for example you set URL root to lists and then store the list information in the model, the model save and fetch methods will let you save/receive JSON describing the mode to/from the server at the lists/<id> endpoint. IE:
ToDoListModel = Backbone.model.extend( {
urlRoot : "lists/" } );
// Once saved, lives at lists/5
list = new ToDoListModel({id: 5, list: ["Take out trash", "Feed Dog"] });
list.save();
So you can use this to interact with data that persists on the server via a RESTful interface. see this tutorial for more.
I disagree with the idea that model is needed only to persist changes (and I am including LocalStorage here, not only the server).
It is nice to have representation of models and collections so that you have object to work with and not only Views. In your example you are only adding and removing divs (html) from the page, which is something you can do normally with jQuery. Having a Model created and added to a Collection everytime you do "add" and maybe removed when you clear it will allow you some nice things, like for example sorting (alphabetically), or filtering (if you want to implement the concept of "complete" to-do).
In your code, for example:
var TodoModel = Backbone.Model.extend({
defaults: {
content: null
complete: false
}
});
var Todos = Backbone.Collection.extend({
model: TodoModel
})
In the View (irrelevant code is skipped):
// snip....
addItem: function(e) {
e.preventDefault();
this.noitems.remove();
var templ = _.template(this.template);
var newTodo = new TodoModel({ content: this.newitem.val() });
this.collection.add(newTodo); // you get the collection property from free from the initializer in Backbone
this.el.append(templ({model: newTodo})); // change the template here of course to use model
this.newitem.val('').focus();
return this;
},
Initialize like this:
self.start = function(){
new TodoView(new Todos());
};
Now you have a backing Collection and you can do all sort of stuff, like filtering. Let's say you have a button for filtering done todos, you hook this handler:
_filterDone: function (ev) {
filtered = this.collection.where({ complete: true });
this.el.html(''); // empty the collection container, I used "el" but you know where you are rendering your todos
_.each(filtered, function (todo) {
this.el.append(templ({model: todo})); // it's as easy as that! :)
});
}
Beware that emptying the container is probably not the best thing if you have events hooked to the inner views but as a starter this works ok.
You may need a hook for setting a todo done. Create a button or checkbox and maybe a function like this:
_setDone: function (ev) {
// you will need to scope properly or "this" here will refer to the element clicked!
todo = this.collection.get($(ev.currentTarget).attr('todo_id')); // if you had the accuracy to put the id of the todo somewhere within the template
todo.set('complete', true);
// some code here to re-render the list
// or remove the todo single view and re-render it
// in the simplest for just redrawr everything
this.el.html('');
_.each(this.collection, function (todo) {
this.el.append(templ({model: todo}));
});
}
The code above would not have been so easy without Models and Collections and as you can see it does not relate in any way with the server.

Categories

Resources