Javascript - two objects which appear to be identical but aren't - javascript

JS comparative newbie... spent about 3 hours on this so far... Please consider this code if you'd be so kind:
function updateDataFields( mapIDToNewValue )
{
$.each(dataFields, function(key, value)
{
var dataField = $( value );
// var dataField = $(this); // same difference
console.log( dataField );
var iD = dataField.attr('id');
// DOES NOT WORK!!! i.e. going val() on this object does not update the INPUT element on the page!
// dataField.val(mapIDToNewValue[ iD ]);
var thePageElement = $( '#' + iD );
console.log( thePageElement );
console.log( '£ is dataField the same object as thePageElement?' + ( dataField === thePageElement ? 'yes' : 'no, you fool' ));
// DOES WORK:
thePageElement.val( mapIDToNewValue[ iD ] );
});
}
Explanation: dataFields, a file-global variable*, is passed by an outside script which calls this one, using a (fully) global variable which attaches dataFields to itself in piggyback fashion. This data structure, dataFields, consists of all the page elements with the class .dataField. All of these are in fact INPUT HTML elements.
Using the supplied param mapIDToNewValue (from an AJAX call to a dbase), I want to update the respective contents (i.e. text) of these INPUTs. mapIDToNewValue is a map in which the key is the same as the attr( 'id' ) of these dataFields, and the value is the new value which needs to be displayed in the INPUT.
It turns out that thePageElement is not the same object as dataField. When I examine the console output for them both they appear virtually the same... except that dataField, for example, has 0 height, which is enough to chill the soul!
My working hypothesis is that somehow dataFields, when passed from the calling script via this piggyback global variable, somehow turned its contents into "phantom" objects: they have the same id as thePageElement... but are incapable of having any effect on the real page elements themselves!
NB there is no possibility of duplicate ids here, or anything like that.
Any explanation welcome!
* implemented using anonymous function as per here.
later, in response to Patrick Barr's comment:
It's quite involved. Given your use of the word "interesting", I was moved to find out what your rep might be. If you had been a proven JS guru (you may nonetheless be one of course) I'd have been inclined to think that I need to embark on a forensic fault-finding mission.
The context: I'm developing a sort of "MSAccess Forms for MySQL front end" type of a thing, which is sorely lacking out there IMHO. At the moment I'm tackling subforms, and a major aim is to re-use code as much as is humanly possible. I'm rapidly getting out of my depth, not least because of the asynchronicity/concurrency issues that spring up, gorgon-like, at every turn. I'm thinking about how to answer your question in an informative, useful way.
To anyone else: I now realise I have to strip down my project to the bare bones to find out what's going on here... and if still baffled post an SSCCE (as we call it in Java) ... just thought initially that an expert out there might recognise a well-known issue and be able to set me straight.

Having examined things here I now understand what happened (to a degree).
In fact the dataFields Array was obtained from the result of an AJAX call which loaded (or more accurately returned, as the data param in the callback function) an HTML fragment, containing several page elements with class .dataField, which I then gathered by going data.find( '.dataField' ).
But in fact these "page elements", despite having hundreds of properties, just like objects directly "mapping" to page elements, were indeed "phantom" objects: specifically, properties such as clientHeight were 0, seemingly an indication that this is a non-visible object.
It was only when the HTML returned in the AJAX callback was inserted into the document structure (and made visible by setting hidden to false for the encompassing DIV) that this HTML generated real page elements. I then had to select these page elements with class .dataField to get hold of the non-phantom objects (having clientHeight 20 or whatever).
Quite strange, as these initial "phantom" objects did NOT become "real" objects as a consequence of the HTML being added to the document. They remained ... useless (and confusing!).

Related

How to get the text from an Insert event in CKEditor 5?

I am trying to process an insert event from the CKEditor 5.
editor.document.on("change", (eventInfo, type, data) => {
switch (type) {
case "insert":
console.log(type, data);
break;
}
});
When typing in the editor the call back is called. The data argument in the event callback looks like approximately like this:
{
range: {
start: {
root: { ... },
path: [0, 14]
},
end: {
root: { ... },
path: [0, 15]
}
}
}
I don't see a convenient way to figure out what text was actually inserted. I can call data.range.root.getNodeByPath(data.range.start.path); which seems to get me the text node that the text was inserted in. Should we then look at the text node's data field? Should we assume that the last item in the path is always an offset for the start and end of the range and use that to substring? I think the insert event is also fired for inserting non-text type things (e.g. element). How would we know that this is indeed a text type of an event?
Is there something I am missing, or is there just a different way to do this all together?
First, let me describe how you would do it currently (Jan 2018). Please, keep in mind that CKEditor 5 is now undergoing a big refactoring and things will change. At the end, I will describe how it will look like after we finish this refactoring. You may skip to the later part if you don't mind waiting some more time for the refactoring to come to an end.
EDIT: The 1.0.0-beta.1 was released on 15th of March, so you can jump to the "Since March 2018" section.
Until March 2018 (up to 1.0.0-alpha.2)
(If you need to learn more about some class API or an event, please check out the docs.)
Your best bet would be simply to iterate through the inserted range.
let data = '';
for ( const child of data.range.getItems() ) {
if ( child.is( 'textProxy' ) ) {
data += child.data;
}
}
Note, that a TextProxy instance is always returned when you iterate through the range, even if the whole Text node is included in the range.
(You can read more about stringifying a range in CKEditor5 & Angular2 - Getting exact position of caret on click inside editor to grab data.)
Keep in mind, that InsertOperation may insert multiple nodes of a different kind. Mostly, these are just singular characters or elements, but more nodes can be provided. That's why there is no additional data.item or similar property in data. There could be data.items but those would just be same as Array.from( data.range.getItems() ).
Doing changes on Document#change
You haven't mentioned what you want to do with this information afterwards. Getting the range's content is easy, but if you'd like to somehow react to these changes and change the model, then you need to be careful. When the change event is fired, there might be already more changes enqueued. For example:
more changes can come at once from collaboration service,
a different feature might have already reacted to the same change and enqueued its changes which might make the model different.
If you know exactly what set of features you will use, you may just stick with what I proposed. Just remember that any change you do on the model should be done in a Document#enqueueChanges() block (otherwise, it won't be rendered).
If you would like to have this solution bulletproof, you probably would have to do this:
While iterating over data.range children, if you found a TextProxy, create a LiveRange spanning over that node.
Then, in a enqueueChanges() block, iterate through stored LiveRanges and through their children.
Do your logic for each found TextProxy instance.
Remember to destroy() all the LiveRanges afterwards.
As you can see this seems unnecessarily complicated. There are some drawbacks of providing an open and flexible framework, like CKE5, and having in mind all the edge cases is one of them. However it is true, that it could be simpler, that's why we started refactoring in the first place.
Since March 2018 (starting from 1.0.0-beta.1)
The big change coming in 1.0.0-beta.1 will be the introduction of the model.Differ class, revamped events structure and a new API for big part of the model.
First of all, Document#event:change will be fired after all enqueueChange blocks have finished. This means that you won't have to be worried whether another change won't mess up with the change that you are reacting to in your callback.
Also, engine.Document#registerPostFixer() method will be added and you will be able to use it to register callbacks. change event still will be available, but there will be slight differences between change event and registerPostFixer (we will cover them in a guide and docs).
Second, you will have access to a model.Differ instance, which will store a diff between the model state before the first change and the model state at the moment when you want to react to the changes. You will iterate through all diff items and check what exactly and where has changed.
Other than that, a lot of other changes will be conducted in the refactoring and below code snippet will also reflect them. So, in the new world, it will look like this:
editor.document.registerPostFixer( writer => {
const changes = editor.document.differ.getChanges();
for ( const entry of changes ) {
if ( entry.type == 'insert' && entry.name == '$text' ) {
// Use `writer` to do your logic here.
// `entry` also contains `length` and `position` properties.
}
}
} );
In terms of code, it might be a bit more of it than in the first snippet, but:
The first snippet was incomplete.
There are a lot fewer edge cases to think about in the new approach.
The new approach is easier to grasp - you have all the changes available after they are all done, instead of reacting to a change when other changes are queued and may mess up with the model.
The writer is an object that will be used to do changes on the model (instead of Document#batch API). It will have methods like insertText(), insertElement(), remove(), etc.
You can check model.Differ API and tests already as they are already available on master branch. (The internal code will change, but API will stay as it is.)
#Szymon Cofalik's answer went into a direction "How to apply some changes based on a change listener". This made it far more complex than what's needed to get the text from the Document#change event, which boils down to the following snippet:
let data = '';
for ( const child of data.range.getChildren() ) {
if ( child.is( 'textProxy' ) ) {
data += child.data;
}
}
However, reacting to a change is a tricky task and, therefore, make sure to read Szymon's insightful answer if you plan to do so.

What is causing this JavaScript associative array to go out of whack?

I have an associative array that I've verified (via console.log) is originally
this.RegionsChecked = {"US":true,"APAC":true,"Canada":true,"France":true,"Germany":true,"India":true,"Japan":true,"LATAM":true,"MEA":true,"UK":true,"WE":true};
and I have an event handler that attempts to toggle the value of a corresponding item when it is checked/unchecked in the HTML:
Calculator.prototype.UpdateGraphs = function ( $elem )
{
// $elem : input[type="checkbox"] element that was clicked, as a jQuery object
var $parent = $elem.parent(); // Parent of input that was clicked.
// Will either be a th.region or a td#guiderow-[metric_name]
if ( $parent.hasClass('region') )
{
var region_name = $parent.text();
this.RegionsChecked[region_name] = !this.RegionsChecked[region_name]; // toggle region name
console.log(JSON.stringify(this.RegionsChecked)); // TEST
}
// ...
What is strange is that when I change the check value of Canada, for example, the array sometimes turns to
this.RegionsChecked = {"US":true,"APAC":true,"Canada":true,"France":true,"Germany":true,"India":true,"Japan":true,"LATAM":true,"MEA":true,"UK":true,"WE":true,"\n\t\t\t\t\tCanada":true};
(look at the last key, what previously was "Unknown")
instead of the expected
this.RegionsChecked = {"US":true,"APAC":true,"Canada":false,"France":true,"Germany":true,"India":true,"Japan":true,"LATAM":true,"MEA":true,"UK":true,"WE":true};
which is does some of the time, I think (but still have to verify). I'm still trying to figure out how consistently it is happening, but you have any ideas on why?
EDIT: Weird ... It just did it correctly. I can't find any discernable pattern in when it works and doesn't .. I am using Microsoft Sharepoint Designer, which can do strange things ...
In general, it's not a very good idea to depend on the actual text of a DOM node as a key for information, especially if that DOM is dynamically generated by a large unwieldy CMS-like like SharePoint that performs various transforms on your text before it ends up in the DOM (using the Designer adds another fun step to that chain). That is what constructs like data attributes are for.
That said, if you have to do it, you are probably better off trimming your text before using it. I.e.,
var region_name = $.trim($parent.text());

Replace content of a web page with stuff from AJAX response

The idea is to replace the contents of a web page with the same page requested through an ajax call, but only the HTML elements that are different.
Current I have this in my $.ajax success callback:
var replace = function(first, second){
$(first.html() !== second.html())
first.replaceWith(second);
};
replace($('#container'), response.find('#container'));
Which works, but because the content gets always replaced I get to see a "clipping" effect. The ajax request runs multiple times, almost every second, until a certain class is added to the container tag from the ajax response, so the clipping is very annoying.
Basically I want to only replace the elements that have different html, but somehow start from the last level, to prevent replacing of elements that have the same html code.
I made a fiddle example here: http://jsfiddle.net/2u4eB/
So in that markup only the contents of the <b> tag should be replaced, and not the entire div like it is now, because only <b> is different.
Does anyone have any pointers on how could I achieve this?
If you can make some assumptions, then it's not so hard:
Assumption 1: The markup is exactly the same every time
Assumption 2: The only thing which changes is the TEXT inside of certain html tags
You must then know the HTML tags. If you are a consistent person, all of your dynamic data should be wrapped in a similar tag - in your question you mentioned a <b> tag, so lets make a third assumption:
Assumption 3: All dynamic data is wrapped with a <b> tag
Then all you have to do is this:
var replace = function(first, second) {
var oldValues = first.find('b'),
newValues = second.find('b');
oldValues.each(function(i) {
if(oldValues[i].textContent != newValues[i].textContent) {
oldValues[i].textContent = newValues[i].textContent;
}
});
};
replace($('#container'), response.find('#container'));
NOTE: this works because jQuery's find() returns a list of nodes in document order - so Assumption #1 is very important.
I highly recommend using a framework that supports client side binding. (Examples include but are not limited to Knockout, Handlebars, Angular, Underscore) This will give you better results faster than writing low level DOM Manipulation.
Knockout.js and Underscore.js are my favorites, but there are many great options.

Why should y.innerHTML = x.innerHTML; be avoided?

Let's say that we have a DIV x on the page and we want to duplicate ("copy-paste") the contents of that DIV into another DIV y. We could do this like so:
y.innerHTML = x.innerHTML;
or with jQuery:
$(y).html( $(x).html() );
However, it appears that this method is not a good idea, and that it should be avoided.
(1) Why should this method be avoided?
(2) How should this be done instead?
Update:
For the sake of this question let's assume that there are no elements with ID's inside the DIV x.
(Sorry I forgot to cover this case in my original question.)
Conclusion:
I have posted my own answer to this question below (as I originally intended). Now, I also planed to accept my own answer :P, but lonesomeday's answer is so amazing that I have to accept it instead.
This method of "copying" HTML elements from one place to another is the result of a misapprehension of what a browser does. Browsers don't keep an HTML document in memory somewhere and repeatedly modify the HTML based on commands from JavaScript.
When a browser first loads a page, it parses the HTML document and turns it into a DOM structure. This is a relationship of objects following a W3C standard (well, mostly...). The original HTML is from then on completely redundant. The browser doesn't care what the original HTML structure was; its understanding of the web page is the DOM structure that was created from it. If your HTML markup was incorrect/invalid, it will be corrected in some way by the web browser; the DOM structure will not contain the invalid code in any way.
Basically, HTML should be treated as a way of serialising a DOM structure to be passed over the internet or stored in a file locally.
It should not, therefore, be used for modifying an existing web page. The DOM (Document Object Model) has a system for changing the content of a page. This is based on the relationship of nodes, not on the HTML serialisation. So when you add an li to a ul, you have these two options (assuming ul is the list element):
// option 1: innerHTML
ul.innerHTML += '<li>foobar</li>';
// option 2: DOM manipulation
var li = document.createElement('li');
li.appendChild(document.createTextNode('foobar'));
ul.appendChild(li);
Now, the first option looks a lot simpler, but this is only because the browser has abstracted a lot away for you: internally, the browser has to convert the element's children to a string, then append some content, then convert the string back to a DOM structure. The second option corresponds to the browser's native understanding of what's going on.
The second major consideration is to think about the limitations of HTML. When you think about a webpage, not everything relevant to the element can be serialised to HTML. For instance, event handlers bound with x.onclick = function(); or x.addEventListener(...) won't be replicated in innerHTML, so they won't be copied across. So the new elements in y won't have the event listeners. This probably isn't what you want.
So the way around this is to work with the native DOM methods:
for (var i = 0; i < x.childNodes.length; i++) {
y.appendChild(x.childNodes[i].cloneNode(true));
}
Reading the MDN documentation will probably help to understand this way of doing things:
appendChild
cloneNode
childNodes
Now the problem with this (as with option 2 in the code example above) is that it is very verbose, far longer than the innerHTML option would be. This is when you appreciate having a JavaScript library that does this kind of thing for you. For example, in jQuery:
$('#y').html($('#x').clone(true, true).contents());
This is a lot more explicit about what you want to happen. As well as having various performance benefits and preserving event handlers, for example, it also helps you to understand what your code is doing. This is good for your soul as a JavaScript programmer and makes bizarre errors significantly less likely!
You can duplicate IDs which need to be unique.
jQuery's clone method call like, $(element).clone(true); will clone data and event listeners, but ID's will still also be cloned. So to avoid duplicate IDs, don't use IDs for items that need to be cloned.
It should be avoided because then you lose any handlers that may have been on that
DOM element.
You can try to get around that by appending clones of the DOM elements instead of completely overwriting them.
First, let's define the task that has to be accomplished here:
All child nodes of DIV x have to be "copied" (together with all its descendants = deep copy) and "pasted" into the DIV y. If any of the descendants of x has one or more event handlers bound to it, we would presumably want those handlers to continue working on the copies (once they have been placed inside y).
Now, this is not a trivial task. Luckily, the jQuery library (and all the other popular libraries as well I assume) offers a convenient method to accomplish this task: .clone(). Using this method, the solution could be written like so:
$( x ).contents().clone( true ).appendTo( y );
The above solution is the answer to question (2). Now, let's tackle question (1):
This
y.innerHTML = x.innerHTML;
is not just a bad idea - it's an awful one. Let me explain...
The above statement can be broken down into two steps.
The expression x.innerHTML is evaluated,
That return value of that expression (which is a string) is assigned to y.innerHTML.
The nodes that we want to copy (the child nodes of x) are DOM nodes. They are objects that exist in the browser's memory. When evaluating x.innerHTML, the browser serializes (stringifies) those DOM nodes into a string (HTML source code string).
Now, if we needed such a string (to store it in a database, for instance), then this serialization would be understandable. However, we do not need such a string (at least not as an end-product).
In step 2, we are assigning this string to y.innerHTML. The browser evaluates this by parsing the string which results in a set of DOM nodes which are then inserted into DIV y (as child nodes).
So, to sum up:
Child nodes of x --> stringifying --> HTML source code string --> parsing --> Nodes (copies)
So, what's the problem with this approach? Well, DOM nodes may contain properties and functionality which cannot and therefore won't be serialized. The most important such functionality are event handlers that are bound to descendants of x - the copies of those elements won't have any event handlers bound to them. The handlers got lost in the process.
An interesting analogy can be made here:
Digital signal --> D/A conversion --> Analog signal --> A/D conversion --> Digital signal
As you probably know, the resulting digital signal is not an exact copy of the original digital signal - some information got lost in the process.
I hope you understand now why y.innerHTML = x.innerHTML should be avoided.
I wouldn't do it simply because you're asking the browser to re-parse HTML markup that has already been parsed.
I'd be more inclined to use the native cloneNode(true) to duplicate the existing DOM elements.
var node, i=0;
while( node = x.childNodes[ i++ ] ) {
y.appendChild( node.cloneNode( true ) );
}
Well it really depends. There is a possibility of creating duplicate elements with the same ID, which is never a good thing.
jQuery also has methods that can do this for you.

Coffeescript/jQuery Pattern: Caching Data Across Events

I have a list of items that are abstracts, and can be made to expand via Ajax on click. I'v written the following code in Coffeescript:
current_open_row = null
$('li.faq-item').live 'click', (event) ->
$.post("/faqs/update_rows", {old_row_id: current_open_row, new_row_id: $(this).attr('id')}, (data) ->
replace_items data
, 'json')
current_open_row = $(this).attr('id')
This doesn't read like smooth Coffeescript and I find myself thinking, "what could I have done better," but in particular, instantiating the current_open_row variable outside the scope of the click handler feels strange. Not doing this, of course, causes a new instantiation upon entry to the handler, which is always undefined.
Other than refactoring $(this).attr('id') into a variable, is there anything that jumps out as ugly, suboptimal, unreadable, etc., or is this pretty much the way of it?
Thanks!
Well, first off, I think you'll find yourself switching to camelCase eventually... I know that many people strongly prefer the readability_of_underscores, but every library you interact with (including jQuery) uses camelCase. Just something to keep in mind.
Setting that aside, the issue of having to scope variables with = null is a good one. I've tried to persuade Jeremy that there should be a nicer scoping syntax, but he's firmly against it. So I'd suggest moving the variable to an object property. Fortunately, this is jQuery, so there are plenty of places to stick data. How about using the list's .data method? This has an additional advantage: If you want to do this on multiple lists in the future (with one current_open_row in each list), you won't have to change your code one bit. Just add more lists with .faq-item children to the markup.
One more minor point: You give the post call a callback
(data) -> replace_items data
If that's all you're doing, why not just pass replace_items directly? :)
I'd also put each argument to the post function on its own line for readability. Key-value pairs are automatically combined into a single object, even without curly braces. Here's how it all looks:
$('li.faq-item').live 'click', (event) ->
$row = $(this)
$list = $row.parent()
row_id = $row.attr 'id'
$.post(
"/faqs/update_rows",
old_row_id: $list.data('current_open_row'),
new_row_id: row_id,
replace_items,
'json'
)
$list.data 'current_open_row', row_id

Categories

Resources