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 want to do something like this:
let DOM = {
slide1: {
button: ".btn",
box: "#box"
}
}
and then when I write:
console.log(DOM.slide1.box);
I want the output to be "#slide1 #box".
After some research, I've tried to use the ES6 Proxy object, but the problem wasn't solved this way:
const dom_names = {
slide1: {
button: ".btn",
box: "#box"
}
}
const DOM_handler = {
get: (target, property, receiver)=>{
if(property == "slide1") return "#slide1";
return Reflect.get(...arguments);
}
}
const DOM = new Proxy(dom_names, DOM_handler);
After this implementation, I've recognized that I need to write the following to get the result I'm looking for:
console.log(`${DOM.slide1} ${DOM.create.box}) // #slide1 #box
As you can see, this is a very bad way, I was thinking of using my solution (using proxies), but the problem is: I need a way to know the "property of the property", I hope you can see and understand what I mean.
Some people will say: use your first implementation (the one without the proxies, meaning the first code in the question) but use for..of loop to get the key names. I will say, I want to use this exact code:
DOM.slide1.box
to get this output:
"#slide1 #box"
Why? why I am doing what I'm doing? Because I need a way to define the HTML classes and IDs in JavaScript, which makes life a lot easier when using document.querySelector. Also, the benefit of changing the classes and ID names in the HTML file, so when I change the classes and ID names in the HTML file, I will no longer need to re-factor (re) (rewrite) (change) the newer classes and IDs names inside the whole JavaScript file.
While a proxy can do this, there's a much simpler approach: just put the right string values in the properties of DOM in the first place. This still allows you to access them like you want:
const DOM = selectorHierarchy({
slide1: {
button: ".btn",
box: "#box"
}
});
function selectorHierarchy(obj, ancestors = []) {
for (var key in obj)
if (typeof obj[key] == "string")
obj[key] = [...ancestors, obj[key]].join(" ");
else if (typeof obj[key] == "object")
selectorHierarchy(obj[key], [...ancestors, '#'+key]);
return obj;
}
console.log(DOM.slide1.box)
This is possible. A naive approach is to apply proxies to every object recursively, then pass a private property along with each call, stringifying the traversal to a leaf.
Proof of concept:
const deepProxyAll = (obj, proxyObj) => {
for (const k in obj) {
if (typeof obj[k] === "object") {
obj[k] = deepProxyAll(obj[k], proxyObj);
}
}
return new Proxy(obj, proxyObj);
};
const proxyObj = {
get: (target, property, receiver) => {
if (!target.hasOwnProperty(property)) {
throw new Error(`property '${property}' does not exist on object`);
}
else if (typeof target[property] === "object" &&
typeof target[property] !== "null") {
target[property]._str = target._str ?
`${target._str} ${property}` : `#${property}`;
return target[property];
}
return target._str ? `${target._str} ${target[property]}`
: `#${target[property]}`;
}
};
const obj = {
slide1: {
button: ".btn",
box: "#box",
foobar: {bar: {quux: {garply: 42}}}
},
test: "hello",
};
const DOM = deepProxyAll(obj, proxyObj);
console.log(DOM.slide1.box);
console.log(DOM.slide1.foobar.bar.quux.garply);
console.log(DOM.test);
Proxies are powerful and it's temping to change the behavior of object property access to suit every whim, but think critically about whether the benefits are worth the downsides before adopting this sort of approach as a core design.
So I'm trying to edit this rss feed with these 2 functions because of the media:content property which I have had no luck accessing directly. the functions I have below work for creating a new value called mediaContent which I can then easily access. The issue is in the rss feed not all objects will have media:content and I want to add a default value for the objects that don't have that property so I have consistency in my objects. Otherwise I end up with undefined on on some of mediaContent in my new object. I wanted to start just added a default value in when media:content is not present in the object but these ||'s are not working as I would have expected. How can I get my else if to punch in a default value if media:content does not exist? I'm probably missing something easy.
function getMediaContent(value) {
for (var i in value) {
if (i === "media:content") {
console.log("MC::", i)
return value[i].$;
} else if (i !== "title" || i !== "link" || i !== "pubDate" || i !== "isoDate" || i !== "guid" || i !== "contentSnippet" || i !== "content") {
debugger;
return "no media content"
}
}
}
function getNewsLinks() {
return newsItems.map(value => ({
value,
mediaContent: getMediaContent(value)
}))
}
SOLUTION (based on accepted answer)
function getMediaContent(value) {
return "media:content" in value ? value["media:content"].$ : "no media content";
}
works perfectly. Thanks!
Since you're just looking to see if a property exists on an object, you can use the in operator:
function getMediaContent(value) {
return "media:content" in value ? value["media:content"].$ : "no media content";
}
That checks if the property exists, and if so, gets the value of its $ property. Otherwise, returns the default value.
I needed something similar and optionally it would work for multi-layered JSON objects. Here is the function I use:
function getFromJSON(obj, ...args) {
for (const arg of args) {
if (!Array.isArray(arg)) {
if (arg in obj) {
obj = obj[arg]
} else {
return `${arg} not found in JSON`;
}
} else {
for (const argOpt of arg) {
if (argOpt in obj) {
obj = obj[argOpt]
break;
}
}
}
}
return obj
}
In addition, you can pass multiple keys in an array if you want to get the value of whichever exists.
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];
}
});
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) })