Using Meteor and React, I am building an application that has a fabric canvas in the middle, a 'components panel' on the left (a library of objects to add to the canvas), and a 'layers panel' on the right (similar to Photoshop, where each object on the canvas is represented as a layer with specific controls like color, opacity etc.).
So far I have managed to display the Layers Panel by using a Meteor session variable that contains an array for the objects on the canvas.
So each time I add, remove, adjust or make any functional change to any object on the canvas, I manually make the adjustment for the object within the session variable array as well.
This has worked well so far but I now want to implement grouping functionality. While it is easy to do this with fabric.js alone, I am having trouble representing the exact state of the groups on the layers panel. It needs to be displayed in a tree format where nested groups and items are within/under other objects.
I also still need to have the layers panel sortable as well.
Using fabric, we can group a set of objects, group another set of objects, and then group the two sets of objects together. This gets quite complicated and is quickly becoming a nightmare to maintain using the method I described above.
So now I am thinking, it would be much easier to just have the layers panel look at the object data directly from the canvas (reactively). This way, I don't need to maintain a second array and manually match every change that occurs. I could then use something like React Sortable Tree.
So in an attempt to make this happen, I am trying to store a copy of canvas.getObjects() into a Meteor session variable after any change occurs on the canvas.
canvas.on({
'after:render': function() {
var canvasObjects = [];
var objects = canvas.getObjects();
objects.forEach(object=>{
canvasObjects.push(object);
});
Session.set('canvasObjects', canvasObjects);
}
});
I am getting the following error though...
TypeError: callback is not a function
at modules.js?hash=d9804d9fae07a146610fc3c3c610d3e79bf33006:261816
at onLoaded (modules.js?hash=d9804d9fae07a146610fc3c3c610d3e79bf33006:244313)
at modules.js?hash=d9804d9fae07a146610fc3c3c610d3e79bf33006:244337
at modules.js?hash=d9804d9fae07a146610fc3c3c610d3e79bf33006:257179
at onLoaded (modules.js?hash=d9804d9fae07a146610fc3c3c610d3e79bf33006:244357)
at modules.js?hash=d9804d9fae07a146610fc3c3c610d3e79bf33006:244379
at Array.forEach (<anonymous>)
at Object.enlivenPatterns (modules.js?hash=d9804d9fae07a146610fc3c3c610d3e79bf33006:244370)
at Function.fabric.Object._fromObject (modules.js?hash=d9804d9fae07a146610fc3c3c610d3e79bf33006:257171)
at Function.fabric.Path.fromObject (modules.js?hash=d9804d9fae07a146610fc3c3c610d3e79bf33006:261457)
If I replace this line...
Session.set('canvasObjects', canvasObjects);
with this line...
console.log(canvasObjects);
I see the array printed in the console.
Why is it able to log the array but unable to store it into the session variable?
Is this even a good idea? I feel like it will be bad for performance. Would there be any other way to keep the canvas state and the layers panel in sync with all the properties that I want (visibility, opacity, fill, lock, index, group hierarchy etc.).
Thanks in advance.
How it is
I have an array of objects called vm.queued_messages (vm is set to this in my controller), and vm.queued_messages is used in ng-repeat to display a list of div's.
When I make an API call which changes the underlying model in the database, I have the API call return a fresh list of queued messages, and in my controller I set the variable vm.queued_messages to that new value, that fresh list of queued messages.
vm.queued_messages = data; // data is the full list of new message objects
The problem
This "full replacement" of vm.queued_messages worked exactly as I wanted, at first. But what I didn't think about was the fact that even objects in that list which had no changes to any properties were leaving and new objects were taking their place. This made no different to the display because the new objects had identical keys and values, they were technically different objects, and thus the div's were secretly leaving and entering every time. THIS MEANS THERE ARE MANY UNWANTED .ng-enter's AND .ng-leave's OCCURRING, which came to my attention when I tried to apply an animation to these div's when they entered or left. I would expect a single div to do the .ng-leave animation on some click, but suddenly a bunch of them did!
My solution attempt
I made a function softRefreshObjectList which updates the keys and values (as well as any entirely new objects, or now absent objects) of an existing list to match those of a new list, WITHOUT REPLACING THE OBJECTS, AS TO MAINTAIN THEIR IDENTITY. I matched objects between the new list and old list by their _id field.
softRefreshObjectList: function(oldObjs, newObjs) {
var resultingObjList = [];
var oldObjsIdMap = {};
_.each(oldObjs, function(obj) {
oldObjsIdMap[obj._id] = obj;
});
_.each(newObjs, function(newObj) {
var correspondingOldObj = oldObjsIdMap[newObj._id];
if (correspondingOldObj) {
// clear out the old obj and put in the keys/values from the new obj
for (var key in correspondingOldObj) delete correspondingOldObj[key];
for (var key in newObj) correspondingOldObj[key] = newObj[key];
resultingObjList.push(correspondingOldObj);
} else {
resultingObjList.push(newObj);
};
});
return resultingObjList;
}
which works for certain things, but with other ng-repeat lists I get odd behavior, I believe because of the delete's and values of the objects being references to other controller variables. Before continuing down this rabbit hole, I want to make this post in case I'm thinking about this wrong, or there's something I'm missing.
My question
Is there a more appropriate way to handle this case, which would either make it easier to handle, or bypass my issue altogether?
Perhaps a way to signal to Angular that these objects are identified by their _id instead of their reference, so that it doesn't make them leave and enter as long as the _id doesn't change.
Or perhaps a better softRefreshObjectList function which iterates through the objects differently, if there's something fishy about how I'm doing it.
Thanks to Petr's comment, I now know about track by for ng-repeat. It's where you can specify a field in your elements that "identifies" that element, so that angular can know when that element really is leaving or entering. In my case, that field was _id, and adding track by message._id to my ng-repeat (ng-repeat="message in ctrl.queued_messages track by message._id") solved my issue perfectly.
Docs here. Search for track by.
I realize hashes have unique keys already, but my question is aimed at a particular use case (that I feel may be common) regarding hashes.
I have a google map (v3) on my website. I load a bunch of locations from my database, and throw markers on the map at each location. I had an issue in my application where I was seeing duplicate markers for every location. I decided to add a check at the top of my add marker function, to use a hash of previously placed markers (stored by location).
markers = {};
function addMarker(latlng) {
if (!markers.hasOwnProperty(latlng)) {
markers[latlng] = true;
} else {
return;
}
// do other stuff
}
This works, but it feels hacky. Adding the unused boolean value to each hash key/pair just doesn't feel right, because it's technically not used... I just need the key half.
Is there another approach to accomplish the same idea?
As part of a Chrome extension I am searching the entire DOM for elements containing specific words inside each ID/Class.
Currently it looks something like:
"allSelectors": document.querySelectorAll("[id*='example'][class*='example']"),
"collapse": function () {
for (var i = 0; i < MyObject.allSelectors.length; i++) {
// Manipulate MyObject.allSelectors[i] etc
}
},
First, I would like to restructure it somehow (possibly using an array?) so that it is easy to add new selectors as doing it like this:
document.querySelectorAll("[id*='example'][class*='example'][id*='other'][class*='other']")
Isn't easily scaleable or good.
Secondly, I think document.querySelectorAll is very slow - the reason I am using it is because I need to search anywhere in an id/class (hence the use of *=) and cannot use a big external library (such as jQuery), as this is a small file and is being injected user side.
Is there an any solution to either of these problems? because if there are many matches then this slowness might become an issue.
First of all I would totally go for querySelectorAll, I don't think it's that slow, and also it totally fits in a situation like yours. I agree with you that adding a library is overkill for this, and additionally it might not be as beneficial as someone thinks (let's test it here).
Then, again I agree with you that the current solution is not very scalable and that the array is the way to go. Here's a very basic implementation using an array:
// an array of classes and ids to match
var nodes,
searches = [
'[id*="test"]',
'[class*="example"]'
];
// a simple function to return an array of nodes
// that match the content of the array
function getNodes(arr){
return Array.prototype.slice.call(document.querySelectorAll( arr.join() ));
}
nodes = getNodes(searches);
The good thing is that new classes and ids can be easily added or removed from the array, for example, later on you can add:
searches.push('[id*="some"]');
nodes = getNodes(searches); // new nodes will be fetched
Here's a jsbin with a full example code.
I'm using Jorn Zaefferer's Autocomplete plugin on a couple of different pages. In both instances, the order of displayed strings is a little bit messed up.
Example 1: array of strings: basically they are in alphabetical order except for General Knowledge which has been pushed to the top:
General Knowledge,Art and Design,Business Studies,Citizenship,Design and Technology,English,Geography,History,ICT,Mathematics,MFL French,MFL German,MFL Spanish,Music,Physical Education,PSHE,Religious Education,Science,Something Else
Displayed strings:
General Knowledge,Geography,Art and Design,Business Studies,Citizenship,Design and Technology,English,History,ICT,Mathematics,MFL French,MFL German,MFL Spanish,Music,Physical Education,PSHE,Religious Education,Science,Something Else
Note that Geography has been pushed to be the second item, after General Knowledge. The rest are all fine.
Example 2: array of strings: as above but with Cross-curricular instead of General Knowledge.
Cross-curricular,Art and Design,Business Studies,Citizenship,Design and Technology,English,Geography,History,ICT,Mathematics,MFL French,MFL German,MFL Spanish,Music,Physical Education,PSHE,Religious Education,Science,Something Else
Displayed strings:
Cross-curricular,Citizenship,Art and Design,Business Studies,Design and Technology,English,Geography,History,ICT,Mathematics,MFL French,MFL German,MFL Spanish,Music,Physical Education,PSHE,Religious Education,Science,Something Else
Here, Citizenship has been pushed to the number 2 position.
I've experimented a little, and it seems like there's a bug saying "put things that start with the same letter as the first item after the first item and leave the rest alone". Kind of mystifying. I've tried a bit of debugging by triggering alerts inside the autocomplete plugin code but everywhere i can see, it's using the correct order. it seems to be just when its rendered out that it goes wrong.
Any ideas anyone?
max
EDIT - reply to Clint
Thanks for pointing me at the relevant bit of code btw. To make diagnosis simpler i changed the array of values to ["carrot", "apple", "cherry"], which autocomplete re-orders to ["carrot", "cherry", "apple"].
Here's the array that it generates for stMatchSets:
stMatchSets = ({'':[#1={value:"carrot", data:["carrot"], result:"carrot"}, #3={value:"apple", data:["apple"], result:"apple"}, #2={value:"cherry", data:["cherry"], result:"cherry"}], c:[#1#, #2#], a:[#3#]})
So, it's collecting the first letters together into a map, which makes sense as a first-pass matching strategy. What i'd like it to do though, is to use the given array of values, rather than the map, when it comes to populating the displayed list. I can't quite get my head around what's going on with the cache inside the guts of the code (i'm not very experienced with javascript).
SOLVED - i fixed this by hacking the javascript in the plugin.
On line 549 (or 565) we return a variable csub which is an object holding the matching data. Before it's returned, I reorder this so that the order matches the original array of value we were given, ie that we used to build the index in the first place, which i had put into another variable:
csub = csub.sort(function(a,b){ return originalData.indexOf(a.value) > originalData.indexOf(b.value); })
hacky but it works. Personally i think that this behaviour (possibly coded more cleanly) should be the default behaviour of the plugin: ie, the order of results should match the original passed array of possible values. That way the user can sort their array alphabetically if they want (which is trivial) to get the results in alphabetical order, or they can preserve their own 'custom' order.
What I did instead of your solution was to add
if (!q && data[q]){return data[q];}
just above
var csub = [];
found in line ~535.
What this does, if I understood correctly, is to fetch the cached data for when the input is empty, specified in line ~472: stMatchSets[""] = []. Assuming that the cached data for when the input is empty are the first data you provided to begin with, then its all good.
I'm not sure about this autocomplete plugin in particular, but are you sure it's not just trying to give you the best match possible? My autocomplete plugin does some heuristics and does reordering of that nature.
Which brings me to my other answer: there are a million jQuery autocomplete plugins out there. If this one doesn't satisfy you, I'm sure there is another that will.
edit:
In fact, I'm completely certain that's what it's doing. Take a look around line 474:
// loop through the array and create a lookup structure
for ( var i = 0, ol = options.data.length; i < ol; i++ ) {
/* some code */
var firstChar = value.charAt(0).toLowerCase();
// if no lookup array for this character exists, look it up now
if( !stMatchSets[firstChar] )
and so on. So, it's a feature.