How to chain JavaScript methods natively from an iterator? - javascript

Say I'm using js methods that have the 'this' returned, so one can chain like such:
something.add({a: 'xyz', b: 123}).add({a: 'abc', b: 456});
How can I chain these on something from an iterator? For example:
$scope.bindings = [
{
key: 'up',
func: function() {
$scope.scroll(1)
}
},{
key: 'down',
func: function() {
$scope.scroll(-1);
}
},
];
---EDIT---
The library I was using is Angular hotkeys. I wanted to ask in a generic way to help anyone else in a similar position.
I have:
var hotBindings = hotkeys.bindTo(scope);
for (var bind of scope.bindings) {
hotBindings = hotBindings.add({
combo: bind.key,
callback: function(e) {
e.preventDefault();
bind.func();
}
})
}
This assigns the 'down' method to both keypresses. If I write out the code without the loop, using scope.bindings[index].key (for example) and chain the .add() method then it works. I also tried without "hotBindings =".
Please don't mention the scopve vs $scope as this is being passed into a link function in a angular directive - angular almost certainly has nothing to do with it.

The only problem I see is the not working for (var bind of bindings).
Edit: thought it had something to do with the for (var bind of bindings) syntax and var v.s. let, turns out the provided code just works. I'll delete this answer if the real problem surfaces. Must be in the Something class?
Everything seems to work:
var Something = function() {
this.data = [];
this.add = function(item) {
this.data.push(item);
return this;
}.bind(this);
};
var bindings = [{
x: 'hi',
func: function() {
console.log("hi");
}
}, {
x: 'bye',
func: function() {
console.log("bye");
}
}];
var something = new Something();
for (var bind of bindings) {
something.add({
x: bind.x,
callback: bind.func
})
};
console.log(something.data);

I'm not sure what SomeThing is or what it's add method returns, but you can replicate the chaining by doing
let something = new Something();
for (const bind of bindings) {
something = something.add({
x: bind.x,
callback: bind.func
});
}

If you do this a lot (perhaps using different methods than just "add"), you might consider using a helper function:
function chain(obj, ...calls) {
for(let {method, args} of calls) {
obj = obj[method](...args)
}
return obj
}
chain(new Somthing(), [
{meth: 'add', args: [
{x: 'hi', func: () => console.log('hi')}]},
{meth: 'add', args: [
{x: 'bye', func: () => console.log('bye')}]}
]})

Related

Why is the fat arrow in a class method not binding to the parent scope's this?

I've got an ES2015 code snippet where I'm trying to dynamically populate the objects this.data.pageCategoryL1~3 keys with different data depending on the state of the originalData. I'm passing class methods as a callback to _categoryMapper, which isn't binding the callback to class's this - it's only passing a pointer to the function without binding it even though it's a method. That alone seems weird odd that it isn't automatically bound to the instance.
The truly puzzling part though: inside of _categoryMapper's reduce function, the fat arrow function's this is undefined. I thought fat arrows were supposed to bind to their parent this's scope?
class AnalyticsData {
constructor(originalData) {
this.data = {};
this.originalData = originalData;
}
mapCategories() {
debugger;
let mappedCategories = {
pageCategoryL1: '',
pageCategoryL2: '',
pageCategoryL3: ''
};
if (this.originalData.search && this.originalData.search.refinements) {
mappedCategories = this._categoryMapper({
pageCategoryL1: 'categoryl1',
pageCategoryL2: 'categoryl2',
pageCategoryL3: 'categoryl3'
},
this._getSomeCategory); // if i bind only here it will work, because it doesn't use fat arrow's this
} else if (this.originalData.items) {
mappedCategories = this._categoryMapper({
pageCategoryL1: 'a',
pageCategoryL2: 'b',
pageCategoryL3: 'c'
},
this._getSomeOtherCategory);
}
return mappedCategories;
}
_categoryMapper(mapping, callback) {
return Object.keys(mapping).reduce((acc, key) => {
// fat arrow in reduce should be implicitly bound to this
console.log(this);
let category = callback(mapping[key]).bind(this);
acc[key] = category ? category : '';
return acc;
}, {});
}
_getSomeCategory(categoryKey) {
// No access to this as currently written
console.log(this)
let refinements = this.originalData.search.refinements;
let matchedObj = refinements.find(({
refinement
}) => categoryKey === refinement.name);
return matchedObj && matchedObj.refinement.value;
}
_getSomeOtherCategory(categoryKey) {
let id = Object.keys(this.originalData.items)[0];
return this.originalData.items[id][categoryKey];
}
}
window.x = new AnalyticsData({
search: {
refinements: [{
refinement: {
name: 'categoryl1',
value: 'yup'
}
}]
}
}).mapCategories()
console.log(x)
/* this.data should be: {
pageCategoryL1: 'yup',
pageCategoryL2: '',
pageCategoryL3: ''
};*/
You're misusing bind here.
let category = callback(mapping[key]).bind(this);
bind creates a copy of a function with this set to whatever you passed it and zero or more arguments preloaded.
function log(argument1) {
console.log(this);
console.log(argument1);
}
let f = log.bind({ a: 1 }, 'a');
let g = log.bind({ b: 2 }, 'b');
f();
g();
What you probably want to use is call which calls a function with this set to it's first argument.
function log(argument1) {
console.log(this);
console.log(argument1);
}
log.call({ a: 1 }, 'a');
log.call({ b: 2 }, 'b');
The reason this === undefined is that callback is not defined with an arrow function nor does it have any other way of defining what this should be. This is essentially what you're doing.
'use strict';
let obj = {
a: 1,
log() {
console.log(this);
}
};
function callCallback(callback) {
callback();
}
// This is what you want to happen
callCallback(obj.log.bind(obj));
// This is what you're doing
callCallback(obj.log);

javascript: extending methods between objects, like mixins

I want to share or reuse some logic between differents objects, that they will be pretty similar, just changing the "scope".
var Mixin = {
show: function () {
this.container.show();
},
hide: function () {
this.container.hide();
},
play: function (data) {
data.map().append();
}
};
var ObjectA = {
container: $('#container_a');
foo: function () {
this.play(otherData); // Mixin common method?
}
};
var ObjectB = {
container: $('#container_b'),
foo: function () {
this.play(data); // Mixin common method?
}
};
ObjectA.show() // show $('#container_a');
ObjectB.show() // show $('#container_b');
I was trying using underscore
_.extend(ObjectA, Mixin);
but it seems like I have issues with the reference of the Mixin (this reference to the last extended object), like if i need to clone the object and extend it?
Is there any approach to do something similar?
Thanks!!
EDIT: I having issue with the scope of 'this', that is referencing to window, when a pass as a callback a function inherits from the mixin, like this.
PersonMixin = {
mixinFoo: function () {
this.handleResponse();
}
};
Person = {
personMethod: function () {
OtherLibrary.libMehtod(this.mixinFoo);
}
};
Object.assign(Person, PersonMixin);
and then, something like this will fail, this an example stack trace
Person.personMethod();
OtherLibrary.libMethod(callbackMixin);
Ajax.post(callbackMixin);
callbackMixin(response); // this.handleResponse() is not defined, because this reference to window object.
EDIT 2: I can solve this issue using bind()
You can do this in a number of ways, my preference is adjusting the objects __proto__ property on creation which will cause it to inherit your mixin via its prototype chain. This does not require the use of underscore.
I adjusted your example for ES6 and made it a bit simpler but should get the point across.
const PlayerType = (
{ show() {
console.info(`show ${this.name}`)
}
, hide() {
console.info(`hide ${this.name}`)
}
, play: function (data) {
data.map().append();
}
}
)
const objA = { __proto__: PlayerType
, name: 'objA'
, foo(...args) {
this.play(...args)
}
}
const objB = { __proto__: PlayerType
, name: 'objB'
, foo(...args) {
this.play(...args)
}
}
objA.show()
objB.show()
Simpler and no ES6:
var Mixin = (
{ show() {
console.info('show ' + this.name)
}
, hide() {
console.info('hide ' + this.name)
}
}
)
var a = { __proto__: Mixin, name: 'a' }
var b = { __proto__: Mixin, name: 'b' }
a.show()
b.show()
Alternate - Does the same thing with Object.create().
var Mixin = (
{ show() {
console.info('show ' + this.name)
}
, hide() {
console.info('hide ' + this.name)
}
}
)
var a = Object.create(Mixin, { name: { value: 'a', enumerable: true } })
var b = Object.create(Mixin, { name: { value: 'b', enumerable: true } })
a.show()
b.show()
It works, just check your syntax also.
var Mixin = {
show: function() {
console.log(this.tmp);
}
}
var oA = {
tmp: 'tmpA'
}
var oB = {
tmp: 'tmpB'
}
var mA = Object.assign(oA, Mixin);
var mB = Object.assign(oB, Mixin)
mA.show();
mB.show()

Assign methods to object by iterating over an array

In ES5, I know that it's possible to assign methods to an object using a forEach loop in the following way:
var myMethods = [
{
name: 'start',
src: someFn
},
{
name: 'stop',
src: someOtherFn
}
];
var myObject = {};
myMethods.forEach(function(method) {
myObject[method.name] = method.src;
});
In ES2015 (or ES6), is it possible to define these methods in tandem with creating the object? Here is an example of how I might expect this to work:
// example
const myObject = {
[...myMethods.map((method) => method.name)]: [...myMethods.map(method) => method.src)]
}
The end result would look like this:
const myObject = {
start: someFn,
stop: someOtherFn
}
If there is a way to iterate over these methods and assign them to myObject, I would happily restructure the myMethods array so that this is possible.
The end goal is to be able to assign each of these methods in an external module and not have to duplicate the definition.
Yes, you can use Object.assign and the spread operator in conjunction with computed property names to do
var myObject = Object.assign({}, ...myMethods.map(({name, src}) => ({[name]: src})));
First we map myMethods to an array of little one-property objects, whose key is given by the value of the name property and value by the src property. Then we use the spread operator ... to pass these to Object.assign as parameters. Object.assign then glues them all together for us.
Reduce should do the trick for you. Note that the optional second parameter is used to start with an empty object at the beginning.
var myMethods = [{
name: 'start',
src: function() {
console.log('started')
}
}, {
name: 'stop',
src: function() {
console.log('stopped')
}
}];
var myObject = myMethods.reduce((obj, method) => {
obj[method.name] = method.src;
return obj;
}, {})
console.log(myObject)
myObject.start()
myObject.stop()
Try assigning to myObject at same line of myMethods assignnemts
var myObject = {};
someFn = function(){console.log(this)};
someOtherFn = function(){console.log(this)};
var myObject = {};
someFn = function(){};
someOtherFn = function(){}
var myMethods = [
{
name: (myObject["start"] = "start"),
src: (myObject["start"] = someFn)
},
{
name: (myObject["stop"] = "stop"),
src: (myObject["stop"] = someOtherFn)
}
];

unable to access function from another function using this within same object

I have the following:
$scope.option = {
generateID:function(){
return Math.random().toString(36).replace(/[^a-z]+/g, '').substr(0, 5);
},
values : [
{id:this.generateId()},
{id:this.generateId()},
{id:this.generateId()},
{id:this.generateId()}
],
markCorrect : function(option){
},
remove:function(option)
{
this.values = this.values.filter(function(value){return value.id!=option.id})
}
}
I always get a this.generateId is not a function error. I am pretty sure that i am missing something fundamental here!
It may be better to store the id generator function in a separate function so it is easier to reference:
function generateId = function() {
return Math.random().toString(36).replace(/[^a-z]+/g, '').substr(0, 5);
}
$scope.option = {
generateID: generateId,
values : [
{id: generateId()},
{id: generateId()},
{id: generateId()},
{id: generateId()}
],
markCorrect : function(option){
},
remove:function(option)
{
this.values = this.values.filter(function(value){return value.id!=option.id})
}
}
The primary issue is that you're trying to access properties of $scope.option in the middle of declaring it. Try doing something like this instead:
$scope.option = (function () {
function generateId () {
/* logic */
}
return {
values: [
{id: generateId()}
// ...
],
markCorrect: function () {},
remove: function () {}
};
}) ();
This is the 'revealing module pattern', i.e. a function that returns an object forming a closure on some other data or functionality.
There is a typo; rename generateID to generateId.

Integrating native JavaScript classes in an Angular app

I have a native JavaScript class:
var Holder = new function(elements) {
this.elements = elements;
this.anyFunction() {
// use of this.elements
};
};
How to use it in an Angular-way? For example, if I would like to use:
.controller('AnyController', ['Holder',
function (Holder) {
var elements = [
{id: 1, label: 'foo'},
{id: 2, label: 'bar'}
];
$scope.holder = new Holder(elements);
}])
How should I register my Holder class then? What are the options (if any)?
In parallel, is it that bad to use native JavaScript classes in an Angular app (i.e. without integrating it within the framework)?
You could return a class with a factory
.factory('Holder', function() {
return (function (){
this.foo = foo;
this.bar = bar;
});
});
Now to use it
.controller('AnyController', ['Holder', function (Holder) {
var holder = new Holder();
}]);
EDIT
Use a factory instead of a service, as suggested in the comments
As I understand it, a factory is a singleton, but a factory can generate a class that can create instances. So the factory would return a reference to the constructor when you inject it, or a wrapper function around the constructor to use it without using new:
.factory('Holder', function() {
function Holder(elements) {
this.elements = elements;
}
Holder.prototype.get = function() {
return this.elements;
};
return function(elements) {
return new Holder(elements);
};
})
.controller('Main', function($scope, Holder) {
var elements = [
{id: 1, label: 'foo'},
{id: 2, label: 'bar'}
];
$scope.elements = Holder(elements).get();
});

Categories

Resources