I ran into an issue in my current project where I needed to access the array that held an object, as the event listener that an object inside that object needed to access all of the elements of the array. So, I did a little test and discovered for myself that you can, in fact, store the container array that contains an object inside that object and it will actually reference the original array, not a copy.
There are some fairly obvious problems with this in terms of being maintainable code, but I'm curious as to what the potential harm of doing this is. Is there anything I should know regarding this recursive property scenario before I put this in my project? Is there a better way to do what I'm looking to do?
For clarity: the way I'm planning on setting it up is as follows:
The array (linesArray) contains a series of objects of class GraphLine. Each Graphline object (lineObject) contains a Raphael canvas element object (line) for the purposes of formatting that element post drawing. It would also contain a reference property to linesArray (container)
I am planning on having the lineObject.line.mousedown() [which fires on clicking the line] event run a for loop through lineObject.container, aka linesArray, to transform each of the lineObject.line's within it based off of which lineObject.line fired the lineObject.line.mousedown() event.
It would look something like this:
class GraphLine {
id;
line;
lineStr;
container;
constructor(container, /* Bunch of Inputs */) {
this.container = container;
//...
}
Draw() {
this.line = canvas.path(this.lineStr);
this.line.mousedown( function() {
for(let i = 0; i < this.container.length;i i++) {
if(this.container[i].id != this.id) {
//Do A Thing
}
else {
//Do A Different Thing
}
}
});
}
}
var canvas;
$(document).ready(function() {
function Run() {
var container = [];
canvas = new Raphael($('#canvas'), 100, 100);
container.push(new GraphLine(container, /* Bunch of Inputs */));
//...
container[0].Draw();
}
Run();
});
Related
I am wanting to attach into an Elements default property such as innerHTML as a backup under an object that way it does not pollute the Elements properties. so to help give an idea of what I am trying to achieve and what currently works:
Element.prototype._backupPropertyDescriptors = {};
Element.prototype._backupProperties = {};
Element.prototype._backupPropertyDescriptors._innerHTML = Object.getOwnPropertyDescriptor(Element.prototype,'innerHTML');
//This is what I want to do but loses Elements scope:
Object.defineProperty(Element.prototype._backupProperties,'_innerHTML',Element.prototype._backupPropertyDescriptors._innerHTML);
//the scope has changed from element to _backupProperties so this property fails.
//The working version:
Object.defineProperty(Element.prototype,'_innerHTML',Element.prototype._backupPropertyDescriptors._innerHTML);
//the reason for this is I want to be able to manipulate the get and set such as:
Object.defineProperty(Element.prototype._backupProperties,'_innerHTML',{configurable:true,enumerable:true,get:function(){console.log('getting',this.innerHTML);return this.innerHTML},set:function(val){console.log('doing something here before setting');this.innerHTML = val;}});
The problem with this is once it is inside of backup the this statement no longer holds the element...
I know one way to do this would be to use a bind or call but that still poses the how do I get the elements scope... as this during define property is the window..
So for anyone looking to try and do this, here is the solution I found :) might be something better out there, but this does work. requires only 3 properties in the prototype and then all others get put inside a single one.
Element.prototype._backupPropertyDescriptors = {};
Element.prototype._backupProperties = {};
Object.defineProperty(Element.prototype,'_backupProvider',
{
writeable:false,
enumerable:true,
configurable:true,
get:function()
{
var _backupProperties = this._backupProperties;
_backupProperties._Element = this;
return {_Element:this,_backupPropertyDescriptors:this._backupPropertyDescriptors,_backupProperties:_backupProperties};
}
});
//These first ones set up the main provider and property and descriptor holders.
//then just copy a descriptor:
Element.prototype._backupPropertyDescriptors._innerHTML = Object.getOwnPropertyDescriptor(Element.prototype,'innerHTML');
//and assign it to a new property inside the backupProperties:
Object.defineProperty(Element.prototype._backupProvider._backupProperties,'_innerHTML',
{
enumerable:true,
configurable:true,
get:function()
{
return this._Element._backupProvider._backupPropertyDescriptors._innerHTML.get.call(this._Element);
},
set:function(val)
{
console.log('setting html to: ',val);
this._Element._backupProvider._backupPropertyDescriptors._innerHTML.set.call(this._Element,val);
}
});
//and if you wanted to do something really crazy.... like overwrite the original..
Object.defineProperty(Element.prototype,'innerHTML',
{
enumerable:true,
configurable:true,
get:function()
{
return this._backupProvider._backupProperties._innerHTML;
},
set:function(val)
{
console.log('setting html to: ',val);
//do some crazy two way template binding here or something else crazy
this._backupProvider._backupProperties._innerHTML = val;
}
});
that is all.. thanks for the help #Bergi
Let's say I have a setup like this.
var Account = function(data) {
this.data = data;
this.domElement = (function(){ code that generates DOM element that will represent this account })();
this.domElement.objectReference = this;
}
Account.prototype = {
show: function() { this.domElement.classList.remove('hidden'); },
hide: function() { this.domElement.classList.add('hidden') }
}
My question is about the last line: this.domElement.objectReference = this;
It would be a useful thing to have because then I can add event listeners to my DOM element and still get access to the object itself. I can add methods that would affect my DOM element, such as hide() and show(), for instance, without having to resort to modifying visibility of the DOM element using CSS directly.
I tested this code and it works like I want it to, but I'm curious whether this would cause memory leaks or some other unpleasantness or if it's an acceptable thing to do?
Thank you!
Luka
I know this has been answered by #PaulS. already, but I find the answer counter intuitive (returning a DOM element from the Account constructor is not expected) and too DOM-centric, but at the same time the implementation is very simple, so I am not sure what to think ;)
Anyway, I just wanted to show a different way of doing it. You can store Account instances in a map and give them a unique id (perhaps they have one already), then you store that id as a data attribute on the DOM element. Finally you implement a getById function or something similar to retrieve the account instance by id from the listeners.
That's pretty much how jQuery's data works.
Here's an example with delegated events like you wanted from the comments.
DEMO
var Account = (function (accounts, id) {
function Account(data) {
accounts[this._id = ++id] = this;
this.el = createEl.call(this);
}
Account.prototype = {
constructor: Account,
show: function() { this.el.classList.remove('hidden'); },
hide: function() { this.el.classList.add('hidden'); }
};
function createEl() {
var el = this.el = document.createElement('div');
el.className = 'account';
el.innerHTML = el.dataset.accountId = this._id;
return el;
}
Account.getById = function (id) {
return accounts[id];
};
Account.init = function () {
//add delegate listeners
document.addEventListener('click', function (e) {
var target = e.target,
account = Account.getById(target.dataset.accountId);
if (!account) return;
account.hide();
});
};
return Account;
})({}, 0);
//once DOM loaded
Account.init(); //start listening to events
var body = document.body;
body.appendChild(new Account().el);
body.appendChild(new Account().el);
Why not have domElement as a variable, and return it from your function? To keep the reference to your constructed Object (but only where this is as expected), you could do a if (this instanceof Account) domElement.objectReference = this;
You've now saved yourself from circular references and can access both the Node and the Object. Doing it this way around is more helpful if you're expecting to lose the direct reference to your Account instance, but expect to need it when "looking up" the Node it relates to at some later time.
Code as requested
var Account = function (data) {
var domElement; // var it
this.data = data;
domElement = (function(){/* ... */}()); // use var
if (this instanceof Account)
domElement.objectReference = this; // assign `this`
return domElement;
};
// prototype as before
Returned element is now the Node, not the Object; so you'd access the Account instance like this
var domElement = new Account();
domElement.objectReference.show(); // for example
In my opinion there is nothing good about referencing the object inside of the object itself. The main reason for this is complexity and obscurity.
If you would point out how exactly are you using this domElement.objectReference later in the code, I am sure that I or someone else would be able to provide a solution without this reference.
I'm using ThreeJS to develop a web application that displays a list of entities, each with corresponding "View" and "Hide" button; e.g. entityName View Hide. When user clicks View button, following function is called and entity drawn on screen successfully.
function loadOBJFile(objFile){
/* material of OBJ model */
var OBJMaterial = new THREE.MeshPhongMaterial({color: 0x8888ff});
var loader = new THREE.OBJLoader();
loader.load(objFile, function (object){
object.traverse (function (child){
if (child instanceof THREE.Mesh) {
child.material = OBJMaterial;
}
});
object.position.y = 0.1;
scene.add(object);
});
}
function addEntity(object) {
loadOBJFile(object.name);
}
And on clicking Hide button, following function is called:
function removeEntity(object){
scene.remove(object.name);
}
The problem is, entity is not removed from screen once loaded when Hide button is clicked. What can I do to make Hide button to work?
I did small experiment. I added scene.remove(object.name); right after scene.add(object); within addEntity function and as result, when "View" button clicked, no entity drawn (as expected) meaning that scene.remove(object.name); worked just fine within addEntity. But still I'm unable to figure out how to use it in removeEntity(object).
Also, I checked contents of scene.children and it shows: [object Object],[object Object],[object Object],[object Object],[object Object],[object Object]
Complete code: http://devplace.in/~harman/model_display1.php.html
Please ask, if more detail is needed. I tested with rev-59-dev and rev-60 of ThreeJS.
Thanks. :)
I think seeing your usage for addEntity and removeEntity code would be helpful, but my first thought is are you actually setting the object.name? Try in your loader just before scene.add(object); something like this:
object.name = "test_name";
scene.add(object);
What might be happening is the default "name" for an Object3D is "", so when you then call your removeEntity function it fails due to the scene objects name being ""
Also, I notice you pass in object.name to your loader? Is this where your storing the URL to the resource? If so, I would recommend using the Object3D's built in .userData method to store that information and keep the name field for scene identification purposes.
Edit: Response to newly added Code
First thing to note is it's not a great idea to have "/" in your object name, it seems to work fine but you never know if some algorithm will decide to escape that string and break your project.
Second item is now that I've seen your code, its actually straight forward whats going on. Your delete function is trying to delete by name, you need an Object3D to delete. Try this:
function removeEntity(object) {
var selectedObject = scene.getObjectByName(object.name);
scene.remove( selectedObject );
animate();
}
Here you see I lookup your Object3D in the Three.js Scene by passing in your object tag's name attribute.
clearScene: function() {
var objsToRemove = _.rest(scene.children, 1);
_.each(objsToRemove, function( object ) {
scene.remove(object);
});
},
this uses undescore.js to iterrate over all children (except the first) in a scene (it's part of code I use to clear a scene). just make sure you render the scene at least once after deleting, because otherwise the canvas does not change! There is no need for a "special" obj flag or anything like this.
Also you don't delete the object by name, just by the object itself, so calling
scene.remove(object);
instead of scene.remove(object.name);
can be enough
PS: _.each is a function of underscore.js
I came in late but after reading the answers more clarification needs to be said.
The remove function you wrote
function removeEntity(object) {
// scene.remove(); it expects as a parameter a THREE.Object3D and not a string
scene.remove(object.name); // you are giving it a string => it will not remove the object
}
A good practice to remove 3D objects from Three.js scenes
function removeObject3D(object3D) {
if (!(object3D instanceof THREE.Object3D)) return false;
// for better memory management and performance
if (object3D.geometry) object3D.geometry.dispose();
if (object3D.material) {
if (object3D.material instanceof Array) {
// for better memory management and performance
object3D.material.forEach(material => material.dispose());
} else {
// for better memory management and performance
object3D.material.dispose();
}
}
object3D.removeFromParent(); // the parent might be the scene or another Object3D, but it is sure to be removed this way
return true;
}
If your element is not directly on you scene go back to Parent to remove it
function removeEntity(object) {
var selectedObject = scene.getObjectByName(object.name);
selectedObject.parent.remove( selectedObject );
}
THIS WORKS GREAT - I tested it
so, please SET NAME for every object
give the name to the object upon creation
mesh.name = 'nameMeshObject';
and use this if you have to delete an object
delete3DOBJ('nameMeshObject');
function delete3DOBJ(objName){
var selectedObject = scene.getObjectByName(objName);
scene.remove( selectedObject );
animate();
}
open a new scene , add object
delete an object and create new
I started to save this as a function, and call it as needed for whatever reactions require it:
function Remove(){
while(scene.children.length > 0){
scene.remove(scene.children[0]);
}
}
Now you can call the Remove(); function where appropriate.
When you use : scene.remove(object);
The object is removed from the scene, but the collision with it is still enabled !
To remove also the collsion with the object, you can use that (for an array) :
objectsArray.splice(i, 1);
Example :
for (var i = 0; i < objectsArray.length; i++) {
//::: each object ::://
var object = objectsArray[i];
//::: remove all objects from the scene ::://
scene.remove(object);
//::: remove all objects from the array ::://
objectsArray.splice(i, 1);
}
I improve Ibrahim code for removeObject3D, added some checks for geometry or material
removeObject3D(object) {
if (!(object instanceof THREE.Object3D)) return false;
// for better memory management and performance
if (object.geometry) {
object.geometry.dispose();
}
if (object.material) {
if (object.material instanceof Array) {
// for better memory management and performance
object.material.forEach(material => material.dispose());
} else {
// for better memory management and performance
object.material.dispose();
}
}
if (object.parent) {
object.parent.remove(object);
}
// the parent might be the scene or another Object3D, but it is sure to be removed this way
return true;
}
this example might give you a different approach . I was trying to implement a similar feature in my project also with scene.remove(mesh). However disposal of geometry and material attributes of mesh worked for me!
source
Use
scene.remove(Object)
The object you want to remove from the scene
I had The same problem like you have. I try this code and it works just fine:
When you create your object put this object.is_ob = true
function loadOBJFile(objFile){
/* material of OBJ model */
var OBJMaterial = new THREE.MeshPhongMaterial({color: 0x8888ff});
var loader = new THREE.OBJLoader();
loader.load(objFile, function (object){
object.traverse (function (child){
if (child instanceof THREE.Mesh) {
child.material = OBJMaterial;
}
});
object.position.y = 0.1;
// add this code
object.is_ob = true;
scene.add(object);
});
}
function addEntity(object) {
loadOBJFile(object.name);
}
And then then you delete your object try this code:
function removeEntity(object){
var obj, i;
for ( i = scene.children.length - 1; i >= 0 ; i -- ) {
obj = scene.children[ i ];
if ( obj.is_ob) {
scene.remove(obj);
}
}
}
Try that and tell me if that works, it seems that three js doesn't recognize the object after added to the scene. But with this trick it works.
You can use this
function removeEntity(object) {
var scene = document.querySelectorAll("scene"); //clear the objects from the scene
for (var i = 0; i < scene.length; i++) { //loop through to get all object in the scene
var scene =document.getElementById("scene");
scene.removeChild(scene.childNodes[0]); //remove all specified objects
}
I am currently running into the following issue, which I'd like to solve more elegantly:
My script works as follows:
Takes an element
Puts the element into a container (defined as var container = $('<div></div>') by using .append()
Keeps track of how far the container is 'filled'
If the container is full, clone a new container and continue there
Repeat this until every element is processed
Right now, this requires me to keep track of a 'fill' (and a 'max') variable to determine how far the container has been filled. So each time I do an append(), I have to increment these counters.
Now, what to me would be more elegant is making the container object smart, and enabling it to 'hook' into the append() event: whenever something is appended, the container object itself executes some code (incrementing its own counter, deciding if it is full, and if it is, returning a new, empty container).
I thought of solving it this way, by creating a function that returns a container:
var container = {
template : $('<div class="container"></div>'),
containers : [],
get : function (i) {
if (!this.containers[i]) {
this.containers[i] = this.template.clone()
.addClass('container-'+i)
.data('max', 500); //this determines the maximum (px) the container can hold
}
return this.containers[i];
}
};
This works, as I can now iterate over all the elements, and call container.get(i).append(element) for each one (while keeping count of height seperately and comparing that to container().get(i).data().max) and later in the script, when I need the output, I can return the container.containers object.
But I can't get it to work having the container.get function to 'watch' for an append() and act on it. I know this is not the way jQuery is meant to work, but I am sure there is another way of doing it, other than keeping local counters in the element iterator.
One other thing I tried is trying to set .on($.append, function() { //do stuff }); on the container, but that was a pipe dream..
I hope I have explained everything clearly, and would love to know if someone has a solution to this.
See this fiddle for a 'working' example (I highly doubt my programming skills)
Maybe you need something like this:
(function($)
{
var oldappend = $.fn.append;
var count = 0;
$.fn.newAppend = function()
{
var ret = oldappend.apply(this, arguments);
//your logic goes here
// count++;
return ret;
};
})(jQuery);
Or you need to store the count variable per container:
(function($)
{
var oldappend = $.fn.append;
$.fn.newAppend = function()
{
var ret = oldappend.apply(this, arguments);
//your logic goes here
if (!this.count){
this.count = 0;
}
this.count++;
return ret;
};
})(jQuery);
Use it:
$('<div class="container"></div>').newAppend(yourElement);
We are using YUI's onclick events, but we create and delete dome nodes rapidly and this leads to memory leak.
Consider this example code below, We have many 3 nested divs many times. The top and the bottom div have YUI onclick events attached. What is the proper way to get rid of those dom elements and not leak memory:
I'm really out of any ideas. As you can see I tried implementing our own destroy function. Actually destroy works and does not leak, but it is slow.
The destroy2 function is 'copy' of the YUI destroy function where we used to debug what is the problem. It looks like the recursive clean up of YUI can not find the child nodes in the _instances dictionary
<!DOCTYPE html5>
<html>
<head>
<script src="http://yui.yahooapis.com/3.4.1/build/yui/yui-min.js"></script>
</head>
<body>
<div id="main">hi there</div>
<script>
YUI().use("node", "event", function(Y) {
window.Y = Y;
function destroy(node) {
(new Y.Node(node)).destroy();
var children = node.children;
for (var i = 0; i<children.length; i++) {
destroy(children[i]);
}
}
function destroy2(node, recursive) {
var UID = Y.config.doc.uniqueID ? 'uniqueID' : '_yuid';
// alert(1);
if (recursive) {
var all = node.all("*");
// alert(all);
Y.NodeList.each(all, function(n) {
instance = Y.Node._instances[n[UID]];
// alert(instance);
if (instance) {
destroy2(instance);
}
});
}
node._node = null;
node._stateProxy = null;
delete Y.Node._instances[node._yuid];
// node.destroy();
}
var main = new Y.Node("#main");
var divs = [];
var iter = 0;
Y.later(10, window, function() {
iter ++ ;
var i;
for (i=0; i<divs.length; i++) {
var d = divs[i];
d.parentNode.removeChild(d);
// (new Y.Node(d)).destroy(true);
//destroy(d);
//destroy2(new Y.Node(d), true);
(new Y.Node(d)).destroy(true);
}
divs = [];
for (i=0; i<1000; i++) {
var d = document.createElement("div");
var i1;
var i2;
d.appendChild(i1=document.createElement("div"));
i1.appendChild(document.createTextNode('inner 1'));
i1.appendChild(i2=document.createElement("div"));
i2.appendChild(document.createTextNode('inner 2'));
Y.on("click", function() {
alert("inner click")
}, i2);
// try to tell YUI to make Node elements
Y.Node.one(d);
Y.Node.one(i1);
Y.Node.one(i2);
// new Y.Node(d);
// new Y.Node(i1);
// new Y.Node(i2);
d.appendChild(document.createTextNode("this is div " + iter + " " + i));
Y.on("click", function(){ alert("you clicked me");}, d);
main.appendChild(d);
//divs.push(i2);
divs.push(d);
}
}, null, true);
})
</script>
</body>
</html>
I'm not sure what you're trying to accomplish here, but a few things stand out in the included code:
var Y = YUI().use(…) -- use() returns the YUI instance. No need to assign window.Y = Y;
Use Y.one(el) instead of new Y.Node(el) or Y.Node.one(el)
Use event delegation rather than subscribing to the click event on each inner div
Use Y.Node.create('<div><div>inner 1<div>inner2</div></div></div>'). You probably don't need Node instances for each div
You're likely creating more work for yourself over the long term by mixing raw DOM interaction and YUI Nodes. If you're using YUI, use YUI's APIs.
The most important of those points are #3 and #4. If you use Node.create (or append, insert, prepend, etc), the markup passed in won't have Nodes created for each element, only the outer most element. If you use event delegation, you won't need individual Nodes, which means you can add your div structures and immediately call node.destroy() (notice not passing true because the inner markup doesn't have nodes needing purging). node.destroy() will purge event listeners, which you don't have because you're using event delegation, and remove the Node from the _instances dictionary to free up memory before any user interaction. If a user clicks on one of the nodes, the event will be caught by the delegate handler and a Node will be created for the e.target element. You're free to call this.destroy() inside your event handler to re-purge the target's Node.
YMMV, considering your code snippet doesn't reflect a real use case. You can also stop into #yui on freenode to get some help or to walk through the issue.
Here is what I ended up doing to avoid the memory leaks:
var destroy = function(dom) {
var ynode = new Y.Node(dom);
ynode.purge(true);
ynode.destroy(true);
}
I have to say I'm very disapointed by the YUI documentation that says this for the YUI destroy function.
With true as a first argument, it works like node.purge(true). The
destroy method does more than detaching event subscribers. Read the
API docs for details.
http://yuilibrary.com/yui/docs/event/
I kind of consider this bug in YUI because it does not call recursively purge when you call destroy recursively. Also it looks like the above destroy function is very slow on Firefox 8 (maybe other versions too) I did write my own recursion down the dom tree and called purge
and destroy without true.