Javascript strange behavior in Json Object Manipulation - javascript

I really can't get my hand around what is happening there and I really think that I should share this with you guys:
I am making a call to my api at '/products' and the response looks something like this in postman:
[
{
"id": 2,
"name": "some_product_name",
"description": "description",
"price": "$120.00",
"firmware_version": "1.0",
"quantity_in_stock": 50,
"selling": true,
"build_version": "1.2",
"category_id": 1,
"available_colors": [
{
"name": "blue",
"in_stock": true,
"picture": {
"type": "Buffer",
"data": []
}
},
{
"name": "black",
"in_stock": true,
"picture": null
},
{
"name": "silver",
"in_stock": true,
"picture": {
"type": "Buffer",
"data": []
}
}
]
}
]
Now what I am trying to do here is to create a new object called products_showcase that has one entry per product color, with the same informations except for the available_colors property, replaced by the color object:
$scope.initModel = function() {
$http({
method: 'GET',
url: '/products'
}).then(function(resp) {
console.log(resp.data);
$scope.products = resp.data;
$scope.products.forEach((item, index, array) => {
item.available_colors.forEach((color, index, array) => {
var product = item;
product.color = {};
product.color = color;
delete product['available_colors'];
$scope.products_showcase.push(product);
});
});
}, function(error) {
$scope.error = error;
});
};
But then, something really strange is happening:
The available_colors property gets deleted also in the response object, that I did not touch in the code in any way.
The color property gets added to the response object too.
The products_showcase is an array containing the same object 3 times with the color property equal to the firs color of the $scope.products.available_colors object i am iterating through

Javascript won't create new object when you assign it to a variable. You can use Object.create function to create a new object
from existing one.
$scope.initModel = function() {
$http({
method: 'GET',
url: '/products'
}).then(function(resp) {
console.log(resp.data);
$scope.products = Object.create(resp.data);
$scope.products.forEach((item, index, array) => {
item.available_colors.forEach((color, index, array) => {
var product = Object.create(item);
product.color = {};
product.color = color;
delete product['available_colors'];
$scope.products_showcase.push(product);
});
});
}, function(error) {
$scope.error = error;
});
};

Javascript assignment operator doesn't apparently create a new instance of the object for the new variable, it simply creates a pointer to the memory address of the 'father' object.
Example code:
var object = {property: 'myProp'};
console.log(object);
{property: 'myProp'}
var newObj = object;
newObj.newProperty = 'myNewProp';
console.log(object);
{property: 'myProp', newProperty: 'myNewProp'}
To create a new instance of the object we have to use the Object.create() method.

Related

How to check whether a JSON key's value is an array or not if you don't know the key

How do I check whether a value in an JSON key's value is an array if you don't know the key's name?
I may receive 2 different formats of a JSON object:
{
"person": [{
"id": "1",
"x": "abc",
"attributes": ["something"]
},
{
"id": "1",
"x": "abc"
}
]
}
Depending on the format I will parse it differently. My goal is to create an if statement that would detect whether the value of the key is an Array or just a value, without knowing the name of the key (in above code let's assume I don't really know the name of "attributes"). How do I achieve that? Not only do I have to loop through all person objects, but also all of it's keys.
I found a solution that does that knowing the name of the attribute and there's just one object "person", but don't know how to build on that with multiple "person" objects and not knowing the name of the key?
if (Array.isArray(json.person['attributes'])) // assuming I hold the JSON in json var and I parse it with JSON.parse
{
}
You can try something like this:
Data payload:
const payload = {
person: [
{
id: 1,
x: 'abc',
attributes: ['something']
},
{
id: 1,
x: 'abc'
}
]
};
Function that will return if some entry has an Array as value:
const arrayEntries = json => {
let response = [{
isArray: false,
jsonKey: null,
jsonValue: null
}];
Object.entries(json).map(entry => {
if (Array.isArray(entry[1])) {
response.push({
isArray: true,
jsonKey: entry[0],
jsonValue: entry[1]
});
}
});
return response;
}
Usage example:
payload.person.map(person => {
console.log(arrayEntries(person));
});
Return will be something like this:
Codepen link: https://codepen.io/WIS-Graphics/pen/ExjVPEz
ES5 Version:
function arrayEntries(json) {
var response = [{
isArray: false,
jsonKey: null,
jsonValue: null
}];
Object.entries(json).map(function(entry) {
if (Array.isArray(entry[1])) {
response.push({
isArray: true,
jsonKey: entry[0],
jsonValue: entry[1]
});
}
});
return response;
}
payload.person.map(function(person) {
console.log(arrayEntries(person));
});

Javascript destructure with computed values, level shifting and new object creation

I get a json response from an api that I want to groom and create a new object from.
const things = [{
"menu": {
"id": "file",
"value": "File",
"popup": {
"menuitem": [{
"value": "3",
"onclick": "CreateNewDoc()"
},
{
"value": "5",
"onclick": "OpenDoc()"
},
{
"value": "8",
"onclick": "CloseDoc()"
}
]
}
}
},
{
"menu": {
"id": "image",
"value": "Image",
"popup": {
"menuitem": [{
"value": "New",
"onclick": "CreateNewImage()"
},
{
"value": "Open",
"onclick": "OpenImage()"
},
{
"value": "Close",
"onclick": "CloseImage()"
}
]
}
}
}
];
I know the old way of doing this like so:
const chs = [];
things.forEach((e) => {
const i = {};
i.Id = e.menu.id;
i.Value = e.menu.value;
i.PopupValue = e.menu.popup.menuitem[0].value;
i.SomethingComputed = e.menu.popup.menuitem[0].value - e.menu.popup.menuitem[1];
i.ShiftedUp = e.menu.popup.menuitem;
chs.push(ch);
});
now I want to do this using ES6 and destructuring. but I don't think I have taken it as far as I can go because I: 1)still have the loop; 2)have to create this new object; and 3) need these separate computed lines.
Can I get this more compact?
const chs = [];
things.forEach((e) => {
const {
Id: {id},
Value: {value},
PopupValue : {menu: {popup} },
} = e;
// computed
const someComputedValue = Value - PopupValue;
// new object
const ch = {
Id,
Value,
SomeComputedValue
}
chs.push(ch);
});
You can use map() instead of forEach(), so you don't need the chs.push() step at the end.
You can put the destructuring directly in the argument list, so you don't need the assignment step. Whether this is more readable is debatable.
If the calculation for SomeComputedValue isn't too complex, you can put it directly in the returned object. Then you can get rid of that assignment, and you can use the shorthand form of arrow function that just returns a value.
const things = [{
"menu": {
"id": "file",
"value": "File",
"popup": {
"menuitem": [{
"value": "3",
"onclick": "CreateNewDoc()"
},
{
"value": "5",
"onclick": "OpenDoc()"
},
{
"value": "8",
"onclick": "CloseDoc()"
}
]
}
}
},
{},
{
"menu": {
"id": "image",
"value": "Image",
"popup": {
"menuitem": [{
"value": "New",
"onclick": "CreateNewImage()"
},
{
"value": "Open",
"onclick": "OpenImage()"
},
{
"value": "Close",
"onclick": "CloseImage()"
}
]
}
}
}
];
const chs = things.map(({
menu: {
id: Id,
value: Value,
popup : PopupValue,
} = {id: "defaultID", value: "defaultValue", popup: "defaultPopup"}}) => ({
Id,
Value,
SomeComputedValue: Value - PopupValue
})
);
console.log(chs);
The non-destructuring way is actually more like this:
const chs = things.map(e => ({
Id: e.menu.id,
Value: e.menu.value,
PopupValue: e.menu.popup.menuitem[0].value,
SomethingComputed: e.menu.popup.menuitem[0].value - e.menu.popup.menuitem[1],
ShiftedUp: e.menu.popup.menuitem,
}));
And destructuring, or just the general concept of splitting stuff into more variables, doesn’t have to be all or nothing:
const chs = things.map(({menu}) => {
const {menuitem} = menu.popup;
return {
Id: menu.id,
Value: menu.value,
PopupValue: menuitem[0].value,
SomethingComputed: menuitem[0].value - menuitem[1],
ShiftedUp: menuitem,
};
});
You cannot avoid loop because things is an array so you have to iterate it. However you can avoid new object creation and pushing in chs array using the map function because it essentially creates a new element for each element of the orignial array and returns the array with newly created elements or objects in you case. So basically both looping and new object creation will be handled by the map function in this case.
Also, you can move the destructuring to the callback parameter of the map function and computation can be performed while returning the object:
const chs=things.map(({menu:{id,value,popup}}) => ({
Id: id,
Value: value,
PopupValue : popup,
SomeComputedValue: value+id
})
);
I have tested the above destructuring, it works for your things array as you provided it.
It's not pretty, but it is possible to do this using destructuring. You could create a arrow function like this which destructures each object in the array and returns a new object. Then use that as callback to map
const things=[{menu:{id:"file",value:"File",popup:{menuitem:[{value:"3",onclick:"CreateNewDoc()"},{value:"5",onclick:"OpenDoc()"},{value:"8",onclick:"CloseDoc()"}]}}},{menu:{id:"image",value:"Image",popup:{menuitem:[{value:"New",onclick:"CreateNewImage()"},{value:"Open",onclick:"OpenImage()"},{value:"Close",onclick:"CloseImage()"}]}}}];
const callback = ({
menu: {
id: Id,
value: Value,
popup: {
menuitem
}
}
}) => ({
Id,
Value,
ShiftedUp: menuitem,
PopupValue: menuitem[0].value,
SomethingComputed: menuitem[0].value - menuitem[1].value
})
console.log(things.map(callback))
You could even destrcuture the menuitem array indexes to get the first 2 values to separate variables like this:
const callback = ({
menu: {
id: Id,
value: Value,
popup: {
menuitem
},
popup: {
menuitem: [
{ value: Value0 },
{ value: Value1 }
]
}
}
}) => ({
Id,
Value,
ShiftedUp: menuitem,
PopupValue: Value0,
SomethingComputed: Value0 - Value1
})

Creating Tableau WDC from associative array

I am creating a Tableau Web Data Connector as described in their Getting Started guide HERE.
I have implemented a similar solution previous with data from a basic associative array, but I am having trouble with my current API call.
I make an API call to an external web service that returns a JSON similar to the one listed below (simplified version)(COMMENT simply added for clarity and not part of original code).
{
"status": true,
"employee": {
"id": 123
},
"company": {
"id": 123
},
"job": {
"id": 123,
"job_workflows": [{
"id": 1,
"name": "Start",
"action_value_entered": "Start"
}, {
"id": 2,
"name": "Date",
"action_value_entered": "2017-09-11"
}, {
"id": 3,
"name": "Crew",
"action_value_entered": "Crew 3"
},
**COMMENT** - 17 other unique fields - omitted for brevity
]
}
}
For my requirements, I need to create a new column for each of my JSON job_workflows to display the data in Tableau. I.e.
Column 1 Header = Start
Column 1 value = Start
Column 2 Header = Date Complete
Column 2 value = 2017-09-11
Etc.
My Tableau JavaScript file looks as below:
(function () {
var myConnector = tableau.makeConnector();
myConnector.init = function(initCallback) {
initCallback();
tableau.submit();
};
myConnector.getSchema = function (schemaCallback) {
var cols = [
{ id : "start", alias : "Start", dataType: tableau.dataTypeEnum.string },
{ id : "date", alias : "Date", dataType: tableau.dataTypeEnum.datetime },
{ id : "crew", alias : "Crew", dataType: tableau.dataTypeEnum.string }
];
var tableInfo = {
id : "myData",
alias : "My Data",
columns : cols
};
schemaCallback([tableInfo]);
};
myConnector.getData = function (table, doneCallback) {
$.getJSON("http://some/api/call/data.php", function(response) {
var resp = response; // Response is JSON as described above
var tableData = [];
// Iterate over the JSON object
for (var i = 0, len = feat.length; i < len; i++) {
// not working
tableData.push({
"start": resp.job.job_workflows[i].action_value_entered,
"date": resp.job.job_workflows[i].action_value_entered,
"crew": resp.job.job_workflows[i].action_value_entered
});
}
table.appendRows(tableData);
doneCallback();
});
};
tableau.registerConnector(myConnector);
})();
How do I iterate over the JSON job_workflow and assign each action_value_entered to be a id in my getSchema() function? My current version simply hangs and no values are returned. NO error or warnings thrown.

Working with JSON Javascript

I have a set of JSON object in a text file called text.json which looks like the following:
{
"school": [
{
"student": {
"name": "John",
"lastname": "Ghram",
"studentId": "000111"
}
},
{
"student": {
"name": "Harry",
"lastname": "Smith",
"studentId": "000112"
}
},
{
"teacher": {
"name": "Teacher One",
"teacherId": 1001
}
}
]
}
The following code is use to read from file
var obj = (function () {
var json = null;
$.ajax({
'async': false,
'global': true,
'url': "text.json",
'dataType': "json",
'Content-type': 'application/json',
'success': function (data) {
obj = data;
}
});
return obj;
})();
When the obj is returned to get the name of student name I am using
obj.school[0].student['name'].
is there a way to store all student information as one JSON object called students and the teacher to other called teachers, so I can access the info without using the index number. For Eg: student.name.
No, this is exactly what arrays are for. You cannot store multiple instances of the name property on one object, that makes no sense. How would student.name possibly resolve to a single name, if student represented multiple students?
I've formatted your json and then applied a for loop to find object keys that match 'student' and then augmented the students object with the name.
If two names are the same, they'll be overwritten, so this isn't the best approach:
var obj = {
"school": [
{"student": { "name":"John", "lastname": "Ghram", "studentId": 000111 }},
{"student": { "name": "Harry","lastname": "Smith", "studentId": 000112 }},
{"teacher": { "name": "Teacher One", "teacherId": 1001 }}
]
};
var i, arr = obj.school, len = arr.length;
var students = Object.create(null);
for(i=0; i<len; i++) {
if(Object.keys(arr[i])[0] === 'student') students[arr[i].student.name] = arr[i];
}
Can now use square-bracket notation:
students['John'], students['Harry'], etc
You just need to new an array to store them, you can write a function like this
function getStudents(obj)
{
var res = new Array() ;
var school = obj['school'] ;
for(item in school)
{
for(q in school[item])
{
if(q=="student")//you can change it to "teacher"
{
res.push(school[item]) ;
}
}
}
console.log(res) ;
return res ;//return an array(object) of students;
}
getStudents(obj) ;

Loading nested JSON array with knockout js

I am trying to load a form where I can update the existing data from the database.
My Object has one nested array in inside each element in that array also have a nested array.
It looks like the following
{
"Id": 13,
"Name": "Category 1",
"DeliveryOptions":
[
{
"Id": 15,
"DeliveryCategoryId": 13,
"Name": "Option 1.-1",
"DeliveryBreakPoints":
[
{
"Id": 19,
"DeliveryOptionId": 15,
"Start": 0,
"End": 10,
"Flat": 1,
"Variable": 2,
"Delete": false
}
]
},
{
"Id": 16,
"DeliveryCategoryId": 13,
"Name": "Option1-2",
"DeliveryBreakPoints":
[
{
"Id": 20,
"DeliveryOptionId": 16,
"Start": 0,
"End": null,
"Flat": 1,
"Variable": 3,
"Delete": false
}
]
}
]
}
You can see inside the object there's an array called "DeliveryOptions" and each "DeliveryOptions" array has another array called "DeliveryBreakPoints"
I have configured knockout js model as the following
function DeliveryCategory(id, name, options) {
this.Id = id;
this.Name = name;
this.DeliveryOptions = ko.observableArray(options);
}
function DeliveryOption(id, name, breakpoint) {
bpIdCounter = 0;
this.Id = id;
this.Name = name;
this.DeliveryBreakPoints = ko.observableArray(breakpoint);
this.addDeliveryBreakPoint = function (option) {
option.DeliveryBreakPoints.push(new DeliveryBreakPoint(bpIdCounter++));
}
}
function DeliveryBreakPoint(id, start, end, flat, variable) {
var self = this;
self.Id = id;
self.Start = start;
self.End = end;
self.Flat = flat;
self.Variable = variable;
}
I am passing parameters (such as id and name ...) as this code is also used to create new object.. However I am not sure if this is preventing it to get the existing model binded.
I also created a view model which looks like this
function DeliveryCategoryViewModel(model) {
var self = this;
if (model.Id > 0) {
self.DeliveryCategory = ko.observable(model);
} else {
self.DeliveryCategory = ko.observable(new DeliveryCategory(null, "", [new DeliveryOption(null, "")]));
}
self.addDeliveryOption = function () {
self.DeliveryCategory().DeliveryOptions.push(new DeliveryOption(null, "", [new DeliveryBreakPoint(null,"0","","","","")]));
}
self.removeDeliveryOption = function (option) {
self.DeliveryCategory().DeliveryOptions.remove(option);
}
self.removeDeliveryBreakPoint = function (option, breakPoint) {
option.DeliveryBreakPoints.remove(breakPoint);
}
self.onSubmit = function () {
$.ajax({
url: "CreateDeliveryCategory",
type: "POST",
data: ko.toJSON(self),
async: true,
contentType: "application/json"
}).success(function (data) {
window.location = data;
}).error(function(error){
alert(error);
})
}
}
This code is also used when I create a new delivery category from the scratch which works fine(this is why there is a check for "model.Id > 0"). It doesn't bind the model if model.id > 0. It only displays Delivery Category name and the first Delivery Option name correctly(even there are two delivery options) and the rest is just broken.
Can anyone please pinpoint what is going on?

Categories