How to interupt Array.push with Proxy? - javascript

I want to add a side effect every when an array being pushed. For example, I want to add console.log:
var arr = [];
arr.push(1); // => I want it to work normally, and in addition, it logs 1 to the console
How to achieve that? I'm looking for a solution using Proxy and I have tried handler.get() and handler.apply() but still, can't figure it out.

To directly answer your initial question...you need to return a closure from the get trap. To actually trap this, you would need to use proxy.push() instead of array.push(), though. For example:
const arr = [];
const arrProxy = new Proxy(arr, {
get(target, prop) {
if (prop === 'push') {
return (...args) => {
console.log(...args);
return target[prop](...args);
};
}
return target[prop];
}
});
arrProxy.push('test');
arrProxy.push('test1', 'test2');

Here's the final answer that I'm comfortable with, it doesn't use Proxy by the way.
{
var arr = [];
// add push
arr.push = function (...items) {
console.log(...items);
Array.prototype.push.apply(this, items);
};
arr.push('test');
arr.push('test1');
// clean up the push
delete arr.push;
}

something like that ?
Object.defineProperty(Array.prototype, 'myPush',
{
value : function (...val)
{
console.log(...val)
return this.push(...val)
}
})
let aa = []
aa.myPush( 5,123)
console.log('aa = ', aa )

Related

Loop through arguments passed to a method

I am trying to loop through an argument that is passed to a method and I am getting a TypeError: individualExpenses.map is not a function. What am I doing wrong here?
class ExpenseTracker {
constructor(payCheck, monthlyExpenses) {
this.payCheck = payCheck;
this.monthlyExpenses = monthlyExpenses;
}
storeExpenses(individualExpenses) {
let expenseStore = [];
individualExpenses.map(expense => {
expenseStore.push(expense)
})
console.log(expenseStore)
}
}
const v = new ExpenseTracker({}, {});
v.storeExpenses(1)
You are passing a numerical value to storeExpenses function and applying map over it. map works only on arrays. If you do
v.storeExpenses([1]);
it'll work just fine.
Alternatively, you can build logic to convert a non-array type to an array and use it in your storeExpenses function. This way you can do either of v.storeExpenses(1) or v.storeExpenses([1]) and the function will still work.
e.g.
const wrapToArray = (obj) => {
if (!obj) return [];
return Array.isArray(obj) ? obj : [obj];
};
and then modify your storeExpenses method as below -
storeExpenses(individualExpenses) {
let expenseStore = [];
wrapToArray(individualExpenses).map(expense => {
expenseStore.push(expense)
})
console.log(expenseStore)
}

Turn a forEach function into a reduce function

I have been asked to refactor the following code:
const temp = {};
this.sessionData = [];
sessionsData.forEach(session => {
const date = moment(session.startDatetime).format('DDMMYYYY');
if (temp[date]) {
temp[date].push(session);
} else {
temp[date] = [session];
}
});
Apparently it can be more efficient using reduce?
I have tried to simply place reduce in the function but this isnt good enough ;)
const temp = {};
this.sessionData = [];
sessionsData.reduce(session => {
const date = moment(session.startDatetime).format('DDMMYYYY');
if (temp[date]) {
temp[date].push(session);
} else {
temp[date] = [session];
}
});
I understand reduce can add the elements together and have other fun stuff happen during the process BUT specifically I have been asked to sort of get rid of my variables and use them within reduce I suppose!
Any help would be greatly appreciated! Thanks!
If you want to refactor this using reduce, you have to set an inital value {} for the accumulator, and then you have to modify this accumulator, this is what reduce will return after the iterations on your data. You then have to assign this returned accumulator to your temp variable.
So the only input you give to reduce is sessionData, on which reduce is applied. Then at each iteration, it gives you the session and you modify accum.
Also, make sure to return this accum at the end of each iteration, since it has to be passed to the next iteration.
Here is the MDN doc on reduce.
const temp = sessionData.reduce((accum, session) => {
const date = moment(session.startDatetime).format('DDMMYYYY');
if (accum[date]) {
accum[date].push(session);
} else {
accum[date] = [session];
}
return accum;
}, {});
Here you go
let dates = [{startDatetime: '2016-01-02'}, {startDatetime: '2016-02-02'}, {startDatetime: '2016-03-02'}];
const temp = dates.reduce(function(acc, cur) {
const date = moment(cur.startDatetime).format('DDMMYYYY');
if(acc[date]) acc[date].push(cur)
else acc[date] = [cur]
return acc;
}, {})
console.log(temp)
You could use in es6
var sessionData = [
{foo:'bar', startDatetime: Date.now()},
{xxx:'yyy', startDatetime: Date.now()}
];
console.log(
sessionData.reduce((obj,next) => {
const key = moment(sessionData.startDatetime).format('DDMMYYYY');
if(!obj[key]) obj[key] = [];
obj[key].push(next);
return obj;
},{})
);
<script src="https://cdnjs.cloudflare.com/ajax/libs/moment.js/2.18.1/moment.min.js"></script>

Shortest way to set value to variable in Knockout

In Knockout I have observable variable location. It is of type LocationEdit. This viewModel has observable and not fields.
I have collection of field names : fields. For each field I want to reset values for location
fields.forEach(field => {
if (this.uniqueField(locs, field)) {
if (ko.isObservable(this.location()[field])) {
this.location()[field](locs[0][field]);
} else {
this.location()[field] = locs[0][field];
}
}
});
To make this code more simpler (remove if-clauses), Can I somehow set value to this.location()[field] in one line?
You could use the conditional operator (... ? ... : ... ;) although it doesn't change much:
fields.forEach(field => {
if (this.uniqueField(locs, field)) {
ko.isObservable(this.location()[field]) ? this.location()[field](locs[0][field]) : this.location()[field] = locs[0][field];
}
});
Or you could write a function:
function upd(arr, index, val) {
ko.isObservable(arr[index]) ? arr[index](val) : arr[index] = val;
}
Usage:
fields.forEach(field => {
if (this.uniqueField(locs, field)) {
upd(this.location(), field, locs[0][field]);
}
});
See demo.
You could even add this function to ko:
if(typeof ko.updatePotentialObservable == 'undefined')
ko.updatePotentialObservable = function (arr[index], val) {
ko.isObservable(obj) ? arr[index](val) : arr[index]= val;
}
Usage:
fields.forEach(field => {
if (this.uniqueField(locs, field)) {
ko.updatePotentialObservable(this.location(), field, locs[0][field]);
}
});
See other demo
To be honest, I think Gôtô's answers are definitely your best options. Basically, you'd want to create a utility function similar to ko.unwrap but setting a value.
But since you said "also want to find another solution", here's a different utility function. I think the most confusing part of your code is the returning calls to locs[0][field] and this.location()[field]. I'd want something with this signature:
reset(source, target, keys);
So, in your code, you could do:
reset(
this.location(),
locs[0],
fields.filter(f => this.uniqueField(locs, f))
);
Now, writing this method, I ended up with this:
const mergePropsObs = (function() {
// Return a method for setting a specific property in object
const getSetter = obj => prop => ko.isObservable(obj[prop])
? obj[prop]
: val => obj[prop] = val;
// Return unique keys for two objects
// (I went with a quick oneliner; there are many ways to do this)
const allKeys = (obj1, obj2) =>
Object.keys(Object.assign({}, obj1, obj2));
return (base, ext, onlyProps) => {
const props = onlyProps || allKeys(base, ext);
const values = props.map(p => ko.unwrap(ext[p]));
props
.map(getSetter(base))
.forEach((setter, i) => setter(values[i]));
};
}());
var base = { a: 1, b: ko.observable(2), c: 5 };
mergePropsObs(
base,
{ a: 2, b: 3 },
["a", "b"]);
console.log(base.a);
console.log(base.b());
console.log(base.c);
<script src="https://cdnjs.cloudflare.com/ajax/libs/knockout/3.2.0/knockout-min.js"></script>
This utility method can be used with the signature mentioned above. It also has a fallback for when you don't provide an array of field names.

Transform all keys in array from underscore to camel case in js

So, I need to transform all keys in array from underscore to camel space in js. That is what I need to do before send form to server. I'm using Angular.js and I want to represent it as a filter (but I think it's not rly important in this case). Anyway, here is a function I've created.
.filter('underscoreToCamelKeys', function () {
return function (data) {
var tmp = [];
function keyReverse(array) {
angular.forEach(array, function (value, key) {
tmp[value] = underscoreToCamelcase(key);
});
return tmp;
}
var new_obj = {};
for (var prop in keyReverse(data)) {
if(tmp.hasOwnProperty(prop)) {
new_obj[tmp[prop]] = prop;
}
}
return new_obj;
};
function underscoreToCamelcase (string) {
return string.replace(/(\_\w)/g, function(m){
return m[1].toUpperCase();
});
}
})
Here I will try to explain how it works, because it looks terrible at first.
underscoreToCamelcase function just reverting any string in underscore to came case, except first character (like this some_string => someString)
So, as I said earlier, I should revert all keys to camel case, but as you understant we can't simply write
date[key] = underscoreToCamelcase(key)
so keyReverse function returns a reverted array, here is example
some_key => value
will be
value => someKey
and for the last I simply reverting keys and values back, to get this
someKey => value
But, as you already may understand, I got a problem, if in array exists the same values, those data will be dissapear
array
some_key1 => value,
some_key2 => value
returns as
someKey2 => value
So how can I fix that? I have a suggestion to check if those value exists and if it is add some special substring, like this
some_key1 => value,
some_key2 => value
value => someKey1,
zx99value => someKey2
and after all parse it for zx99, but it I think I`m going crazy...
Maybe some one have a better solution in this case?
Important! Them main problem is not just to transform some string to camel case, but do it with array keys!
If you use an existing library to do the camelCase transform, you can then reduce an object like so
import {camelCase} from 'lodash/string'
const camelCaseKeys = (obj) =>
Object.keys(obj).reduce((ccObj, field) => ({
...ccObj,
[camelCase(field)]: obj[field]
}), {})
.filter('underscoreToCamelKeys', function () {
return function (data) {
var tmp = {};
angular.forEach(data, function (value, key) {
var tmpvalue = underscoreToCamelcase(key);
tmp[tmpvalue] = value;
});
return tmp;
};
function underscoreToCamelcase (string) {
return string.replace(/(\_\w)/g, function(m){
return m[1].toUpperCase();
});
}
})
thanks to ryanlutgen
As an alternative solution, you could use the optional replacer parameter of the JSON.stringify method.
var result = JSON.stringify(myVal, function (key, value) {
if (value && typeof value === 'object') {
var replacement = {};
for (var k in value) {
if (Object.hasOwnProperty.call(value, k)) {
replacement[underscoreToCamelcase(k)] = value[k];
}
}
return replacement;
}
return value;
});
Of course you'll end up with a string and have to call JSON.parse to get the object.

How can I extend Array.prototype.push()?

I'm trying to extend the Array.push method so that using push will trigger a callback method and then perform the normal array function.
I'm not quite sure how to do this, but here's some code I've been playing with unsuccessfully.
arr = [];
arr.push = function(data){
//callback method goes here
this = Array.push(data);
return this.length;
}
arr.push('test');
Since push allows more than one element to be pushed, I use the arguments variable below to let the real push method have all arguments.
This solution only affects the arr variable:
arr.push = function () {
//Do what you want here...
return Array.prototype.push.apply(this, arguments);
}
This solution affects all arrays. I do not recommend that you do that.
Array.prototype.push = (function() {
var original = Array.prototype.push;
return function() {
//Do what you want here.
return original.apply(this, arguments);
};
})();
First you need subclass Array:
ES6 (https://kangax.github.io/compat-table/es6/):
class SortedArray extends Array {
constructor(...args) {
super(...args);
}
push() {
return super.push(arguments);
}
}
ES5 (proto is almost deprecated, but it is the only solution for now):
function SortedArray() {
var arr = [];
arr.push.apply(arr, arguments);
arr.__proto__ = SortedArray.prototype;
return arr;
}
SortedArray.prototype = Object.create(Array.prototype);
SortedArray.prototype.push = function() {
this.arr.push(arguments);
};
Array.prototype.push was introduced in JavaScript 1.2. It is really as simple as this:
Array.prototype.push = function() {
for( var i = 0, l = arguments.length; i < l; i++ ) this[this.length] = arguments[i];
return this.length;
};
You could always add something in the front of that.
You could do it this way:
arr = []
arr.push = function(data) {
alert(data); //callback
return Array.prototype.push.call(this, data);
}
If you're in a situation without call, you could also go for this solution:
arr.push = function(data) {
alert(data); //callback
//While unlikely, someone may be using "psh" to store something important
//So we save it.
var saved = this.psh;
this.psh = Array.prototype.push;
var ret = this.psh(data);
this.psh = saved;
return ret;
}
While I'm telling you how to do it, you might be better served with using a different method that performs the callback and then just calls push on the array rather than overriding push. You may end up with some unexpected side effects. For instance, push appears to be varadic (takes a variable number of arguments, like printf), and using the above would break that.
You'd need to do mess with _Arguments() and _ArgumentsLength() to properly override this function. I highly suggest against this route.
Or you could use "arguments", and that'd work too. I still advise against taking this route though.
There's another, more native method to achieve this: Proxy
const target = [];
const handler = {
set: function(array, index, value) {
// Call callback function here
// The default behavior to store the value
array[index] = value;
// Indicate success
return true;
}
};
const proxyArray = new Proxy(target, handler);
I wanted to call a function after the object has been pushed to the array, so I did the following:
myArray.push = function() {
Array.prototype.push.apply(this, arguments);
myFunction();
return myArray.length;
};
function myFunction() {
for (var i = 0; i < myArray.length; i++) {
//doSomething;
}
}

Categories

Resources