I am coding a chainable library and I want the API to allow calling part of the chain as static values (with a default value) and sometimes as functions, so parameters could be pass to them.
Simplified example:
var obj = {};
var chainCache = [];
Reflect.defineProperty(obj, 'color', {
get(){
chainCache.push('red');
return obj;
}
})
Reflect.defineProperty(obj, 'background', {
get(){
chainCache.push('black');
return obj;
}
})
Reflect.defineProperty(obj, 'end', {
value(){
var value = chainCache.join(" ");
chainCache.length = 0;
return value;
}
})
console.log( obj.color.background.end() ) // red black
This is a very simplified example and in reality I would also like to include an ability in the above "API" to optionally use the same color key, like this:
obj.color.background.end() // current API (great)
obj.color('#FF0').background.end() // optionally call "color" as function
obj.color().background.end() // bad API, I do not want this
Can color be both function and property at the same time, depending how it is called?
I took a harder look at this than I intended. It looks like with the advent of Proxies this is possible. Building on your example:
let chainCache = [];
let obj = {
color: new Proxy(()=>{}, {
get: (target, prop) => {
chainCache.push("red");
if(prop === "background") {
chainCache.push("black");
return { end: obj.end };
}
},
apply: (target, prop, args) => {
if(args.length > 0) {
chainCache.push(args.pop());
} else {
throw new Error("color expects argument if called like function");
}
return { background: new Proxy(()=>{}, {
get: (target,prop) => { chainCache.push("black"); return obj.end;}
})};
}
}),
end: () => {
let value = chainCache.join(" ");
chainCache = [];
return value;
}
};
console.log(obj.color.background.end());
console.log(obj.color("#FF0").background.end());
console.log(obj.color().background.end());
In the debug console you get:
red black
#FF0 black
Error: color expects argument if called like function
Essentially color is a proxy that wraps an anonymous arrow function, if color is accessed like a property the get() trap gets called, if color gets accessed as a function the apply() trap gets called. This allows for a certain degree of meta-programming like you're looking for.
You will need to make obj a function.
A function is just an object and can have properties as well - obj.color must return a function (for obj.color()) that has properties (for obj.color.background).
But no, when the getter is accessed you don't know yet whether it will be used for a method invocation or not - obj.color() is a property access plus a function call.
var chainCache = [];
var obj = Object.defineProperties(function obj(...args) {
chainCache.push(args);
return obj;
}, {
color: {
get() {
chainCache.push('color');
return obj;
}
},
background: {
get() {
chainCache.push('background');
return obj;
}
},
end: {
value: function() {
var res = chainCache.reduce((acc, val) =>
Array.isArray(val)
? `${acc}(${val.join(',')})`
: `${acc}.${val}`
, "obj");
chainCache.length = 0;
return res;
}
}
})
Related
The function skeleton is at the top of mixin.js. It takes in a target object (o) and another object with properties to be mixed in (mixin).
You should return a Proxy object from this function. Override the "get" trap so that:
1) If the object already has a property, the object's property is returned.
2) If the object does not have the property, it returns the property from the mixin object.
3) If the property is "__original", it returns the original object "o". (This design provides a way to "unmix" a mixin.)
4) If none of the other cases hold, undefined should be returned as the result.
You should not make any changes to this file outside of the addMixin function definition.
This is what I have so far,
function addMixin(o, mixin) {
let oldValue = {};
return new Proxy(o, {
get: function(target, property) {
if(target.hasOwnProperty(property) === true) {
return target[property];
} else if (target.hasOwnProperty(property) === false) {
oldValue[property] = target[property];
return mixin[property];
} else if (property === '__original') {
return target[property] = oldValue[property];
} else {
return undefined;
}
}
})
// A sample mixin.
let PlayableMixin = {
// Plays a system bell 3 times
play: function() {
console.log("\u0007");
console.log("\u0007");
console.log("\u0007");
},
duration: 100,
};
function Song(name, performer, duration) {
this.name = name;
this.performer = performer;
this.duration = duration;
}
Song.prototype = addMixin(Song.prototype, PlayableMixin);
Song.prototype.display = function() {
console.log(`Now playing "${this.name}", by ${this.performer}. (${this.duration})`);
}
let s = new Song("Gun Street Girl", "Tom Waits", "4:17");
s.display();
s.play();
console.log(s.duration);
s = s.__original;
console.log(s.play);
Expected Output
Now playing "Gun Street Girl", by Tom Waits. (4:17)
4:17
undefined
Gives me an error instead of printing undefined.
You don't really need the if else chain. return already breaks out of the function. __original should be checked first, because it should return the original object regardless of whether it exists as a property or not; it is essentially a reserved symbol name for a computed property.
Your code is also missing a closing bracket.
function addMixin(o, mixin) {
let oldValue = {};
return new Proxy(o, {
get: function(target, property) {
if (property === '__original') {
return o;
}
if (target.hasOwnProperty(property) === true) {
return target[property];
} else if (target.hasOwnProperty(property) === false) {
oldValue[property] = target[property];
return mixin[property];
} else {
return undefined;
}
}
})
}
// A sample mixin.
let PlayableMixin = {
// Plays a system bell 3 times
play: function() {
console.log("\u0007");
console.log("\u0007");
console.log("\u0007");
},
duration: 100,
};
function Song(name, performer, duration) {
this.name = name;
this.performer = performer;
this.duration = duration;
}
Song.prototype = addMixin(Song.prototype, PlayableMixin);
Song.prototype.display = function() {
console.log(`Now playing "${this.name}", by ${this.performer}. (${this.duration})`);
}
let s = new Song("Gun Street Girl", "Tom Waits", "4:17");
s.display();
s.play();
console.log(s.duration);
s = s.__original;
console.log(s, s.play);
Just a side comment: I'd complain about the problem description. The way declared conditions are listed almost seems like it was purposely set up to be confusing. It's like one of those silly riddles where someone makes you repeat what they say and say something stupid about yourself.
I'm adding listeners to one link or multiple links, using the following code:
function getData() {
var context = {};
context['triggers'] = triggers();
context['msg'] = msg;
return context
}
function triggers() {
var arr = [];
document.querySelectorAll('.trigger').forEach(function (trigger, index) {
arr[index] = {};
arr[index]['trigger'] = trigger;
});
return arr;
}
function addListeners(data) {
data.triggers.forEach(function (trigger) {
trigger.addEventListener('click', change)
});
}
data = geData()
Trigger is an anchor:
I get the following error:
TypeError: trigger.addEventListener is not a function
The object in triggers isn't the anchor, it's an object that contains the anchor as a property called trigger. So:
function addListeners(data) {
data.triggers.forEach(function (entry) { // *** `entry` instead of `trigger`
entry.trigger.addEventListener('click', change)
// -----^^^^^^
});
}
We know this because of this code:
function triggers() {
var arr = [];
document.querySelectorAll('.trigger').forEach(function (trigger, index) {
arr[index] = {};
arr[index]['trigger'] = trigger;
});
return arr;
}
That's clearly creating an object, then setting the element as a trigger property on it.
Side note: You can use property initializers and property literal syntax in several places where you're using strings, and FWIW you can apply map to a NodeList:
function getData() {
return {
triggers: triggers(),
msg: msg
};
}
function triggers() {
return Array.prototype.map.call(
document.querySelectorAll('.trigger'),
function(anchor) {
return {trigger: anchor};
}
);
}
function addListeners(data) {
data.triggers.forEach(function (entry) {
entry.trigger.addEventListener('click', change)
});
}
data = geData();
I want to do:
properties.email.value without triggering an error like: Can't read 'value' of 'undefined'
However, I don't want to do:
properties.email && properties.email.value and I don't want to use an helper, something like: get(properties, 'email.value').
I really want to keep the syntax properties.email.value
I can solve this by doing:
Object.defineProperty(properties, 'email', {
get: () => properties.email && properties.email.value,
enumerable: true,
configurable: true
});
Now the getter is in charge of doing my safety check. Perfect.
But I also want to be able to do properties.name.value safely.
But as the properties object comes from the API (json), I don't know the full list of properties possible.
So, is there a way to use this "magical" get syntax for any prop access like: properties[ANYTHING].value ?
OK, I've got something like this.
But you must create properties that way.
Hope this help :)
var properties = {
phone : {
value: "123456789"
}
}
var handler = {
get: function(target, name) {
return target.hasOwnProperty(name) ? target[name] : {};
}
};
var new_properties = new Proxy(properties, handler);
console.log("phone.value = " + new_properties.phone.value);
console.log("email.value = " + new_properties.email.value);
new_properties.email = {
value: 1
};
console.log("email.value after assign = " + new_properties.email.value);
The document reference here.
Edited
Even if the original properties object is unknown, this kind of usage works as well.
You could use a Proxy and get known properties and a custom result for unknow properties.
For changing properties, you could take the same approach and set the value.
var properties = { email: { value: 'foo#example.com' } },
proxy = new Proxy(
properties,
{
get: function(target, prop, receiver) {
if (prop in target) {
return target[prop] && target[prop].value
} else {
return;
}
},
set: function(target, prop, value) {
if (prop in target) {
target[prop].value = value;
} else {
target[prop] = { value };
}
}
}
);
console.log(proxy.email);
console.log(proxy.bar);
proxy.email = '41';
console.log(proxy.email);
I can't believe I'm doing this...
var wordlength = 7;
var alphabet="abcdefghijklmnopqrstuvwxyz";
alphabet += alphabet.toUpperCase() + "0123456789_";
var alen = alphabet.length;
var buildWord = function(number){
if(number===0){
return '';
}
return alphabet[number%alen]+buildWord(Math.floor(number/alen));
};
var total = Math.pow(alen, wordlength);
for(var i = 1; i<total; i++){
var w = buildWord(i);
if(isNaN(w[0]) && Object.prototype[w]===undefined){
Object.prototype[w]={};
}
}
I want to achieve this functionality:
I have an object var obj = {};
I have three properties on that obj, obj.zero & obj.one& obj.binaryString
obj.zero & obj.one are methods while obj.binaryString is a string
When I chain the properties, I want them to add their respective digit to the binaryString. So for example:
obj.one.zero.zero.one => makes obj.binaryString = 1001
obj.one.zero.one.one.zero => makes obj.binaryString = 10110
I have achieved the above functionality with this:
function Binary () {
var obj = { binaryString: '' };
Object.defineProperty(obj, 'zero', {
get: function() {
obj.binaryString += '0';
return obj;
}
});
Object.defineProperty(obj, 'one', {
get: function() {
obj.binaryString += '1';
return obj;
}
});
return obj;
}
var binary = new Binary();
binary.one.zero.zero.one // => obj.binaryString becomes '1001'
Now I want to log out the completed binaryString, plus and additionalString which I have accomplished with the code below:
// placed inside Binary constructor function
Object.defineProperty(obj, 'log', {
get: function() {
return function(additionalString) {
console.log(obj.binaryString + additionalString);
};
}
});
So with this current code I can do this:
binary.one.zero.one.zero.one.log(' is the answer');
// logs out `10101 is the answer`
What I want to do is get rid of the log and make the one and zero methods invokable or not so I can achieve this functionality:
binary.one.one.zero.one(' is the result')
// => logs out `1101 is the result`
How can I do this?
I believe it would be similar functionality to how Chalk works:
chalk.blue.bold('Hello world!');
// `blue` is not invoked here but it adds the color blue to the style
chalk.blue('Hello world!');
// `blue` IS invoked here. It adds blue to the style and returns the stylized string
Just Make the obj as function , and print what ever you want .
function Binary () {
var obj = function(msg){ console.log(msg+this.binaryString ) };
obj.binaryString = ''
Object.defineProperty(obj, 'zero', {
get: function() {
obj.binaryString += '0';
return obj;
}
});
Object.defineProperty(obj, 'one', {
get: function() {
obj.binaryString += '1';
return obj;
}
});
return obj;
}
var binary = new Binary();
binary.one.zero.zero.one.zero(" is the result ")
I would like to point out that what you're doing is a very bad/dangerous idea: abusing read properties to mutate the object itself is asking for trouble. You may not see it now, but it's going to lead to pain and heartache down the line in the form of difficult-to-find bugs and convoluted patterns.
What you can do is, which is not so dangerous, is instead of mutating the object itself, return a new instance of Binary with every call to #one or #zero. For example:
function Binary(s) {
this.binaryString = s || ''
}
Object.defineProperty(Binary.prototype, 'zero', {
get: function() {
return new Binary(this.binaryString + '0')
}})
Object.defineProperty(Binary.prototype, 'one', {
get: function() {
return new Binary(this.binaryString + '1')
}})
This is the approach taken by Chalk, and will be much safer and less error-prone.
UPDATE:
After thinking about your problem, and seeing your question, I think the best approach at all is not to use classes at all. You can solve this problem with a pure function-based approach. It's immutable, it's safe, and I believe it's less confusing. Here it is in ES5:
function bin(str) {
if(!str) str = ''
function f(msg) { return str + ' ' + msg }
return Object.defineProperties(f, {
zero: {
get: function() {
return bin(str + '0')
},
},
one: {
get: function() {
return bin(str + '1')
},
},
})
}
And if you can use ES6 (aka ES2015), you can make it much more compact:
function bin(str = '') {
return Object.defineProperties(msg => `${str} ${msg}`, {
zero: { get() { return bin(str + '0') } },
one: { get() { return bin(str + '1') } },
})
}
You would use it like this:
bin().one.zero.one.zero('is the answer') // '1010 is the answer'
I have a JSON object that looks a bit like this:
{
name: 'test',
details: {
description: 'This is the long description',
shortDescription: 'This is the short description (ironically longer than the description!)'
}
}
Obviously the real object is a lot more complicated than this example, but I have omitted the details because they will only complicate the question.
So, with this object, I have a function that tries to get the value of the property, it looks like this:
// Private function for matching fields
var _matchField = function (item, filter) {
// Our variables
var text = item[filter.field],
values = filter.expression.split(',');
// If we have any text
if (text) {
// Loop through our values
angular.forEach(values, function (value) {
console.log(text);
console.log(value);
// See if we have a match
if (text.toLowerCase().indexOf(value.toLowerCase()) > -1) {
// We have found a match
return true;
}
});
}
// We have found no matches
return false;
}
The issue is the line:
var text = item[filter.field],
If the property was just the name then item['name'] would work with the above object. But if I want to get the description; item['details.descrption'] doesn't work.
So I need a function that will allow me to specify a property name and it will find the property and return its value.
But before I try to write one, I was hoping there might be a simple solution that someone has come across.
you can write your custom function for this
function getProperty(json, field) {
if (json == null || field == null) {
return null;
}
var value = json;
var fields = field.split(".");
for (var i = 0; i < fields.length; i++) {
value = value[fields[i]];
if (value == null) {
return null;
}
}
return value;
}
check this plnkr example https://plnkr.co/edit/8Ayd9wnh1rJh1ycx5R1f?p=preview
You can split the reference to the object and use a function for getting the right nested object/value.
function getValue(o, p) {
if (typeof p === 'string') {
p = p.split('.')
}
return p.length ? getValue(o[p.shift()], p) : o;
}
var item = { name: 'test', details: { description: 'This is the long description', shortDescription: 'This is the short description (ironically longer than the description!)' } };
document.write(getValue(item, 'details.description'));
I solved this by creating this function:
// Private function to get the value of the property
var _getPropertyValue = function (object, notation) {
// Get all the properties
var properties = notation.split('.');
// If we only have one property
if (properties.length === 1) {
// Return our value
return object[properties];
}
// Loop through our properties
for (var property in object) {
// Make sure we are a property
if (object.hasOwnProperty(property)) {
// If we our property name is the same as our first property
if (property === properties[0]) {
// Remove the first item from our properties
properties.splice(0, 1);
// Create our new dot notation
var dotNotation = properties.join('.');
// Find the value of the new dot notation
return _getPropertyValue(object[property], dotNotation);
}
}
}
};