Unexpected behavior using ko.mapping.fromJS - javascript

I am experimenting with ko.mapping.fromJS
(http://knockoutjs.com/documentation/plugins-mapping.html)
Given the code below, and expectations, could someone explain to me why the actual output differs?
var obj = { name: "frederick", minions: [{id:1, name:'Alice'},{id:2, name:'Bob'}] }
var model = {
'minions': {
key: function(data) { return ko.utils.unwrapObservable(data.id); }
}
}
var vm = ko.mapping.fromJS(obj, model);
vm.name.subscribe(x=>console.log("Changed name"));
vm.minions.subscribe(x => console.log("changed minions"));
vm.minions()[0].id.subscribe(x => console.log("changed id [0]"));
vm.minions()[0].name.subscribe(x => console.log("changed name [0]"));
vm.minions()[1].id.subscribe(x => console.log("changed id [1]"));
vm.minions()[1].name.subscribe(x => console.log("changed name [1]"));
obj.minions[0].name = 'Charlie';
ko.mapping.fromJS(obj, vm);
Expected logging:
changed name [0]
Actual logging:
changed name [0]
changed minions
Question:
Since there are no records added or removed to the array, why do I see "Changed minions"? Are these events always bubbled up, or only in case of arrays and direct child objects (/rows)?
(Or did I make a mistake in the model? Can I 'fix' this?)

What's happening under the hood is this:
var a = { a: 1 };
var b = { b: 2 };
var arr1 = [a, b];
var obsArray = ko.observableArray(arr2);
obsArray.subscribe(_ => console.log("change"));
var arr2 = [a, b];
// Logs change, since
// arr1 !== arr2
// even though:
// arr1[0] === arr2[0] and
// arr1[1] === arr2[1]
obsArray([a, b]);
<script src="https://cdnjs.cloudflare.com/ajax/libs/knockout/3.4.2/knockout-min.js"></script>
I'd say the easiest way to counter this issue is to check for "equality" in your subscription method, but that's mainly because I don't know the mapping plugin very well...
How I'd solve it:
Create a helper function that takes an observableArray and a callback function.
The helper defines an equalityCheck that takes two arrays as input and returns true or false
The helper creates a regular subscription to the observable but only calls the passed callback if the equality check passes.
To compare the new value that is passed to the knockout subscription with the previous one, it needs to store a reference to the previous value.
This means your syntax will change from:
vm.minions.subscribe(x => console.log("changed minions"));
to:
customSubscribe(vm.minions, x => console.log("changed minions"));
There might also be a way to fix this via the plugin or via an extender...
const obsArraySubThatChecksInner = (arr, cb) => {
let prevValues = arr();
// Check if items are equal without checking the array reference
const equalityCheck = (arr1, arr2) => {
return arr1.length === arr2.length &&
arr1.every((item, i) => item === arr2[i]);
}
return arr.subscribe(newValues => {
// Make a copy to prevent mutations in cb
const newArray = newValues.slice();
if (!equalityCheck(prevValues, newValues)) {
cb(newValues);
};
prevValues = newArray;
});
};
var obsArr = ko.observableArray([1, 2, 3]);
var regularSub = obsArr.subscribe(vals => console.log("Regular sub:", JSON.stringify(vals)));
var specialSub = obsArraySubThatChecksInner(
obsArr, vals => console.log("Special sub:", JSON.stringify(vals))
);
// Is "similar", so will only trigger the regular sub.
obsArr([1, 2, 3]);
// Is different, so will trigger both
obsArr([1, 2]);
obsArr.push(3);
<script src="https://cdnjs.cloudflare.com/ajax/libs/knockout/3.4.2/knockout-min.js"></script>
With your data in a fiddle: https://jsfiddle.net/nk074pb3/

Related

Oh man groupBy() and arrayToObject() are breaking me

So I have been working on some extra credit for my classes. I am very new to programming and have already sought help for this same assignment, I started rolling through and now am absolutely lost.
I need to define the two functions groupBy() and arrayToObect() as asked in the below test.
I'm not necessarily looking for the answer but if someone could help point me in the right direction that would be awesome.
What I have deduced is as follows:
I need to be using the spread operator ...
I need to create a newObj = {}
a. and somehow push the element derived from the array into the obj
I need to take the individual values of the array and assign them as keys, with the variances as the properties of the key.
Bracket notation
I have been racking my brain for hours on this now and could really use some guidance!
describe('groupBy', function () {
const input = [4.2, 6.1, 6.3]
const result = groupBy(input, (el) => Math.floor(el))
it('returns an object', function () {
expect(result).to.be.an('object')
})
it('group array items together based on the callback return value', function () {
expect(result).to.be.eql({
4: [4.2],
6: [6.1, 6.3],
})
})
})
describe('arrayToObject', function () {
const input = ['cat', 'dog', 'bird']
const result = arrayToObject(input, (word) => word + 's')
it('returns an object', function () {
expect(result).to.be.an('object')
})
it('object has original array elements as keys and the result of the callback as values', function () {
expect(result).to.be.eql({
cat: 'cats',
dog: 'dogs',
bird: 'birds',
})
})
})
})
groupBy
Write a function called groupBy which takes an array and a callback. The function should return an object. Each return value of the callback should be a key of the object and the values should be the input element with which the callback was called.
arrayToObject
Write a function called arrayToObject which takes an array and a callback. The function should return an object. Each element of the input array should be a key of the returned object and the output from the callback with an element passed in as the corresponding value.
These questions have been answered a million times on stackoverflow. Essentially what you want to be doing here is using the common js array functions map, filter, reduce, flatten, ..., and think about how your problem can be expressed in terms of those.
A lot of real world code is transforming data like this, so it's good to be comfortable doing it.
Also realize that spread syntax copies the entire object which can be pretty inefficient. JavaScript doesn't have persistent data structures! It's usually better to just mutate — as long as your code is what "owns" the object.
const groupBy = (elts, keyfn) =>
elts.reduce((m, elt) => {
const key = keyfn(elt);
m[key] = m[key] || [];
m[key].push(elt);
return m;
}, {});
const arrayToObject = (elts, fn) =>
elts.reduce(
(obj, elt) => Object.assign(obj, { [elt]: fn(elt) }),
{},
);
I figured it out using a for loop!!
function groupBy(arr, callback) {
const newObj = {}
for (let i = 0; i < arr.length; i++) {
if (callback(arr[i])) {
const key = callback(arr[i])
newObj[key] = newObj[key] || []
newObj[key].push(arr[i])
}
}
return newObj
}
function arrayToObject(arr, callback) {
const obj = {}
for (let i = 0; i < arr.length; i++) {
if (callback(arr[i])) {
const key = callback(arr[i])
obj[arr[i]] = obj[key] || callback(arr[i])
}
}
return obj
}

ES6 Filtering objects by unique attribute

I've an array of errors, each error has a non-unique param attribute.
I'd like to filter the array based on whether the param has been seen before.
Something like this:
const filteredErrors = [];
let params = [];
for(let x = 0; x < errors.length; x++) {
if(!params.includes(errors[x].param)) {
params.push(errors[x].param);
filteredErrors.push(errors[x]);
}
}
But I've no idea how to do this in ES6.
I can get the unique params const filteredParams = Array.from(new Set(errors.map(error => error.param)));
but not the objects themselves.
Pretty sure this is just a weakness in my understanding of higher order functions, but I just can't grasp it
You could destrucure param, check against params and add the value to params and return true for getting the object as filtering result.
As result you get an array of first found errors of the same type.
const
params = [],
filteredErrors = errors.filter(({ param }) =>
!params.includes(param) && params.push(param));
Instead of an array you can make use of an object to keep a map of existing values and make use of filter function
let params = {};
const filteredErrors = errors.filter(error => {
if(params[error.param]) return false;
params[error.param] = true;
return true;
});
i'd probably do it like this with a reduce and no need for outside parameters:
const filteredErrors = Object.values(
errors.reduce((acc, val) => {
if (!acc[val.param]) {
acc[val.param] = val;
}
return acc;
}, {}))
basically convert it into an object keyed by the param with the object as values, only setting the key if it hasn't been set before, then back into an array of the values.
generalized like so
function uniqueBy(array, prop) {
return Object.values(
array.reduce((acc, val) => {
if (!acc[val[prop]]) {
acc[val[prop]] = val;
}
return acc;
}, {}))
}
then just do:
const filteredErrors = uniqueBy(errors, 'param');
If your param has a flag identifier if this param has been seen before then you can simply do this.
const filteredErrors = errors.filter(({ param }) => param.seen === true);
OR
const filteredErrors = errors.filter((error) => error.param.seen);
errors should be an array of objects.
where param is one of the fields of the element of array errors and seen is one of the fields of param object.
You can do it by using Array.prototype.reduce. You need to iterate through the objects in the array and keep the found params in a Set if it is not already there.
The Set.prototype.has will let you find that out. If it is not present in the Set you add it both in the Set instance and the final accumulated array, so that in the next iteration if the param is present in your Set you don't include that object:
const errors = [{param: 1, val: "err1"}, {param: 2, val: "err2"}, {param: 3, val: "err3"}, {param: 2, val: "err4"}, {param: 1, val: "err5"}];
const { filteredParams } = errors.reduce((acc, e) => {
!acc.foundParams.has(e.param) && (acc.foundParams.add(e.param) &&
acc.filteredParams.push(e));
return acc;
}, {foundParams: new Set(), filteredParams: []});
console.log(filteredParams);

Return false if one element of array is not empty without use Iterators - Typescript

I have a method where I receive an string array and I want to return false if one of array elements is false.
myMethod(attrs: Array<String>) {
for (const element of attrs) {
if (!element) {
return false;
}
}
return true;
}
Is it possible to simplify this code?
Javascript Array has a method that checks if every item satisfies a predicate: every.
Example:
const arr = [1, 2, null];
const every = arr.every(item => item !== null); // Check if every item is not-null. This will be false
It can be shortened by casting an item to a boolean:
const every2 = arr.every(item => Boolean(item)); // This will give false for 0, null, undefined, etc.
And even shorter by just passing a Boolean constructor to a callback, which will pass items to it's constructor
const every2 = arr.every(Boolean);
Also check methods find and some.
Keep in mind this does use iterator, it's just that JS built-in functions do it under the hood.
Try This:
var attrs =[true,true,false];
var result = !attrs.some( elm => (elm === false) );
console.log(result);

Javascript - map value to keys (reverse object mapping)

I want to reverse the mapping of an object (which might have duplicate values). Example:
const city2country = {
'Amsterdam': 'Netherlands',
'Rotterdam': 'Netherlands',
'Paris': 'France'
};
reverseMapping(city2country) Should output:
{
'Netherlands': ['Amsterdam', 'Rotterdam'],
'France': ['Paris']
}
I've come up with the following, naive solution:
const reverseMapping = (obj) => {
const reversed = {};
Object.keys(obj).forEach((key) => {
reversed[obj[key]] = reversed[obj[key]] || [];
reversed[obj[key]].push(key);
});
return reversed;
};
But I'm pretty sure there is a neater, shorter way, preferably prototyped so I could simply do:
const country2cities = city2country.reverse();
You could use Object.assign, while respecting the given array of the inserted values.
const city2country = { Amsterdam: 'Netherlands', Rotterdam: 'Netherlands', Paris: 'France' };
const reverseMapping = o => Object.keys(o).reduce((r, k) =>
Object.assign(r, { [o[k]]: (r[o[k]] || []).concat(k) }), {})
console.log(reverseMapping(city2country));
There is no such built-in function in JavaScript. Your code looks fine, but given that there are so many edge cases here that could wrong, I'd suggesting using invertBy from lodash, which does exactly what you describe.
Example
var object = { 'a': 1, 'b': 2, 'c': 1 };
_.invertBy(object);
// => { '1': ['a', 'c'], '2': ['b'] }
You can use something like this to get raid of duplicates first :
function removeDuplicates(arr, key) {
if (!(arr instanceof Array) || key && typeof key !== 'string') {
return false;
}
if (key && typeof key === 'string') {
return arr.filter((obj, index, arr) => {
return arr.map(mapObj => mapObj[key]).indexOf(obj[key]) === index;
});
} else {
return arr.filter(function(item, index, arr) {
return arr.indexOf(item) == index;
});
}
}
and then use this to make it reverse :
function reverseMapping(obj){
var ret = {};
for(var key in obj){
ret[obj[key]] = key;
}
return ret;
}
You could try getting an array of values and an array of keys from the current object, and setup a new object to hold the result. Then, as you loop through the array of values -
if the object already has this value as the key, like Netherlands, you create a new array, fetch the already existing value (ex: Rotterdam), and add this and the new value (Amsterdam) to the array, and set up this array as the new value for the Netherlands key.
if the current value doesn't exist in the object, set it up as a new string, ex: France is the key and Paris is the value.
Code -
const city2country = {
'Amsterdam': 'Netherlands',
'Rotterdam': 'Netherlands',
'Paris': 'France',
};
function reverseMapping(obj) {
let values = Object.values(obj);
let keys = Object.keys(obj);
let result = {}
values.forEach((value, index) => {
if(!result.hasOwnProperty(value)) {
// create new entry
result[value] = keys[index];
}
else {
// duplicate property, create array
let temp = [];
// get first value
temp.push(result[value]);
// add second value
temp.push(keys[index]);
// set value
result[value] = temp;
}
});
console.log(result);
return result;
}
reverseMapping(city2country)
The benefit here is - it adjusts to the structure of your current object - Netherlands being the repeated values, gets an array as it's value in the new object, while France gets a string value Paris as it's property. Of course, it should be very easy to change this.
Note - Object.values() might not be supported across older browsers.
You could use reduce to save the declaration line reduce.
Abusing && to check if the map[object[key]] is defined first before using Array.concat.
It's shorter, but is it simpler? Probably not, but a bit of fun ;)
const reverseMapping = (object) =>
Object.keys(object).reduce((map, key) => {
map[object[key]] = map[object[key]] && map[object[key]].concat(key) || [key]
return map;
}, {});
#Nina Scholz answer works well for this exact question. :thumbsup:
But if you don't need to keep both values for the Netherlands key ("Netherlands": ["Amsterdam", "Rotterdam"]), then this is a little bit shorter and simpler to read:
const city2country = { Amsterdam: 'Netherlands', Rotterdam: 'Netherlands', Paris: 'France' };
console.log(
Object.entries(city2country).reduce((obj, item) => (obj[item[1]] = item[0]) && obj, {})
);
// outputs `{Netherlands: "Rotterdam", France: "Paris"}`

Quickest way to check which property inside an object holds value?

Probably a dumb question, Assume i have an object like below,
{"Country":"country","Continent":"continent","Province":"","District":"","State":"state","City":""}"
What is the quickest way to check which properties hold the values in the above object without using a loop?
while doing this inside a for loop is working,
if(typeof someUndefVar == whatever) -- works
EXPECTED OUTPUT:
Country, Continent and State
var a = {"Country":"country","Continent":"continent","Province":"","District":"","State":"state","City":""};
Object.keys(a).filter( prop => a[prop] );
It also depends on how you want to handle the 0, null, undefined values.
You might do as follows;
var obj = {"Country":"country","Continent":"continent","Province":"","District":"","State":"state","City":""};
for (var prop in obj) !!obj[prop] && console.log(prop);
You need to use the object.keys() function,
Like this,
var jsonData= {"Country":"country","Continent":"continent","Province":"","District":"","State":"state","City":""};
console.log(Object.keys(jsonData));
Hope this helps!
Sorry you said no loop and I got excited and I was bored and I got functional.
First I shortened some names and I guess they are kind of self explanatory:
head: first element of an array
tail: all the elements but the first
isEmpty: check if an array has length 0
transform does the actual work using a callback, it stops if the keys array is empty if not goes recursive.
var data = {"Country":"country","Continent":"continent","Province":"","District":"","State":"state","City":""}
// plumbing
var head = (a) => a[0]
var tail = (a) => a.slice(1)
var isEmpty = (a) => a.length === 0
// actual stuff happens here
var transform = (obj, callback) => {
var withval = (keys) => {
if(isEmpty(keys)) return
var p = head(keys)
if(!!obj[p]) callback(p)
withval(tail(keys))
}
withval(Object.keys(obj))
}
// Logs
var log = (p) => console.log(p)
transform(data, log)
// Array making
var arr = [];
var toArr = (p) => arr.push(p)
transform(data, toArr)
console.log(arr)
// Object making
var obj = {}
var toObj = (p) => obj[p] = data[p]
transform(data, toObj)
console.log(obj)
Outcome:
Country
Continent
State
[ 'Country', 'Continent', 'State' ]
{ Country: 'country', Continent: 'continent', State: 'state' }

Categories