I am a newbie in JavaScript and AngularJS. I was trying out looping a object, get its key-value pair and then use it to build an array of new objects.
var actorMovie = {
"Leonardo DiCaprio" : "The Revenant",
"Christian Bale" : "The Dark Knight Rises",
"Sylvester Stallone" : "Rocky"
};
if(actorMovie){
var actorMovieArray = [];
angular.forEach(actorMovie, function(value, key) {
actorMovieArray.push ({key: {
"Movies": {
"Best Movie": value
}
}});
});
}
console.log(actorMovieArray);
This console log prints out the right values, but the key remains as 'key' and never updated to the actor's name as expected.
What am I doing wrong here? I tried searching for an answer but did not find any solution. Am I missing something?
I would do something like
angular.forEach(actorMovie, function(value, key) {
actorMovieArray[key]= {
"Movies": {
"Best Movie": value
}
};
});
In your code, javascript does not know that you want to evaluate the key variable to assign the property, and considers the key to be the key string.
As #Hugues pointed out, there is no way for JavaScript to know if you mean the key as literal or as variable, so the literal value is used.
Please be aware that the answer does not behave the same way as you wanted to in your question. Using the key as an array identifier has two drawbacks:
the order of the items when iterating over the keys cannot be retained
if there are two items having the same key (here the actor name), you will only get one in the result as you are overwriting some previously added value. (this is not really the case as your input already is an object literal so that duplicate keys are no concern, but could be a problem when switching to some other input, e.g. an array of items)
This could be okay for you as long as order doesn't matter and you know your keys are unique. If you want the structure as defined in your question, please consider the following snippet:
function buildItem(value, key) {
var res = {};
res[key] = {
"Movies": {
"Best Movie": value
}
};
return res;
}
if(actorMovie){
var actorMovieArray = [];
angular.forEach(actorMovie, function(value, key) {
actorMovieArray.push(buildItem(value, key));
});
}
Try out this jsbin: http://jsbin.com/luborejini/edit?js,console
I would use Object.keys and Array.forEach on the resulting array. And I would also embrace Javascript's functional nature. That way you could easily pull out the creator function into factory mapping libraries for your api json data.
if(actorMovie){
var actorMovieArray = [];
Object.keys(actorMovie).forEach(function(actor){
actorMovieArray[actor] = function(){
return {
Movies: {
BestMovie: actorMovie[actor]
}
};
}();
});
}
I would also recommend not using the actor name as the key in the array. I would rather map it to a model structure, it will make your views / controllers cleaner and easier to understand:
if(actorMovie){
var actorMovieArray = [];
Object.keys(actorMovie).forEach(function(actor) {
actorMovieArray.push(function(){
return {
Actor: actor,
Movies: {
BestMovie: actorMovie[actor]
}
};
}());
});
}
This will drive you into more concise view models, and set you up for easy refactoring once your structure is in place. It will also make testing easier, at least in my opinion.
Related
The problem isn't the code, it's that I don't understand why what I have works, although it does what I need it to do. I'm building an app that keeps track of jobs. The jobs, each an object, are stored in an array in a JSON file. I'm adding the functionality to edit a job's key/value pairs in the JSON file.
Anyway, my function editJob takes in an object as an argument that has an id and a variable amount of other properties. The goal is then to locate the job in JSON that matches the id, then update that job's properties based only on the editItems object.The code below allows for that. I just don't understand the line below the Object.keys code. Why would I not compare the located job's keys to the editItems keys?
I don't know why it works and am worried it will break at some point because it's not properly coded.
function editJob (editItems) {
// editItems is an object like this: ex. { id: 3, customer: 'Artemis', source: 'Google', description: 'Fixed toilet' }
return this.jobs.map(job => {
let editedJobs = Object.assign({}, job);
if (editedJobs.id === editItems.id) {
Object.keys(editItems).forEach(k => {
if (editedJobs[k] === job[k]) { // WHY DOES THIS WORK. why job[k] and not editItems[k]???
editedJobs[k] = editItems[k];
}
});
}
return editedJobs;
});
}
Since you just did editedJobs = Object.assign({}, job), the expression editedJobs[k] === job[k] will be true for every k. You can just omit it. You would achieve the same thing by
function editJob (editItems) {
return this.jobs.map(job => {
return job.id === editItems.id
? Object.assign({}, job, editItems)
: job;
});
}
If I have object with following structure:
var test = {
property1: "value1",
property2: "value2",
property3: "value3",
property4: "value4",
property5: "value5"
}
Assuming that property names are fixed and not always in this order, what is the most elegant way to convert this object into following one:
var test_copy = {
prop1Copy: "value1",
propConcat: "value2, value3, value4, value5"
}
I don't think there's any particularly elegant way to do this.
Since your input data has a small number fixed keys there's barely any point using a loop, so this works:
function munge(o) {
return {
prop1Copy: o.property1,
propConcat: [o.property2, o.property3, o.property4, o.property5].join(', ')
}
}
Try this:
function concatObject(object, levels){
var currentLevel = 0;
var newObj = {propConcat: ""};
for(var prop in object){
if(currentLevel < levels){
newObj[prop] = object[prop];
}
else{
newObj["propConcat"] += object[prop];
}
}
}
concatObject(test, 1) would give you the answer, however it would keep the same property name for the variables. You need some kind of function of mapping if you want to change the actual property names (example: from property1 to prop1copy)
This would transform property# to property#copy:
function concatObject(object, levels){
var currentLevel = 0;
var newObj = {propConcat: ""};
for(var prop in object){
if(currentLevel < levels){
newObj[prop+"copy"] = object[prop];
}
else{
newObj["propConcat"] += object[prop];
}
}
}
Im not sure what you need to accomplish here. But if you want copy first item and concat all other take a look at this.
function concatValues (obj) {
var resObj = {prop1Copy: ""}, count = 0, mergedArr = [];
for (var k in obj) {
count == 0 ? resObj.prop1Copy = obj[k] : mergedArr.push(obj[k]);
count++;
}
resObj.propConcat = mergedArr.join(", ");
return resObj;
}
Hope this helps
Here is a more generic solution that would work on a wider range of input with some caveats.
function concatenateObjectValues(obj) {
//If you want the output to be sorted differently, you need to provide your own sort order. This sorts by alphabetical order
var keys = Object.keys(test).sort();
//assuming the first property would always be the copy
//removing the first element and returning it
var copyProp = keys.unshift();
//generate an array that has the values of the remaining properties from the input
var concatProp = keys.reduce(function(memo, key) {
memo.push(test[key]);
return memo;
}, []);
//create `propConcat` and combine the values using the specified separator
var newObj = {
propConcat: concatProp.join(", ")
};
//add the `prop1Copy` property. The first part of the name would be derived from the actual first property .
newObj[copyProp + "Copy"] = obj[copyProp];
return newObj;
}
Assuming you want your concatenated properties in alphabetical order,
the above would work. If not, then you would need to specify a
different sort order. This can be passed in as an argument, if it's going to vary).
if the copy property is going to vary, then this code might also need to change. Also, something that can be passed in as a parameter - trivial if it's just the index, but if you have to look them up by name (e.g., if you want to say "prop1" regardless of where it is., you need to also implement that).
if the names propConcat and prop1Copy need to vary more than that, the logic needs to be implemented. Or the values passed in...
there is no validation. I kept it simple for the sake of the example, but some error handling would be good.
To be honest, if your expected output is going to vary by more than one thing, for example, if you need the copy property to be different and the sort order to be different, then it might just be better to scrap this function. Big variations in the expected input/output make it a bit unwieldy, if you need to pass in most of the stuff to construct the result.
I am learning functional programming in Javascript and using Ramda. I have this object
var fieldvalues = { name: "hello there", mobile: "1234",
meta: {status: "new"},
comments: [ {user: "john", comment: "hi"},
{user:"ram", comment: "hello"}]
};
to be converted like this:
{
comments.0.comment: "hi",
comments.0.user: "john",
comments.1.comment: "hello",
comments.1.user: "ram",
meta.status: "new",
mobile: "1234",
name: "hello there"
}
I have tried this Ramda source, which works.
var _toDotted = function(acc, obj) {
var key = obj[0], val = obj[1];
if(typeof(val) != "object") { // Matching name, mobile etc
acc[key] = val;
return acc;
}
if(!Array.isArray(val)) { // Matching meta
for(var k in val)
acc[key + "." + k] = val[k];
return acc;
}
// Matching comments
for(var idx in val) {
for(var k2 in val[idx]) {
acc[key + "." + idx + "." + k2] = val[idx][k2];
}
}
return acc;
};
// var toDotted = R.pipe(R.toPairs, R.reduce(_toDotted, {}));
var toDotted = R.pipe(R.toPairs, R.curry( function(obj) {
return R.reduce(_toDotted, {}, obj);
}));
console.log(toDotted(fieldvalues));
However, I am not sure if this is close to Functional programming methods. It just seems to be wrapped around some functional code.
Any ideas or pointers, where I can make this more functional way of writing this code.
The code snippet available here.
UPDATE 1
Updated the code to solve a problem, where the old data was getting tagged along.
Thanks
A functional approach would
use recursion to deal with arbitrarily shaped data
use multiple tiny functions as building blocks
use pattern matching on the data to choose the computation on a case-by-case basis
Whether you pass through a mutable object as an accumulator (for performance) or copy properties around (for purity) doesn't really matter, as long as the end result (on your public API) is immutable. Actually there's a nice third way that you already used: association lists (key-value pairs), which will simplify dealing with the object structure in Ramda.
const primitive = (keys, val) => [R.pair(keys.join("."), val)];
const array = (keys, arr) => R.addIndex(R.chain)((v, i) => dot(R.append(keys, i), v), arr);
const object = (keys, obj) => R.chain(([v, k]) => dot(R.append(keys, k), v), R.toPairs(obj));
const dot = (keys, val) =>
(Object(val) !== val
? primitive
: Array.isArray(val)
? array
: object
)(keys, val);
const toDotted = x => R.fromPairs(dot([], x))
Alternatively to concatenating the keys and passing them as arguments, you can also map R.prepend(key) over the result of each dot call.
Your solution is hard-coded to have inherent knowledge of the data structure (the nested for loops). A better solution would know nothing about the input data and still give you the expected result.
Either way, this is a pretty weird problem, but I was particularly bored so I figured I'd give it a shot. I mostly find this a completely pointless exercise because I cannot picture a scenario where the expected output could ever be better than the input.
This isn't a Rambda solution because there's no reason for it to be. You should understand the solution as a simple recursive procedure. If you can understand it, converting it to a sugary Rambda solution is trivial.
// determine if input is object
const isObject = x=> Object(x) === x
// flatten object
const oflatten = (data) => {
let loop = (namespace, acc, data) => {
if (Array.isArray(data))
data.forEach((v,k)=>
loop(namespace.concat([k]), acc, v))
else if (isObject(data))
Object.keys(data).forEach(k=>
loop(namespace.concat([k]), acc, data[k]))
else
Object.assign(acc, {[namespace.join('.')]: data})
return acc
}
return loop([], {}, data)
}
// example data
var fieldvalues = {
name: "hello there",
mobile: "1234",
meta: {status: "new"},
comments: [
{user: "john", comment: "hi"},
{user: "ram", comment: "hello"}
]
}
// show me the money ...
console.log(oflatten(fieldvalues))
Total function
oflatten is reasonably robust and will work on any input. Even when the input is an array, a primitive value, or undefined. You can be certain you will always get an object as output.
// array input example
console.log(oflatten(['a', 'b', 'c']))
// {
// "0": "a",
// "1": "b",
// "2": "c"
// }
// primitive value example
console.log(oflatten(5))
// {
// "": 5
// }
// undefined example
console.log(oflatten())
// {
// "": undefined
// }
How it works …
It takes an input of any kind, then …
It starts the loop with two state variables: namespace and acc . acc is your return value and is always initialized with an empty object {}. And namespace keeps track of the nesting keys and is always initialized with an empty array, []
notice I don't use a String to namespace the key because a root namespace of '' prepended to any key will always be .somekey. That is not the case when you use a root namespace of [].
Using the same example, [].concat(['somekey']).join('.') will give you the proper key, 'somekey'.
Similarly, ['meta'].concat(['status']).join('.') will give you 'meta.status'. See? Using an array for the key computation will make this a lot easier.
The loop has a third parameter, data, the current value we are processing. The first loop iteration will always be the original input
We do a simple case analysis on data's type. This is necessary because JavaScript doesn't have pattern matching. Just because were using a if/else doesn't mean it's not functional paradigm.
If data is an Array, we want to iterate through the array, and recursively call loop on each of the child values. We pass along the value's key as namespace.concat([k]) which will become the new namespace for the nested call. Notice, that nothing gets assigned to acc at this point. We only want to assign to acc when we have reached a value and until then, we're just building up the namespace.
If the data is an Object, we iterate through it just like we did with an Array. There's a separate case analysis for this because the looping syntax for objects is slightly different than arrays. Otherwise, it's doing the exact same thing.
If the data is neither an Array or an Object, we've reached a value. At this point we can assign the data value to the acc using the built up namespace as the key. Because we're done building the namespace for this key, all we have to do compute the final key is namespace.join('.') and everything works out.
The resulting object will always have as many pairs as values that were found in the original object.
I'm new JavaScript and trying to find out an easier way to find name given a value from object literal.
e.g.
var cars ={ Toyata: ['Camry','Prius','Highlander'],
Honda: ['Accord', 'Civic', 'Pilot'],
Nissan: ['Altima', 'Sentra', 'Quest']};
Given 'Accord', I want to get Honda from the object Cars.
You would need to loop through, like this:
function getManufacturer(carName) {
for(var key in cars) {
if(cars.hasOwnProperty(key)) {
for(var i=0; i<cars[key].length; i++) {
if(cars[key][i] == carName) return key;
}
}
}
return "Not found";
}
You can test it out here, for the same of working cross-browser, this ignores the existence of .indexOf() since IE doesn't have it...that version would look like this:
function getManufacturer(carName) {
for(var key in cars) {
if(cars.hasOwnProperty(key) && cars[key].indexOf(carName) != -1) {
return key;
}
}
return "Not found";
}
If you're going to be doing this once, then use a function like the one given by Bobby. If you're going to be doing this multiple times then I'd suggest creating a reverse mapping of cars to manufacturers:
var manufacturers = {};
// create a map of car models to manufacturers:
for (var manf in cars) {
/* see note below */
for (var i=0; i<cars[manf].length; i++) {
manufacturers[cars[manf][i]] = manf;
}
}
// Now referencing the manufacturers is
// a very fast hash table lookup away:
var model = 'Accord';
alert(manufacturers[model]);
note for those with itchy downvoting fingers: For objects that don't inherit anything as given in the OP a hasOwnProperty check here is unnecessary. For objects that do inherit it depends on the programmer. If you want composability via inheritance then a hasOwnProperty check is exactly what you DONT want. If you don't care about inheritance then use a hasOwnProperty check but if so you would not be inheriting in the first place which would make a hasOwnProperty check unnecessary. In the rare case where you are forced to create the object via inheritance but don't want to check the parent's attributes then you should do a hasOwnProperty check. Of course, if you use a library like Prototype.js that insists on modifying the Object object then I feel sorry for you because you are forced to do a hasOwnProperty check.
Maintain a separate mapping of models to manufacturers.
var cars ={ Toyata: ['Camry','Prius','Highlander'],
Honda: ['Accord', 'Civic', 'Pilot'],
Nissan: ['Altima', 'Sentra', 'Quest']};
var models = {};
var hasOwnProperty = Object.prototype.hasOwnProperty;
for (key in cars) {
if (hasOwnProperty.call(cars, key)) {
var i=0,l=cars[key].length,manufacturer=cars[key];
while (i<l) {
if ( ! hasOwnProperty.call(models, manufacturer)) {
models[manufacturer] = key;
} else {
// Throw an error, or change the value to an array of values
}
i++;
}
}
}
arr[key] = value;
where key is a jQuery object and value is an array.
Associative arrays don't really exist in JavaScript. However, you can achieve similar functionality using JavaScript objects:
// Create object
var myObject = {
key: value,
helloText: "Hello World!"
};
// Access object in some statement via:
myObject.helloText
// ...or:
myObject["helloText"]
To use an object as a key, you would have to do something like:
var a = {
helloText: "Hello World!"
};
var b = {};
b[a] = "Testing";
alert(b[a]); // Returns "Testing" (at least, in Safari 4.0.4)
Using an object as a key sounds a bit weird, though. Are you sure you need to do this?
Update:
You can't actually use an object as a key in JavaScript. The reason the above code appears to work is that, in the statement b[a] = "Testing";, JavaScript converts a to a string via a.toString(), which results in "[object Object]", and uses this string as the key. So our statement is actually b["[object Object]"] = "Testing"; and our alert statement is exactly the same as alert(b["[object Object]"]);.
Thanks to CMS for pointing this out in the comments!
Update 2:
Tim Down points out that his JavaScript library jshashtable allows you use an object as a key.
You can use jshashtable, which allows any JavaScript object as a key.
Just guessing here, but it seems you're trying to associate some (arbitrary) data with a jQuery object (possibly an element). In that case, why not use the data () method?
$('#el').data (value);
You can't use objects as keys, and assocative arrays are not what they seem in Javascript because all you're doing is setting a property on the array object, when you loop through by the .length it natively doesn't account for the manually defined properties you set.
I suggest storing the elements and arrays inside of object literals, all inside of an array. Eg:
var list = [
{
el:document.body,
arr:[1,2]
}
];
for ( var i = 0, l = list.length; i<l; ++i ) {
alert( list[i]['el'] )
alert( list[i]['arr'][0] )
}
// add elements to the array
list.push({
el:document.body.firstChild,
arr:[3,4]
})
As kprime mentioned in his answer though, it might be better to use .data() if you are using Javascript.
if ( !$(el).data('key') ) {
$(el).data('key', [2,3,4] );
}
I would suggest assigning a unique ID to each element you want to put in the associative container (object in JS) and use the ID as key:
var idCounter = 0;
var container = { };
function storeValue(element, value) {
if (!element.getAttribute('id')) {
element.setAttribute('id', makeID());
}
var id = element.getAttribute('id');
container[id] = value;
}
function makeID() {
return 'unique-id-' + idCounter++;
}
EDIT: This solution assumes that jQuery is not available. If it is, use data('key', value).
every javascript object is an associative array, this is a property build in the language, you do not need to anything special, just use it like that