Polymer - notify that a object.property changed in a array for DOM - polymer

In Polymer 1.*, I have a dom repeat. The obj.property is not updating in the DOM when I mutate array itemsCollection.
I tried 'this.notifyPath(itemCollection.${i}.value)' after 'this.set(itemCollection.${i}.value, existing.value);' but it did not update in the DOM.
Am I supposed to use this.notifySplices instead? And if so, how would I use it after this.set(itemCollection.${i}.value, existing.value);?
_populateAlerts: function(existingValues) {
this.itemCollection.forEach((question, i)=> {
const existing =
existingValues.find(value => value.name === question.name);
this.set(`itemCollection.${i}.picklist_value_id`,
existing.picklist_value_id);
this.set(`itemCollection.${i}.value`, existing.value);
});
},

this.notifyPath is never needed if you use this.set, and should probably only be used if another framework sets the variable.
It's weird code, with cubic looping, and setting subproperties in itemCollection, while looping through said array, through Polymer methods.
Anyway, I wonder if there is an Array reference problem. Where existingValues = itemCollection, so every time existingValues changes, itemCollection is changed as well but in a way that doesn't update the DOM. This means that itemCollection tries to set itself to an already existing value when being set through this.set, hence not updating the DOM through dirty checking.
A simple solution could be to just set itemCollection with a copy of itself.
_populateAlerts: function(existingValues) {
this.itemCollection.forEach((question, i)=> {
const existing =
existingValues.find(value => value.name === question.name);
this.set(`itemCollection.${i}.picklist_value_id`,
existing.picklist_value_id);
this.set(`itemCollection.${i}.value`, existing.value);
});
this.set('itemCollection', JSON.parse( JSON.stringify(this.itemCollection) );
// Alternatively
// const tempArr = JSON.parse( JSON.stringify(this.itemCollection) );
// this.set('itemCollection, []); // override dirty checking, as stated in the documentation
// this.set('itemCollection', tempArr);
},
Another solution could be to create a new array of existingValues, breaking the "reference chain" so existingValues != itemCollection. That is, if the issue is a reference problem.
_populateAlerts: function(existingValues) {
const copiedExistingValues = JSON.parse( JSON.stringify(existingValues) );
this.itemCollection.forEach((question, i)=> {
const existing =
copiedExistingValues.find(value => value.name === question.name);
this.set(`itemCollection.${i}.picklist_value_id`,
existing.picklist_value_id);
this.set(`itemCollection.${i}.value`, existing.value);
});
},
However, if you're only interested in the first occurance, I would create an object of existingArrays to avoid cubic looping while also breaking the reference chain.
_populateAlerts: function(existingValues) {
const existingValuesObj = this._createObjectFrom(existingValues, 'name');
this.itemCollection.forEach((question, i)=> {
this.set(`itemCollection.${i}.picklist_value_id`,
existingValuesObj[question.name].picklist_value_id);
this.set(`itemCollection.${i}.value`, existingValuesObj[question.name].value);
});
},
_createObjectFrom: function (arr, property, overwritePreviousObj) {
var obj = {};
var propertyName = '';
for (var i = 0; i < arr.length; i++) {
propertyName = arr[i][property];
if (overwritePreviousObj) {
obj[propertyName] = arr[i];
} else if (!obj.hasOwnProperty(propertyName) {
obj[propertyName] = arr[i];
}
}
return obj;
},

Related

finding Text with specific format and delete it

I have a big google doc file with over 100 pages(with tables etc) and there is some reference text in that document in multiple locations reference texts are highlighted with the color "grey", I want to have a function that can find those colors/style in the table or paragraph and delete it. So Step 1 is finding it, and then deleting(removing those texts from the document) it in one go.
How we did it in MS Word is, we created custom styles and assign those styles to those "Remarks Text"(in grey) and in VBA we look for text matching the style name, and if it returns true than we delete those texts. As much i know about doc, there is no option to create custom styles.
Here is the code I am trying:-
function removeText()
{
var doc = DocumentApp.getActiveDocument()
var body = doc.getBody()
body.getParagraphs().map(r=> {
if(r.getAttributes().BACKGROUND_COLOR === "#cccccc")
{
//Don't know what to do next, body.removeChild(r.getChild()) not working
}
})
}
Can you guide me on how I can achieve this effectively please.
Thanks
Try this
body.getParagraphs().forEach( r => {
if( r.getAttributes().BACKGROUND_COLOR === "#cccccc" ) {
r.removeFromParent();
}
}
Reference
Paragraph.removeFromParent()
Google Apps Script hasn't a method to find text based on their style attributes, instead we need to get each part and in order to be able to get their attributes. The following example, if the format is applied to the whole paragraph, it is deleted, if not, it uses the regular expression for finding any single character ..
function removeHighlightedText() {
// In case that we want to remove the hightlighting instead of deleting the content
const style = {};
style[DocumentApp.Attribute.BACKGROUND_COLOR] = null;
const backgroundColor = '#cccccc';
const doc = DocumentApp.getActiveDocument();
const searchPattern = '.';
let rangeElement = null;
const rangeElements = [];
doc.getParagraphs().forEach(paragraph => {
if (paragraph.getAttributes().BACKGROUND_COLOR === backgroundColor) {
paragraph.removeFromParent();
// Remove highlighting
// paragraph.setAttributes(style);
} else {
// Collect the rangeElements to be processed
while (rangeElement = paragraph.findText(searchPattern, rangeElement)) {
if (rangeElement != null && rangeElement.getStartOffset() != -1) {
const element = rangeElement.getElement();
if (element.getAttributes(rangeElement.getStartOffset()).BACKGROUND_COLOR === backgroundColor) {
rangeElements.push(rangeElement)
}
}
}
}
});
// Process the collected rangeElements in reverse order (makes things easier when deleting content)
rangeElements.reverse().forEach(r => {
if (r != null && r.getStartOffset() != -1) {
const element = r.getElement();
// Remove text
element.asText().deleteText(r.getStartOffset(), r.getEndOffsetInclusive())
// Remove highlighting
// element.setAttributes(textLocation.getStartOffset(), textLocation.getEndOffsetInclusive(), style);
}
});
}

Google-Apps-Script Convert XML to JSON

I've got an XML string, like this:
'<ALEXA VER="0.9" URL="davidwalsh.name/" HOME="0" AID="="><SD TITLE="A" FLAGS="" HOST="davidwalsh.name"><TITLE TEXT="David Walsh Blog :: PHP, MySQL, CSS, Javascript, MooTools, and Everything Else"/><LINKSIN NUM="1102"/><SPEED TEXT="1421" PCT="51"/></SD><SD><POPULARITY URL="davidwalsh.name/" TEXT="7131"/><REACH RANK="5952"/><RANK DELTA="-1648"/></SD></ALEXA>'
I'd like to convert it into JSON format:
{
"ALEXA":{
"#attributes":{
"VER":"0.9",
"URL":"davidwalsh.name/",
"HOME":"0",
"AID":"="
},
"SD":[
{
"#attributes":{
"TITLE":"A",
"FLAGS":"",
"HOST":"davidwalsh.name"
},
"TITLE":{
"#attributes":{
"TEXT":"David Walsh Blog :: PHP, MySQL, CSS, Javascript, MooTools, and Everything Else"
}
...
I've found lot's of solutions for js, but none of them worked in google-apps-script.
I've also seen this question:
Parsing XML on a Google Apps script
but it does not exactly my case: I'de like to parse any XML into JSON, not just the provided sample.
I've found own solution (in the answer), and not sure it matches all cases.
I thought the solution should be a recursion function. After some research, I've found this great code by David Walsh and was able to adopt it. Here's what I've come to:
// Changes XML to JSON
// Original code: https://davidwalsh.name/convert-xml-json
function xmlToJson_(xml) {
// Create the return object
var obj = {};
// get type
var type = '';
try { type = xml.getType(); } catch(e){}
if (type == 'ELEMENT') {
// do attributes
var attributes = xml.getAttributes();
if (attributes.length > 0) {
obj["#attributes"] = {};
for (var j = 0; j < attributes.length; j++) {
var attribute = attributes[j];
obj["#attributes"][attribute.getName()] = attribute.getValue();
}
}
} else if (type == 'TEXT') {
obj = xml.getValue();
}
// get children
var elements = [];
try { elements = xml.getAllContent(); } catch(e){}
// do children
if (elements.length > 0) {
for(var i = 0; i < elements.length; i++) {
var item = elements[i];
var nodeName = false;
try { nodeName = item.getName(); } catch(e){}
if (nodeName)
{
if (typeof(obj[nodeName]) == "undefined") {
obj[nodeName] = xmlToJson_(item);
} else {
if (typeof(obj[nodeName].push) == "undefined") {
var old = obj[nodeName];
obj[nodeName] = [];
obj[nodeName].push(old);
}
obj[nodeName].push(xmlToJson_(item));
}
}
}
}
return obj;
};
I've posted the sample on GitHub.
Usage:
var xml = XmlService.parse(xmltext);
Logger.log(JSON.stringify(xmlToJson_(xml)));
Reference:
XmlService
The original answer didn't work for me. There may have been a change in the apps script XML API but it wouldn't include the text content of a node without children. Here is the code I wrote that seems to work well.
Note, it outputs in a slightly different fashion than the example you provided. I found that this might be a more consistent format for a broader range of use cases. I also found that including the attributes wasn't necessary for everything I was doing and created clutter, so I've included a version that doesn't parse attributes.
If you include attributes, the output follows this pattern:
{foo:{attributes:{...},content:{...}}
To Include Attributes:
function xmlParse(element) {
/*
* Takes an XML element and returns an object containing its children or text
* If children are present, recursively calls xmlTest() on them
*
* If multiple children share a name, they are added as objects in an array
* If children have unique names, they are simply added as keys
* i.e.
* <foo><bar>one</bar><baz>two</baz></foo> === {foo: {bar: 'one', baz: 'two'}}
* <foo><bar>one</bar><bar>two</bar></foo> === {foo: [{bar: 'one'},{bar: 'two'}]}
*/
let obj = {}
const rootName = element.getName();
// Parse attributes
const attributes = element.getAttributes();
const attributesObj = {};
for(const attribute of attributes) {
attributesObj[attribute.getName()] = attribute.getValue();
}
obj[rootName] = {
attributes: attributesObj,
content: {}
}
const children = element.getChildren();
const childNames = children.map(child => child.getName());
if (children.length === 0) {
// Base case - get text content if no children
obj = {
content: element.getText(),
attributes: attributesObj
}
} else if (new Set(childNames).size !== childNames.length) {
// If nonunique child names, add children as an array
obj[rootName].content = [];
for (const child of children) {
if (child.getChildren().length === 0) {
const childObj = {};
childObj[child.getName()] = xmlParse(child);
obj[rootName].content.push(childObj)
} else {
const childObj = xmlParse(child);
obj[rootName].content.push(childObj)
}
}
} else {
// If unique child names, add children as keys
obj[rootName].content = {};
for (const child of children) {
if (child.getChildren().length === 0) {
obj[rootName].content[child.getName()] = xmlParse(child);
} else {
obj[rootName].content = xmlParse(child);
}
}
}
return obj;
}
Without Attributes:
function xmlParse(element) {
/*
* Takes an XML element and returns an object containing its children or text
* If children are present, recursively calls xmlTest() on them
*
* If multiple children share a name, they are added as objects in an array
* If children have unique names, they are simply added as keys
* i.e.
* <foo><bar>one</bar><baz>two</baz></foo> === {foo: {bar: 'one', baz: 'two'}}
* <foo><bar>one</bar><bar>two</bar></foo> === {foo: [{bar: 'one'},{bar: 'two'}]}
*/
let obj = {}
const rootName = element.getName();
const children = element.getChildren();
const childNames = children.map(child => child.getName());
if (children.length === 0) {
// Base case - get text content if no children
obj = element.getText();
} else if (new Set(childNames).size !== childNames.length) {
// If nonunique child names, add children as an array
obj[rootName] = [];
for (const child of children) {
if (child.getChildren().length === 0) {
const childObj = {};
childObj[child.getName()] = xmlParse(child);
obj[rootName].push(childObj)
} else {
const childObj = xmlParse(child);
obj[rootName].push(childObj)
}
}
} else {
// If unique child names, add children as keys
obj[rootName] = {};
for (const child of children) {
if (child.getChildren().length === 0) {
obj[rootName][child.getName()] = xmlParse(child);
} else {
obj[rootName] = xmlParse(child);
}
}
}
return obj;
}
Usage for both of these:
const xml = XmlService.parse(xmlText);
const rootElement = xml.getRootElement();
const obj = xmlParse(rootElement);
const asJson = JSON.stringify(obj);
Reference:
XMLService

How to get nested deep property value from JSON where key is in a variable?

I want to bind my ng-model with JSON object nested key where my key is in a variable.
var data = {"course":{"sections":{"chapter_index":5}}};
var key = "course['sections']['chapter_index']"
Here I want to get value 5 from data JSON object.
I found the solution to convert "course.sections.chapter_index" to array notation like course['sections']['chapter_index'] this. but don't know how to extract value from data now
<script type="text/javascript">
var BRACKET_REGEXP = /^(.*)((?:\s*\[\s*\d+\s*\]\s*)|(?:\s*\[\s*"(?:[^"\\]|\\.)*"\s*\]\s*)|(?:\s*\[\s*'(?:[^'\\]|\\.)*'\s*\]\s*))(.*)$/;
var APOS_REGEXP = /'/g;
var DOT_REGEXP = /\./g;
var FUNC_REGEXP = /(\([^)]*\))?$/;
var preEval = function (path) {
var m = BRACKET_REGEXP.exec(path);
if (m) {
return (m[1] ? preEval(m[1]) : m[1]) + m[2] + (m[3] ? preEval(m[3]) : m[3]);
} else {
path = path.replace(APOS_REGEXP, '\\\'');
var parts = path.split(DOT_REGEXP);
var preparsed = [parts.shift()]; // first item must be var notation, thus skip
angular.forEach(parts, function (part) {
preparsed.push(part.replace(FUNC_REGEXP, '\']$1'));
});
return preparsed.join('[\'');
}
};
var data = {"course":{"sections":{"chapter_index":5}}};
var obj = preEval('course.sections.chapter_index');
console.log(obj);
</script>
Hope this also help others. I am near to close the solution,but don't know how can I get nested value from JSON.
This may be a good solution too
getDeepnestedValue(object: any, keys: string[]) {
keys.forEach((key: string) => {
object = object[key];
});
return object;
}
var jsonObject = {"address": {"line": {"line1": "","line2": ""}}};
var modelName = "address.line.line1";
var result = getDescendantPropValue(jsonObject, modelName);
function getDescendantPropValue(obj, modelName) {
console.log("modelName " + modelName);
var arr = modelName.split(".");
var val = obj;
for (var i = 0; i < arr.length; i++) {
val = val[arr[i]];
}
console.log("Val values final : " + JSON.stringify(val));
return val;
}
You are trying to combine 'dot notation' and 'bracket notation' to access properties in an object, which is generally not a good idea.
Source: "The Secret Life of Objects"
Here is an alternative.
var stringInput = 'course.sections.chapter_index'
var splitInput = stringInput.split(".")
data[splitInput[1]]][splitInput[2]][splitInput[3]] //5
//OR: Note that if you can construct the right string, you can also do this:
eval("data[splitInput[1]]][splitInput[2]][splitInput[3]]")
Essentially, if you use eval on a string, it'll evaluate a statement.
Now you just need to create the right string! You could use the above method, or tweak your current implementation and simply go
eval("data.course.sections.chapter_index") //5
Source MDN Eval docs.
var data = {
"course": {
"sections": {
"chapter_index": 5
}
}
};
var key = "course['sections']['chapter_index']";
var keys = key.replace(/'|]/g, '').split('[');
for (var i = 0; i < keys.length; i++) {
data = data[keys[i]];
}
console.log(data);
The simplest possible solution that will do what you want:
var data = {"course":{"sections":{"chapter_index":5}}};
var key = "course['sections']['chapter_index']";
with (data) {
var value = eval(key);
}
console.log(value);
//=> 5
Note that you should make sure key comes from a trusted source since it is eval'd.
Using with or eval is considered dangerous, and for a good reason, but this may be one of a few its legitimate use cases.
If you don't want to use eval you can do a one liner reduce:
var data = {"course":{"sections":{"chapter_index":5}}};
var key = "course['sections']['chapter_index']"
key.split(/"|'|\]|\.|\[/).reduce((s,c)=>c===""?s:s&&s[c], data)

Apps-Script: Element equality?

Since this is always false:
doc.getBody().getParagraphs()[0] == doc.getBody().getParagraphs()[0]
How do you test element equality in Apps-Script?
I'm not entirely sure if you are comparing the contents or the position. Let's assume you can compare the contents with getAsText().
To compare the position, it's fairly easy to create an element index (the path at which an element appears in a document).
/**
* create a path in document serial for an element
* #param {Document.Element} element an element
* #param {string} [path=''] the path so far
* #return {string} the path
*/
function pathInDocument (element,path) {
path = path || "" ;
var parent = element.getParent();
if (parent) {
path = pathInDocument( parent , Utilities.formatString ( '%04d.%s', parent.getChildIndex(element),path ));
}
return path;
};
which can be called like this
var path = pathInDocument(element);
and will return something like
0000.0001.0004....etc
If the paths of two elements are the same, they appear in the same position in the document and are therefore the same element.
For an example of using this (in this case to sort bookmarks) see https://ramblings.mcpher.com/google-docs/sorting-bookmarks-in-a-document/
Eventually I came up with a solution for comparing elements.
first of all let me point that this code works and returns true:
var paragraphs = doc.getBody().getParagraphs();
Logger.log(paragraphs[0] == paragraphs[0]);
that is because you are comparing the same element from an array. The way you did in the question, you had two different arrays of paragraphs.
However there are situations when you can not do that, because you may not be comparing paragraphs, or you don't even know what elements you have.
What I do is create a path to the elements all the way up to the body section of the Document. If the paths are equal, you have the same elements.
function bodyPath(el, path) {
path = path? path: [];
var parent = el.getParent();
var index = parent.getChildIndex(el);
path.push(index);
var parentType = parent.getType();
if (parentType !== DocumentApp.ElementType.BODY_SECTION) {
path = bodyPath(parent, path);
} else {
return path;
};
return path;
};
function isSameElement(element1, element2) {
var path1 = bodyPath(element1);
var path2 = bodyPath(element2);
if (path1.length == path2.length) {
for (var i=0; i<path1.length; i++) {
if (path1[i] !== path2[i]) {
return false;
};
};
} else {
return false;
};
return true;
};
This method has proved itself quite fast. Any additions are welcome!
I wrote a recursive solution to avoid string comparison and short-circuit the path walk. Note that you can always convert to loops if you're not happy with the stack dependency of recursion.
function isSameElement(elem1, elem2) {
if (!elem1 && !elem2) return true;
if (!elem1 || !elem2) return false;
var p1=elem1.getParent();
var p2=elem2.getParent();
if (!p1 && !p2) {
return true;
} else if (!p1 || !p2) {
return false;
} else if (p1.getChildIndex(elem1)==p2.getChildIndex(elem2)){
return isSameElement(p1,p2);
} else {
return false;
}
}
I tried it and its always false, for some reason the method returns different objects.
In this case you are comparing the objects and not the content of the objects which indeed are different. You could get the content of the object with .getText(), then this comparison would return true.

Slickgrid - Column Definition with Complex Objects

I have a Java object where the person object contains a displayName object. I have converted it to a JSON object for my JSP. The data looks like the following:
var people = [
{"id":52959,"displayName":{"firstName":"Jim","lastName":"Doe","middleName":"A"},"projectId":50003,"grade":"8","statusCode":"A","gradYear":2016,"buyer":false},
{"id":98765,"displayName":{"firstName":"Jane","lastName":"Doe","middleName":"Z"},"projectId":50003,"grade":"8","statusCode":"A","gradYear":2016,"buyer":true}
];
I want to bind my columns to the name properties that reside within the displayName object, but I am cannot get the column definition to recognize where the data resides. Here is an example of my firstName column definition:
{id: 'displayName.firstName', field: 'displayName.firstName', name: 'First Name',
width: 110, sortable: true, editor: TextCellEditor, formatter: SpaceFormatter,
cssClass: '', maxLength: 250, editable: true}
The view does not render the names although the data is there. Is it possible to bind a column to an object property that resides within another object? If so, what am I doing wrong?
Slickgrid doesn't support this capability by default, but you can workaround it by adding custom value extractor to your options object:
var options = {
dataItemColumnValueExtractor: function(item, columnDef) {
var names = columnDef.field.split('.'),
val = item[names[0]];
for (var i = 1; i < names.length; i++) {
if (val && typeof val == 'object' && names[i] in val) {
val = val[names[i]];
} else {
val = '';
}
}
return val;
}
}
var grid = new Slick.Grid($("#slickgrid"), data, columns, options);
The code is tested with slickgrid 2.0 and is working just fine. Unfortunately seems that slickgrid code is a bit inconsistent and editors don't take into account this option, so this solution is usable only if you will display the data without editing.
I know this is a bit old... but my work around is to do a pre-process on my items. Basically, flattening the model out:
var preProcessItems = function (items) {
var newItems = [];
for (var i = 0; i < items.length; i++) {
var item = items[i];
item['firstName'] = item['displayName']['firstName'];
newItems[i] = item;
}
return newItems;
};
/// when the value is updated on the flat structure, you can edit your deep value here
var fNameFormatter = function (row, cell, value, columnDef, dataContext) {
// datacontext.displayName.firstName = value;
return value ? value : "";
};
This problem seems to be more a of a data modeling issue though.