Manipulating DOM of Multiple dijit Widget Instances - javascript

I have developed a custom dijit Templated Widget. I have to do some DOM manipulation of the children of the containerNode. Everything works fine, except when I have two of the Widgets loaded, and the manipulation of the children of the containerNode seems to affect all of the Widgets of the same type, not just the particular instance of the widget.
I think I have narrowed it down to this part of my code where I "unload" the "children" I am executing the following function:
popPage: function() {
if (this._pagesLoaded) {
var i = this._pagesLoaded - 1;
var y = this.containerNode.children[i];
if (typeof y !== "undefined") {
this.containerNode.removeChild(y);
}
var page = this.pages.pop();
page.unsetPage(); //Internal sub object cleanup
page.destroyRecursive();
this.endPageLoaded--;
this._calcPagesLoaded(); //recalcs this._pagesLoaded
}
},
When I seem to execute this, it seems that the child is removed from the DOM of all Widgets. It just doesn't make any sense, and manually inspecting things in Firebug (like dijit.byId("logScroller62").containerNode.children) shows that the browser thinks everything is seperate and I get two different sets of results for two different instances.

I don't really understand your description, but the error seems to be here:
it seems that the child is removed from the DOM of all Widgets
A DOMNode can't be the child of multiple DOMNodes, nor can a widget be the child of multiple widgets at once.

It appears that I had an initialisation/scoping issue. I was keeping my child objects in an array. I had initialised the array in the property definition of my dojo Object Prototype by doing the following:
pages: [],
But it seems that this causes a scoping issue, as simple change to:
pages: null,
And then adding the initialisation to the postCreate function of the Widget like:
this.pages = [];
Seems to have fixed the problem. I am not sure why something like this causes a scoping issue, though.

Related

Query.swap Backwards compatibility

I'm trying to use a datepicker known as flexcal on my site. It is important for me to use it accurately, to allow selection from a Jewish calendar. My site is based on jQuery v3.3.1, but flexcal was designed for jQuery v2.1.3. I thought it shouldn't cause any problems, but I came across the following error:
Uncaught TypeError: $.swap is not a function
After searching, I found here that this is a method that was intended to be private and was never documented, Anyway at the moment I'm having trouble embedding a widget on my site. Reviewing the source code of the widget reveals that the use of the method looks like this:
return $.swap(
parent,
{display:'inline-block'}, // make it visible but shrink to contents
swapper.bind(this, elem, parent.parentNode)
);
Does anyone know what the purpose of the method is, does it have a parallel alternative, or some other troubleshooting advice?
If you look at the source of jQuery.swap (https://github.com/jquery/jquery/blob/3.4.1/src/css/var/swap.js), you'll see that all it does is temporarily change some CSS attributes of the first argument (parent, in your case), run a calculation, and the restore the original attribute values. You can implement that yourself. It's particularly easy in your case, since the only CSS attribute we're temporarily changing is display:
var old_display = parent.style['display'];
parent.style['display'] = 'inline-block';
var ret = swapper.bind(this, elem, parent.parentNode).apply(parent, []);
parent.style['display'] = old_display;
return ret;

How to avoid memory leaks from jQuery?

jQuery holds references to DOM nodes in its internal cache until I explicitly call $.remove(). If I use a framework such as React which removes DOM nodes on its own (using native DOM element APIs), how do I clean up jQuery's mem cache?
I'm designing a fairly large app using React. For those unfamiliar, React will tear down the DOM and rebuild as needed based on its own "shadow" DOM representation. The part works great with no memory leaks.
Flash forward, we decided to use a jQuery plugin. After React runs through its render loop and builds the DOM, we initialize the plugin which causes jQuery to hold a reference to the corresponding DOM nodes. Later, the user changes tabs on the page and React removes those DOM elements. Unfortunately, because React doesn't use jQuery's $.remove() method, jQuery maintains the reference to those DOM elements and the garbage collector never clears them.
Is there a way I can tell jQuery to flush its cache, or better yet, to not cache at all? I would love to still be able to leverage jQuery for its plugins and cross-browser goodness.
jQuery keeps track of the events and other kind of data via the internal API jQuery._data() however due to this method is internal, it has no official support.
The internal method have the following signature:
jQuery._data( DOMElement, data)
Thus, for example we are going to retrieve all event handlers attached to an Element (via jQuery):
var allEvents = jQuery._data( document, 'events');
This returns and Object containing the event type as key, and an array of event handlers as the value.
Now if you want to get all event handlers of a specific type, we can write as follow:
var clickHandlers = (jQuery._data(document, 'events') || {}).click;
This returns an Array of the "click" event handlers or undefined if the specified event is not bound to the Element.
And why I speak about this method? Because it allow us tracking down the event delegation and the event listeners attached directly, so that we can find out if an event handler is bound several times to the same Element, resulting in memory leaks.
But if you also want a similar functionality without jQuery, you can achieve it with the method getEventHandlers
Take a look at this useful articles:
getEventHandlers
getEventListeners - chrome
getEventListeners - firebug
Debugging
We are going to write a simple function that prints the event handlers and its namespace (if it was specified)
function writeEventHandlers (dom, event) {
jQuery._data(dom, 'events')[event].forEach(function (item) {
console.info(new Array(40).join("-"));
console.log("%cnamespace: " + item.namespace, "color:orangered");
console.log(item.handler.toString());
});
}
Using this function is quite easy:
writeEventHandlers(window, "resize");
I wrote some utilities that allow us keep tracking of the events bound to DOM Elements
Gist: Get all event handlers of an Element
And if you care about performance, you will find useful the following links:
Leaking Memory in Single Page Apps
Writing Fast, Memory-Efficient JavaScript
JavaScript Memory Profiling
I encourage anybody who reads this post, to pay attention to memory allocation in our code, I learn the performance problems ocurrs because of three important things:
Memory
Memory
And yes, Memory.
Events: good practices
It is a good idea create named functions in order to bind and unbind event handlers from DOM elements.
If you are creating DOM elements dynamically, and for example, adding handlers to some events, you could consider using event delegation instead of keep bounding event listeners directly to each element, that way, a parent of dynamically added elements will handle the event. Also if you are using jQuery, you can namespace the events ;)
//the worse!
$(".my-elements").click(function(){});
//not good, anonymous function can not be unbinded
$(".my-element").on("click", function(){});
//better, named function can be unbinded
$(".my-element").on("click", onClickHandler);
$(".my-element").off("click", onClickHandler);
//delegate! it is bound just one time to a parent element
$("#wrapper").on("click.nsFeature", ".my-elements", onClickMyElement);
//ensure the event handler is not bound several times
$("#wrapper")
.off(".nsFeature1 .nsFeature2") //unbind event handlers by namespace
.on("click.nsFeature1", ".show-popup", onShowPopup)
.on("click.nsFeature2", ".show-tooltip", onShowTooltip);
Circular references
Although circular references are not a problem anymore for those browsers that implement the Mark-and-sweep algorithm in their Garbage Collector, it is not a wise practice using that kind of objects if we are interchanging data, because is not possible (for now) serialize to JSON, but in future releases, it will be possible due to a new algorithm that handles that kind of objects. Let's see an example:
var o1 = {};
o2 = {};
o1.a = o2; // o1 references o2
o2.a = o1; // o2 references o1
//now we try to serialize to JSON
var json = JSON.stringify(o1);
//we get:"Uncaught TypeError: Converting circular structure to JSON"
Now let's try with this other example
var freeman = {
name: "Gordon Freeman",
friends: ["Barney Calhoun"]
};
var david = {
name: "David Rivera",
friends: ["John Carmack"]
};
//we create a circular reference
freeman.friends.push(david); //freeman references david
david.friends.push(freeman); //david references freeman
//now we try to serialize to JSON
var json = JSON.stringify(freeman);
//we get:"Uncaught TypeError: Converting circular structure to JSON"
PD: This article is about Cloning Objects in JavaScript. Also this gist contain demos about cloning objects with circular references: clone.js
Reusing objects
Let's follow some of the programming principles, DRY (Don't Repeat Yourself) and instead of creating new objects with similar functionality, we can abstract them in a fancy way. In this example I will going to reuse an event handler (again with events)
//the usual way
function onShowContainer(e) {
$("#container").show();
}
function onHideContainer(e) {
$("#container").hide();
}
$("#btn1").on("click.btn1", onShowContainer);
$("#btn2").on("click.btn2", onHideContainer);
//the good way, passing data to events
function onToggleContainer(e) {
$("#container").toggle(e.data.show);
}
$("#btn1").on("click.btn1", { show: true }, onToggleContainer);
$("#btn2").on("click.btn2", { show: false }, onToggleContainer);
And there are a lot of ways to improve our code, having an impact on performance, and preventing memory leaks. In this post I spoke mainly about events, but there are other ways that can produce memory leaks. I suggest read the articles posted before.
Happy reading and happy coding!
If your plugin exposes a method to programatically destroy one of its instances (i.e. $(element).plugin('destroy')), you should be calling that in the componentWillUnmount lifecycle of your component.
componentWillUnmount is called right before your component is unmounted from the DOM, it's the right place to clean up all external references / event listeners / dom elements your component might have created during its lifetime.
var MyComponent = React.createClass({
componentDidMount() {
$(React.findDOMNode(this.refs.jqueryPluginContainer)).plugin();
},
componentWillUnmount() {
$(React.findDOMNode(this.refs.jqueryPluginContainer)).plugin('destroy');
},
render() {
return <div ref="jqueryPluginContainer" />;
},
});
If your plugin doesn't expose a way to clean up after itself, this article lists a few ways in which you can try to dereference a poorly thought out plugin.
However, if you are creating DOM elements with jQuery from within your React component, then you are doing something seriously wrong: you should almost never need jQuery when working with React, since it already abstracts away all the pain points of working with the DOM.
I'd also be wary of using refs. There are only few use cases where refs are really needed, and those usually involve integration with third-party libraries that manipulate/read from the DOM.
If your component conditionally renders the element affected by your jQuery plugin, you can use callback refs to listen to its mount/unmount events.
The previous code would become:
var MyComponent = React.createClass({
handlePluginContainerLifecycle(component) {
if (component) {
// plugin container mounted
this.pluginContainerNode = React.findDOMNode(component);
$(this.pluginContainerNode).plugin();
} else {
// plugin container unmounted
$(this.pluginContainerNode).plugin('destroy');
}
},
render() {
return (
<div>
{Math.random() > 0.5 &&
// conditionally render the element
<div ref={this.handlePluginContainerLifecycle} />
}
</div>
);
},
});
How about do this when the user exits the tab:
for (x in window) {
delete x;
}
This is much better to do, though:
for (i in $) {
delete i;
}

How to re-evaluate a script that doesn't expose any global in a declarative-style component

I have been writing a reusable script, let's call it a plugin although it's not jQuery, that can be initialised in a declarative way from the HTML. I have extremely simplified it to explain my question so let's say that if a user inserts a tag like:
<span data-color="green"></span>
the script will fire because the attribute data-color is found, changing the color accordingly.
This approach proved very handy because it avoids anyone using the plugin having to initialise it imperatively in their own scripts with something like:
var elem = document.getElementsByTagName('span')[0];
myPlugin.init(elem);
Moreover by going the declarative way I could get away without defining any global (in this case myPlugin), which seemed to be a nice side effect.
I simplified this situation in an example fiddle here, and as you can see a user can avoid writing any js, leaving the configuration to the HTML.
Current situation
The plugin is wrapped in a closure like so:
;(function(){
var changeColor = {
init : function(elem){
var bg = elem.getAttribute('data-color');
elem.style.background = bg;
}
};
// the plugin itslef looks for appropriate HTML elements
var elem = document.querySelectorAll('[data-color]')[0];
// it inits itself as soon as it is evaluated at page load
changeColor.init(elem);
})();
The page loads and the span gets the correct colour, so everything is fine.
The problem
What has come up lately, though, is the need to let the user re-evaluate/re-init the plugin when he needs to.
Let's say that in the first example the HTML is changed dynamically after the page is loaded, becoming:
<span data-color="purple"></span>
With the first fiddle there's no way to re-init the plugin, so I am now testing some solutions.
Possible solutions
Exposing a global
The most obvious is exposing a global. If we go this route the fiddle becomes
http://jsfiddle.net/gleezer/089om9z5/4/
where the only real difference is removing the selection of the element, leaving it to the user:
// we remove this line
// var elem = document.querySelectorAll('[data-color]')[0];
and adding something like (again, i am simplifying for the sake of the question):
window.changeColor = changeColor;
to the above code in order to expose the init method to be called from anywhere.
Although this works I am not satisfied with it. I am really looking for an alternative solution, as I don't want to lose the ease of use of the original approach and I don't want to force anyone using the script adding a new global to their projects.
Events
One solution I have found is leveraging events. By putting something like this in the plugin body:
elem.addEventListener('init', function() {
changeColor.init(elem);
}, false);
anybody will be able to just create an event an fire it accordingly. An example in this case:
var event = new CustomEvent('init', {});
span.dispatchEvent(event);
This would re-init the plugin whenever needed. A working fiddle is to be found here:
http://jsfiddle.net/gleezer/tgztjdzL/1/
The question (finally)
My question is: is there a cleaner/better way of handling this?
How can i let people using this plugin without the need of a global or having to initialise the script themselves the first time? Is event the best way or am I missing some more obvious/better solutions?
You can override Element.setAttribute to trigger your plugin:
var oldSetAttribute = Element.prototype.setAttribute;
Element.prototype.setAttribute = function(name, value) {
oldSetAttribute.call(this, name, value);
if (name === 'data-color') {
changeColor.init(this);
}
}
Pros:
User does not have to explicitly re-initialize the plugin. It will happen automatically when required.
Cons:
This will, of course, only work if the user changes data-color attributes using setAttribute, and not if they create new DOM elements using innerHTML or via some other approach.
Modifying host object prototypes is considered bad practice by many, and for good reasons. Use at your own risk.

setReadOnly causes error when called on instanceReady of CKEditor

I'm trying to set my CKEditor instance to be "readOnly" after the instance has fully loaded but I'm getting a Javascript error: Cannot call method 'setReadOnly' of null. When I dig into it, the error is coming from this line in the ckeditor.js, within the editor.setReadOnly method: this.editable().setReadOnly(a); That means that the editor exists, but the editable method/attribute (on the CKEditor instance) does not.
Below is my code, and I'll explain it a little. My app is a combination of GWT and Backbone. The CKEditor itself is created by the Backbone code but the parent element is in GWT so that's where I initiate the setEnabled action.
private native void setEnabledOnLoad(boolean enabled, String id) /*-{
CKEDITOR.on("instanceReady", function(evt) {
if(evt.editor.name === id) {
Namespace.trigger(Namespace.Events.SET_ENABLED, enabled);
}
});
}-*/;
setEnabled: function(enabled) {
this.editor.setReadOnly(!enabled);
if(enabled){
this.editor.focusManager.focus();
} else {
this.editor.focusManager.blur();
}
}
The Backbone class has a listener for Namespace.Events.SET_ENABLED that triggers setEnabled.
Is there another CKEditor event that I should listen for? There doesn't appear to be an instanceReady event on editable. What am I missing?
EDIT
this.editor is created in the Backbone class render function like this:
this.editor = CKEDITOR.replace(this.$(this.id)[0], config);
The reason I don't add the instanceReady listener right after it's created is because the function setEnabledOnLoad is called in GWT before the instance has been fully initialized. This is a result of having the code in two places. GWT has said "ok, create the instance" but Backbone hasn't finished by the time GWT goes to the next line of code and wants to set it enabled/disabled.
Two years later, but here is my solution. Maybe someone else will find it useful.
As stated above, the event is appearantly triggered before the editable() function is fully set up, and therefore one solution is to simply wait for it to finish before setting it to readonly. This may be an ugly way to do it, but it works.
//Delayed execution - ckeditor must be properly initialized before setting readonly
var retryCount = 0;
var delayedSetReadOnly = function () {
if (CKEDITOR.instances['bodyEditor'].editable() == undefined && retryCount++ < 10) {
setTimeout(delayedSetReadOnly, retryCount * 100); //Wait a while longer each iteration
} else {
CKEDITOR.instances['bodyEditor'].setReadOnly();
}
};
setTimeout(delayedSetReadOnly, 50);
You could try subscribing to instanceReady event this way:
CKEDITOR.instances.editor.on("instanceReady", onInstanceReadyHandler)
However, the editor instance must have been already created by then (inspect CKEDITOR.instances in the debugger).
I'm a bit confused about the difference between editable and editor. Could you show the fragments of your code where this.editor and this.editable get assigned?
[EDITED] I guess I see what's going on. CKEDITOR is a global object, you may think of it as of a class which holds all CKEDITOR instances. Trying to handle events with CKEDITOR.on isn't right, you need to do it on a specific instance (like I've shown above). I assume, "editor" is the ID of your parent element you want to attach a CKEDITOR instance to (please correct me if I'm wrong). I'm not familiar with Backbone, but usually it's done with replace:
var editorInstance = CKEDITOR.replace("editor", { on: {
instanceReady: function(ev) { alert("editor is ready!"); }}});
Here we attach a new instance of CKEDITOR to the editor parent element and subscribe to the instanceReady event at the same time. The returned object editorInstance should provide all the APIs you may need, including setReadOnly. You could also access it through the global CKEDITOR object using the parent element ID, i.e. CKEDITOR.instances.editor. On the other hand, editable is rather a service object available on editor. I can't think of any specific case where you might need to use it directly.
I apologize for never updating this with my solution. I needed to decouple the GWT function further from the CKEditor behavior. So, I added a function in GWT 'setEnabled' that is called from the parent object when it wants to update the enabled state of the CKEditor object.
public void setEnabled(boolean enabled) {
this.enabled = enabled;
toggleCKEditorEnabled(enabled);
}
Then changed the function referenced above 'setEnabledOnLoad' to be 'toggleCKEditorEnabled' which triggers the SET_ENABLED event with the enabled value.
Instead of attaching the listener to the specific instance of CKEditor, I added in to the Backbone MessageEntryView class that is the container of the CKEditor instance. In the initialize function of the MessageEntryView, I added this line
Namespace.on(Namespace.Events.SET_ENABLED, this.setEnabled);
This only works because I have one instance of CKEditor loaded on the screen at any given time. This problem and its solution stopped us from being able to add more CKEditor instances to the page at a time, which is something we discussed before moving on and replacing our whole client with Backbone.

Is saving elements as object properties good practice?

I'm writing a small JavaScript framework for fun and possible implementation similar to backbone(hence the tag). I've started saving elements as object properties, as shown below. I'm not sure if I've seen this done, so I was curious if this causes any issues.
Similarly, If the module depends on other modules I list those at the top of the object in the form of....another object.
I wanted a way to list dependencies ( page elements or JavaScript modules ) and detect any issues up front. This has similar ( not same ) benefits as dependency injection.
This is a specific question on this code review post which explains a bit further on how the framework works.
/*MUserTry
**
**
**
*/
$A.modelAjax({
Name: 'MUserTry',
S: {
DynSma: SDynSma,
DynTwe: SDynTwe,
DynArc: SDynArc,
AniFlipPage: SAniFlipPage,
ClientStorage: SClientStorage
},
E: {
but: $A('#ut_but')[0]
},
J: {
box: $('#ut_box')
},
init: function () {
var pipe = {},
this_hold = this;
this.J.box.draggable();
this.E.but.addEventListener("click", function () {
pipe = $A.definePipe(this_hold.Name);
$A.ajaxMachine(pipe);
}, false);
},
pre: function (pipe) {
pipe.page.email = this.e_button.getAttribute('data-email');
pipe.proceed = true;
},
post: function (pipe) {
this.S.ClientStorage.setAll(pipe.server.smalls);
this.S.DynSma.run(pipe.server.smalls);
this.S.DynArc.run(pipe.server.arcmarks);
this.S.DynTwe.run(pipe.server.tweets);
this.S.AniFlipPage.run('ma');
},
finish: function (pipe) {
$A.log(pipe);
}
});
Ok first off let me offer the obligatory "you'll never get a better wheel by re-inventing the wheel" warning. Whatever you're trying to accomplish, you're almost certainly going to be more successful with it if you use an existing library. And even if there is good cause for you to make your own, it would still benefit you immensely to at least look at existing libraries.
But ... maybe you're just having fun with this project, and looking at other projects isn't fun so you're not doing it. Fair enough.
In any case, if you do look at Backbone, you'll find that this practice is core to the Backbone View class. Every View in Backbone has an "el" and "$el" property, which refer to the raw DOM element for the view and the jQuery-wrapped version of that element.
Backbone has no real performance issues with this because variables/properties in JS are just pointers; in other words, when you set the property of an object to an element, you aren't duplicating that element, you're just adding a reference to it (to put it another way, it's more like you're an A tag rather than a whole new document).
The one time Backbone does have a problem though (and your framework will too) is with stale references. In other words, if you just remove element X from the page, the browser will stop using memory for it (eventually, via garbage collection). But if there is an object out there which points to that element, it won't get garbage-collected, because anything with a reference isn't "garbage".
So, the main thing you have to watch out for is making sure that these objects either:
A) get deleted whenever their elements do, or
B) get rid of their references (eg. delete obj.reference) when their elements get deleted
If you don't do that, things will still probably work just fine ... until you use it on a page with lots of elements being created/deleted, at which point Firefox will start popping up "this script took way too long to run, are you really sure you want to be doing this?" messages.

Categories