I created this object to get css property names according to the browser.
e.g. js.transition will return either 'webkitTransition' or 'transition' as appropriate. All values are cached, ie. first reference will look up the value in the testElementStyle object, repeated references will return the cached value.
const js = {
get testElementStyle() {
delete this.testElementStyle;
return this.testElementStyle = document.createElement('div').style;
},
get transition() {
delete this.transition;
return this.transition = "transition" in this.testElementStyle ? "transition" : "webkitTransition"
},
get transform() {
delete this.transform;
return this.transform = "transform" in this.testElementStyle ? "transform" : "webkitTransform"
},
get userSelect() {
delete this.userSelect
return this.userSelect = "userSelect" in this.testElementStyle ? "userSelect" : "webkitUserSelect" }
}
As you can see the caching code is duplicated for each property. Ideally I want to create a single generic function that accepts prop name and does the rest.
e.g.
const cache = prop => alt => {
delete this[prop];
return this[prop] = prop in this.testElementStyle ? prop : alt;
}
... which of course does not work, I'm kinda stuck, help please!
This is what I did in the interim before reading your comments. With your tips I can now take it to the next level. Thanks everyone!
const testElementStyle = document.createElement('div').style;
function cache(prop, alt) {
delete this[prop];
return this[prop] = prop in testElementStyle ? prop : alt;
}
const js = {
get transition() { return cache.call(this, "transition", "webkitTransition") },
get transform() { return cache.call(this, "transform", "webkitTransform") },
get userSelect() { return cache.call(this, "userSelect", "webkitUserSelect") },
}
const css = {
get transform() { return cache.call(this, "transform", "-webkit-transform") },
}
You have two options:
Use a function and pass in a string
Use a Proxy (ES2015+)
As CertainPerformance has Proxy covered, here's how you'd do it with a function:
const js = {
get testElementStyle() {
return this.testElementStyle = document.createElement('div').style;
},
get(name) {
return this[name] = name in this.testElementStyle ? name : "webkit" + name.substring(0, 1).toUpperCase() + name.substring(1);
}
};
Usage:
js.testElementStyle(); // It's unclear to me why you make this setup call necessary
const transform = js.get("transform");
One option would be to use a Proxy, which can detect the property string you're trying to access on the object and perform custom actions depending on that string. With that string, you can then check to see if it exists in the style, use bracket notation to assign it to the object, and concatenate with webkit:
const js = new Proxy({}, {
get: (obj, prop) => {
if (prop === 'testElementStyle') {
obj.testElementStyle = document.createElement('div').style;
return this.testElementStyle;
}
this[prop] = prop in obj.testElementStyle
? prop
: 'webkit' + prop.replace(/^./, char => char.toUpperCase());
return this[prop];
}
});
Related
I have a very very very deep nested object state.
and i want to change all id properties at once with lodash cloneDeepWith methods.
i'm using cloneDeepWith and only works on first match.
if i dont return the modified object then it won't modifiy anything.
and if i return the value i think the function stops.
the function its working ok but the only problem is that only will run once.
const handleChangeIds = (value) => {
if (value === sections) {
const modifiedObject = cloneDeepWith(value, (sectionsValue) => {
if (sectionsValue && Object.hasOwn(sectionsValue, 'id')) {
const clonedObj = cloneDeep(sectionsValue);
clonedObj.id = generateObjectId();
return clonedObj;
// I Also Tried sectionsValue = clonedObj; its the same behavior
}
});
return modifiedObject;
}
};
const DuplicateSection = () => {
console.log('Original Store', form);
const store = cloneDeepWith(form, handleChangeIds);
console.log('Modified', store)
};
For those who want to achieve same thing like me.
I had a super deep nested object for form. and that form had a repeatable functionality.
and i needed to do two thing in generating another form.
generate new Id for every field Id.
clear the input Value.
I solved my problem like this
and it works perfectly for a super deep nested object.
import cloneDeepWith from 'lodash/cloneDeepWith';
const clearInputAndChangeId = (sections: FormSectionProps): FormSectionProps => {
return cloneDeepWith(sections, (value, propertyName, object) => {
if (propertyName === 'id') return generateObjectId();
if (propertyName === 'selected') return false;
if (propertyName === 'checked') return false;
if (propertyName === 'value') {
if (object.type === 'file') return [];
if (object.type === 'checkbox/rating') return 1;
return '';
}
});
};
I have an array of object values each with it's own sub array of objects.
I'm attempting to use the Array.find() method to target each array node specifically and return a value from it's subarray based on name.
I have my function all written out but nothing is being returned and I keep getting a warning stating:
Expected to return a value at the end of the function(array-callback-return)
And I'm not entirely sure what I'm missing.
The function and data structure for reference as well as a CodeSandbox:
const subjectTypes = [
{
name: "pokemon",
details: [
{
type: "subtitle",
value: "This is the subtitle for pokemon"
}
]
},
{
name: "marvel",
details: [
{
type: "subtitle",
value: "This is the subtitle for marvel"
}
]
}
];
const setCustomValue = (location) => {
let value;
switch (location) {
case "/pokemon":
return (value = subjectTypes.find(function (subject, idx) {
if (subject.name === "pokemon") return subject.details.value;
}));
case "/marvel":
return (value = subjectTypes.find(function (subject, idx) {
if (subject.name === "marvel") return subject.details.value;
}));
default:
return null;
}
};
const customValue = setCustomValue("/pokemon");
export default function App() {
return (
<div className="App">
<h1>Hello CodeSandbox</h1>
<h2>Dynamically Populated Content Below:</h2>
<p>{customValue}</p>
</div>
);
}
Also I'm aware that this may appear like over engineering and that I could easily just use a .map() or similar to print out values, but I'm using this as a simplified version of a larger problem I'm trying to solve that requires something like the .find() method to return a specific result.
Array#find() expects a true or false from the callback. Or at the very least a truthy or falsy value.
The ESLint rule array-callback-return reports a related problem. The callback only returns something from the if, not when the condition is not met. This is normally valid - no return makes the function implicitly return undefined. Since this value is falsy, find will ignore this item and continue with the next one. However, the ESLint rule is there to make these returns explicit and prevent mistakes, in case it is actually a bug that a branch of code does not return anything.
In the callback the return value is subject.details.value but the actual path is subject.details[0].value because subject.detailsis an array. Therefore the only return you have, produces undefined.
It's not possible to both locate a value with .find() and modify what is returned at the same time. You need to return the value first and then get a sub-section of it.
No need to declare and assign value when it is never used.
To address these, here is the code a bit cleaned up:
const setCustomValue = (location) => {
let value;
switch (location) {
case "/pokemon":
value = subjectTypes.find(function (subject, idx) {
return subject.name === "pokemon";
});
return value.details[0].value;
case "/marvel":
value = subjectTypes.find(function (subject, idx) {
return subject.name === "pokemon";
});
return value.details[0].value;
default:
return null;
}
};
To avoid some of the repetition, you can extract the .find() and return logic outside the switch:
const setCustomValue = (location) => {
let name;
switch (location) {
case "/pokemon":
name = "pokemon";
break;
case "/marvel":
name = "marvel";
break;
default:
return null;
}
const value = subjectTypes.find((subject) => subject.name === name);
return value.details[0].value;
};
The whole switch can be eliminated by using a simple lookup instead:
const paths = {
"/pokemon": "pokemon",
"/marvel": "marvel",
}
const setCustomValue = (location) => {
let name = paths[location];
if (!name)
return null;
const value = subjectTypes.find((subject) => subject.name === name);
return value.details[0].value;
};
And even more minimal using optional chaining and the nullish coalescing operator
const paths = {
"/pokemon": "pokemon",
"/marvel": "marvel",
}
const setCustomValue = (location) =>
subjectTypes.find((subject) => subject.name === paths[location])?.details[0].value ?? null;
Find returns you the element, based on a boolean query. So you have to find through name, and then get that element's value
Try:
return (value = subjectTypes.find( function (subject, idx) {
return subject.name === "pokemon";
}).details.value);
A cleaner way of writing this, in my totally personal opinion:
value = subjectTypes.find(
subject => subject.name === "pokemon"
).details.value
The find() method returns the value of the first element in the
provided array that satisfies the provided testing function. If no
values satisfy the testing function, undefined is returned.
it will return all the element that satisfies the provided testing function
try like this :
return subjectTypes.find(function (subject,idx) {
return subject.name === "pokemon"
}).details[0].value
details it is not an object it is an array so you have to select the oject inside the array then get the value of it
or you can simply use arrow function like this
reuturn subjectTypes.find( (subject,idx)=>
subject.name === "pokemon"
).details[0].value
I wanted to check what does a library do with video element I pass to it so I naively did this:
cosnt videoElement = new Proxy(document.querySelector('video'), {
get(target, key) {
const name = typeof key === 'symbol'? key.toString() : key;
console.info(`Accessing video.${ name }`)
return target[key];
}
});
But I got an error:
TypeError: Failed to execute 'contains' on 'Node': parameter 1 is not of type 'Node'.
Is there a way to make this work?
EDIT: I have gained some knowledge, with it I updated my proxy as fallow:
cosnt videoElement = document.querySelector('video');
cosnt proxyElement = new Proxy(videoElement , {
get(target, key) {
if (key == '___element___') {
return video;
}
const name = typeof key === 'symbol'? key.toString() : key;
console.info(`Accessing video.${ name }`);
const value = video[key];
if (value instanceof Function) {
return video[key].bind(video);
} else {
return video[key];
}
},
set(target, key, value) {
const name = typeof key === 'symbol'? key.toString() : key;
console.info(`Writing video.${ name } = ${ value }`);
video[key] = value;
return true;
}
});
It is meant for debugging, so I edited the compiled code and replaced all DOM manipulation references with element.___element___.
Next I found out that there seem to be problems with calling functions trough proxy, so I added the .bind(video) part.
And finally setting values was throwing. So I had to replace target with direct video reference (as matter of fact, I replaced all target references with video, just to be sure), that made it work... I am not sure why or how, but it did.
Questions are:
Is this really how it is supposed to be? (the document.body.contains(myProxyElement) part)
Setting values to video element throwing when setting inside proxy seemed weird, is it a bug? (3rd point, kinda second too, I suppose it is connected)
Bonus: playground
const video = document.querySelector('video');
const proxy = new Proxy(video, {
get(target, key) {
console.log(`Getting video.${typeof key === 'symbol'? key.toString() : key}`);
const value = video[key];
if (value instanceof Function) {
return value.bind(video);
} else {
return value;
}
},
set(target, key, value) {
console.log(`Setting video.${typeof key === 'symbol'? key.toString() : key} =`, value);
video[key] = value;
return true;
}
});
proxy.muted = true;
proxy.play();
proxy.controls = true;
try {
console.log(document.body.contains(proxy));
} catch (e) {
console.error('DOM operation failed', e);
}
video { max-width: 100%; }
<video src="//vjs.zencdn.net/v/oceans.mp4">
As already mentioned in the comments, the Proxy object will not get automatically casted into Node when calling document.body.contains(proxy).
Therefore, you can i.e. set a specific key that will return the proxy's target:
const video = document.querySelector('video');
const proxy = new Proxy(video, {
get(target, key) {
const value = video[key];
if (value instanceof Function) {
return value.bind(video);
} else if (key == 'target') {
return target;
} else {
return value;
}
},
set(target, key, value) {
target[key] = value;
return true;
}
});
And then you can do:
console.log(document.body.contains(proxy.target));
Edit:
Is this really how it is supposed to be? (the
document.body.contains(myProxyElement) part):
Yes, I do not see any other way how to do this.
Setting values to video element throwing when setting inside proxy
seemed weird, is it a bug? (3rd point, kinda second too, I suppose it
is connected):
Since I am not able to reproduce this issue, it is hard to say. Maybe try to replace new Proxy(video, ... with new Proxy({}, ... and then set the video as proxy.video = video (you will have to also update the logic in the get and set) and see how that behaves?
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 am looking for an efficient way to translate my Ember object to a json string, to use it in a websocket message below
/*
* Model
*/
App.node = Ember.Object.extend({
name: 'theName',
type: 'theType',
value: 'theValue',
})
The websocket method:
App.io.emit('node', {node: hash});
hash should be the json representation of the node. {name: thename, type: theType, ..}
There must be a fast onliner to do this.. I dont want to do it manualy since i have many attributes and they are likely to change..
As stated you can take inspiration from the ember-runtime/lib/core.js#inspect function to get the keys of an object, see http://jsfiddle.net/pangratz666/UUusD/
App.Jsonable = Ember.Mixin.create({
getJson: function() {
var v, ret = [];
for (var key in this) {
if (this.hasOwnProperty(key)) {
v = this[key];
if (v === 'toString') {
continue;
} // ignore useless items
if (Ember.typeOf(v) === 'function') {
continue;
}
ret.push(key);
}
}
return this.getProperties.apply(this, ret);
}
});
Note, since commit 1124005 - which is available in ember-latest.js and in the next release - you can pass the ret array directly to getProperties, so the return statement of the getJson function looks like this:
return this.getProperties(ret);
You can get a plain JS object (or hash) from an Ember.Object instance by calling getProperties() with a list of keys.
If you want it as a string, you can use JSON.stringify().
For example:
var obj = Ember.Object.create({firstName: 'Erik', lastName: 'Bryn', login: 'ebryn'}),
hash = obj.getProperties('firstName', 'lastName'), // => {firstName: 'Erik', lastName: 'Bryn'}
stringHash = JSON.stringify(hash); // => '{"firstName": "Erik", "lastName": "Bryn"}'
I have also been struggling with this. As Mirko says, if you pass the ember object to JSON.stringify you will get circular reference error. However if you store the object inside one property and use stringify on that object, it works, even nested subproperties.
var node = Ember.Object.create({
data: {
name: 'theName',
type: 'theType',
value: 'theValue'
}
});
console.log(JSON.stringify(node.get('data')));
However, this only works in Chrome, Safari and Firefox. In IE8 I get a stack overflow so this isn't a viable solution.
I have resorted to creating JSON schemas over my object models and written a recursive function to iterate over the objects using the properties in the schemas and then construct pure Javascript objects which I can then stringify and send to my server. I also use the schemas for validation so this solution works pretty well for me but if you have very large and dynamic data models this isn't possible. I'm also interested in simpler ways to accomplish this.
I modifed #pangratz solution slightly to make it handle nested hierarchies of Jsonables:
App.Jsonable = Ember.Mixin.create({
getJson: function() {
var v, json = {};
for (var key in this) {
if (this.hasOwnProperty(key)) {
v = this[key];
if (v === 'toString') {
continue;
}
if (Ember.typeOf(v) === 'function') {
continue;
}
if (App.Jsonable.detect(v))
v = v.getJson();
json[key] = v;
}
}
return json;
}
});
App.io.emit('node', {node: node.toJSON()});
Or if you have an ID property and want to include it:
App.io.emit('node', {node: node.toJSON({includeId: true})});
Will this work for you?
var json = JSON.stringify( Ember.getMeta( App.node, 'values') );
The false is optional, but would be more performant if you do not intend to modify any of the properties, which is the case according to your question. This works for me, but I am wary that Ember.meta is a private method and may work differently or not even be available in future releases. (Although, it isn't immediately clear to me if Ember.getMeta() is private). You can view it in its latest source form here:
https://github.com/emberjs/ember.js/blob/master/packages/ember-metal/lib/utils.js
The values property contains only 'normal' properties. You can collect any cached, computed properties from Ember.meta( App.node, false ).cached. So, provided you use jQuery with your build, you can easily merge these two objects like so:
$.extend( {}, Ember.getMeta(App.node, 'values'), Ember.getMeta(App.node, 'cache') );
Sadly, I haven't found a way to get sub-structures like array properties in this manner.
I've written an extensive article on how you can convert ember models into native objects or JSON which may help you or others :)
http://pixelchild.com.au/post/44614363941/how-to-convert-ember-objects-to-json
http://byronsalau.com/blog/convert-ember-objects-to-json/
I modified #Kevin-pauli solution to make it works with arrays as well:
App.Jsonable = Ember.Mixin.create({
getJson: function() {
var v, json = {}, inspectArray = function (aSome) {
if (Ember.typeof(aSome) === 'array') {
return aSome.map(inspectArray);
}
if (Jsonable.detect(aSome)) {
return aSome.getJson();
}
return aSome;
};
for (var key in this) {
if (this.hasOwnProperty(key)) {
v = this[key];
if (v === 'toString') {
continue;
}
if (Ember.typeOf(v) === 'function') {
continue;
}
if (Ember.typeOf(v) === 'array') {
v = v.map(inspectArray);
}
if (App.Jsonable.detect(v))
v = v.getJson();
json[key] = v;
}
}
return json;
}
});
I also made some further modification to get the best of both worlds. With the following version I check if the Jsonable object has a specific property that informs me on which of its properties should be serialized:
App.Jsonable = Ember.Mixin.create({
getJson: function() {
var v, json = {}, base, inspectArray = function (aSome) {
if (Ember.typeof(aSome) === 'array') {
return aSome.map(inspectArray);
}
if (Jsonable.detect(aSome)) {
return aSome.getJson();
}
return aSome;
};
if (!Ember.isNone(this.get('jsonProperties'))) {
// the object has a selective list of properties to inspect
base = this.getProperties(this.get('jsonProperties'));
} else {
// no list given: let's use all the properties
base = this;
}
for (var key in base) {
if (base.hasOwnProperty(key)) {
v = base[key];
if (v === 'toString') {
continue;
}
if (Ember.typeOf(v) === 'function') {
continue;
}
if (Ember.typeOf(v) === 'array') {
v = v.map(inspectArray);
}
if (App.Jsonable.detect(v))
v = v.getJson();
json[key] = v;
}
}
return json;
}
});
I am using this little tweak and I am happy with it. I hope it'll help others as well!
Thanks to #pangratz and #Kevin-Pauli for their solution!
Here I take #leo, #pangratz and #kevin-pauli solution a little step further. Now it iterates not only with arrays but also through has many relationships, it doesn't check if a value has the type Array but it calls the isArray function defined in Ember's API.
Coffeescript
App.Jsonable = Em.Mixin.create
getJson: ->
jsonValue = (attr) ->
return attr.map(jsonValue) if Em.isArray(attr)
return attr.getJson() if App.Jsonable.detect(attr)
attr
base =
if Em.isNone(#get('jsonProperties'))
# no list given: let's use all the properties
this
else
# the object has a selective list of properties to inspect
#getProperties(#get('jsonProperties'))
hash = {}
for own key, value of base
continue if value is 'toString' or Em.typeOf(value) is 'function'
json[key] = jsonValue(value)
json
Javascript
var hasProp = {}.hasOwnProperty;
App.Jsonable = Em.Mixin.create({
getJson: function() {
var base, hash, hashValue, key, value;
jsonValue = function(attr) {
if (Em.isArray(attr)) {
return attr.map(jsonValue);
}
if (App.Jsonable.detect(attr)) {
return attr.getJson();
}
return attr;
};
base = Em.isNone(this.get('jsonProperties')) ? this : this.getProperties(this.get('jsonProperties'));
json = {};
for (key in base) {
if (!hasProp.call(base, key)) continue;
value = base[key];
if (value === 'toString' || Em.typeOf(value) === 'function') {
continue;
}
json[key] = jsonValue(value);
}
return json;
}
});
Ember Data Model's object counts with a toJSON method which optionally receives an plain object with includeId property used to convert an Ember Data Model into a JSON with the properties of the model.
https://api.emberjs.com/ember-data/2.10/classes/DS.Model/methods/toJSON?anchor=toJSON
You can use it as follows:
const objects = models.map((model) => model.toJSON({ includeId: true }));
Hope it helps. Enjoy!
I have:
fixed and simplified code
added circular reference prevention
added use of get of value
removed all of the default properties of an empty component
//Modified by Shimon Doodkin
//Based on answers of: #leo, #pangratz, #kevin-pauli, #Klaus
//http://stackoverflow.com/questions/8669340
App.Jsonable = Em.Mixin.create({
getJson : function (keysToSkip, visited) {
//getJson() called with no arguments,
// they are to pass on values during recursion.
if (!keysToSkip)
keysToSkip = Object.keys(Ember.Component.create());
if (!visited)
visited = [];
visited.push(this);
var getIsFunction;
var jsonValue = function (attr, key, obj) {
if (Em.isArray(attr))
return attr.map(jsonValue);
if (App.Jsonable.detect(attr))
return attr.getJson(keysToSkip, visited);
return getIsFunction?obj.get(key):attr;
};
var base;
if (!Em.isNone(this.get('jsonProperties')))
base = this.getProperties(this.get('jsonProperties'));
else
base = this;
getIsFunction=Em.typeOf(base.get) === 'function';
var json = {};
var hasProp = Object.prototype.hasOwnProperty;
for (var key in base) {
if (!hasProp.call(base, key) || keysToSkip.indexOf(key) != -1)
continue;
var value = base[key];
// there are usual circular references
// on keys: ownerView, controller, context === base
if ( value === base ||
value === 'toString' ||
Em.typeOf(value) === 'function')
continue;
// optional, works also without this,
// the rule above if value === base covers the usual case
if (visited.indexOf(value) != -1)
continue;
json[key] = jsonValue(value, key, base);
}
visited.pop();
return json;
}
});
/*
example:
DeliveryInfoInput = Ember.Object.extend(App.Jsonable,{
jsonProperties: ["title","value","name"], //Optionally specify properties for json
title:"",
value:"",
input:false,
textarea:false,
size:22,
rows:"",
name:"",
hint:""
})
*/
Ember.js appears to have a JSON library available. I hopped into a console (Firebug) on one the Todos example and the following worked for me:
hash = { test:4 }
JSON.stringify(hash)
So you should be able to just change your line to
App.io.emit('node', { node:JSON.stringify(hash) })