Cannot reinitalize Sortable after ajax content update - javascript

I'm using Sortable to organise lists inside of parent groupings, which themselves are also sortable, similar to the multi example on their demo page, but with text. This works fine and uses code along the lines of:
var globObj = {};
function prepSortCats() {
globObj.subCatsGroup = [];
// Changing parent group order
globObj.sortyMainCats = Sortable.create(catscontainer, {
// options here omitted from example
onUpdate: function( /**Event*/ evt) {
// Send order to database. Works fine.
}
});
// Changing sub list order
globObj.subCatsGroup.forEach.call(document.getElementById('catscontainer').getElementsByClassName('subcatlist'), function(el) {
var sortySubCats = Sortable.create(el, {
// options here from example
onUpdate: function( /**Event*/ evt) {
// Send order to database. Works fine.
}
});
});
}
Which is called when the page loads using:
$(document).ready(function () {
// Sortable
prepSortCats();
});
All good so far. However, the user can introduce new elements into any of the lists (sub or parent). In brief, any new elements added by the user are first added to the database, then the relevant section of the page is refreshed using ajax to pull the updated content from the database and display that. The user sees their newly added items added to one of the existing lists. Ajax call is as follows:
function refreshCategories() {
var loadUrl = "page that pulls lists from database and formats it";
$("#catscontainer")
.html(ajax_load)
.load(loadUrl);
return false;
};
This works fine too. Except, Sortable no longer works. I can't drag any lists. My first thought was to destroy the existing Sortable instances and reinitialize them. Right after I called refreshCategories() I call the following:
if(globObj.sortyMainCats.length !== 0) {
globObj.sortyMainCats.destroy();
}
if(globObj.subCatsGroup.length !== 0) {
var i;
for (i = globObj.subCatsGroup.length - 1; i >= 0; i -= 1) {
globObj.subCatsGroup[i].destroy();
globObj.subCatsGroup.splice(i, 1);
}
}
prepSortCats();
But Sortable still has no effect. I introduced the global object (although controversial) so that I could target the Sortable instances outside their scope but I appear to have overlooked something. Any ideas? Apologies for not providing a working example. As I make various ajax calls to a server, I don't think this is possible here.
Update
I'm clearly misunderstanding some action that's taking place. Well, I should preface that by saying I missed that I could still organise the group/parent lists after reloading a section of the page by ajax with refreshCategories(). This is very much a secondary action to being able to sort the sub lists, which is what I noticed was broken and remains so.
But it did point out that although the entirety of $("#catscontainer") was being replaced with a refreshed version of the lists (and that's all it contains, list elements), Sortable still had some sort of instance running on it. I was under the understanding that it was somehow tied to the elements that were removed. Now I'm a bit more lost on how to get Sortable to either: (a) just start from scratch on the page, performing prepSortCats() as if it was a fresh page load and removing any previous Sortable instance, or (b) getting the remaining Sortable instance, after the ajax call to recognise the added elements.
Update 2
Making some progress.
Through trial and error I've found that right after calling refreshCategories(), calling globObj.sortyMainCats.destroy() is preventing even the group lists from being ordered. Then if I call prepSortCats() after this, I can move them again. But not the sub lists.
This isn't conclusive but it looks like I'm successfully destroying and reinitializing Sortable, which was my goal, but something about the ajax loaded elements isn't working with Sortable.

I was looking for the answer in the wrong place, being sure it was an issue with ajax loaded content and the dom having some inconsistencies with what Sortable expected.
Turns out it was an asynchronous problem. Or, to put it simpler, the section of the page being loaded by ajax wasn't quite ready when Sortable was being asked to be reinitalized.
For anyone having the same trouble, I changed:
$("#catscontainer")
.html(ajax_load)
.load(loadUrl);
to
$("#catscontainer")
.html(ajax_load)
.load(loadUrl, function() {
reinitSortable();
});
where reinitSortable() is just a function that fires off the destroy and prepSortCats() functions similar to how they're displayed above.

Related

Possible to simulate select using setSelection?

I have a project where I am using the vis.js timeline module as a type of image carousel where I have a start and an end time, plot the events on the timeline, and cycle through them automatically and show the image attached to each event in another container.
I already have this working and use something similar to the following to accomplish this, except one part:
var container = document.getElementById('visualization');
var data = [1,2,3,4,5];
var timeline = new vis.Timeline(container, data);
timeline.on('select', function (properties) {
// do some cool stuff
}
var i = 0;
(function timelapseEvents(i) {
setTimeout(function(){
timeline.setSelection(data[i], {focus: true, animation:true});
if (i < data.length - 1) {
timelapseEvents(i+1);
}
}, 2000);
})(i)
The timeline.setSelection() part above works, the timeline event is selected and focused on. However, the "select" event is NOT triggered. This is verified as working as expected in the documentation (under Events > timeline.select) where it says: Not fired when the method timeline.setSelection() is executed.
So my question is, does anyone know how to use the timeline.setSelection() method and actually trigger the select event? Seems unintuitive to me to invoke the timeline.setSelection()method and not actually trigger the select event.
Spent a few hours on this and came up short. I ended up just taking the code I had in my timeline.on('select', function (properties) { block and turning it into a function and calling it after the timeline.setSelection() call.
Basically, I didn't fix the issue but worked around it. Will keep an eye on this in case anyone actually is able to figure out how to add the select() event to the setSelection() method.

Two methods of looping through Ajax requests for jQuery Then When - which to use?

I've got a deceptively simple blog project in the works, where I'm trying to bring together Isotope Jquery (for layout/filtering/sorting), Infinite Scroll, and dynamic loading of all blog excerpts via Ajax (so filtering and sorting is applied to all excerpts before the user scrolls down the page (after which time they're loaded into the dom and then accessible)).
This question primarily deals with getting the blog post excerpt data via Ajax, to then be passed into Isotope filtering code. I'm not sure of the best way to do this, but am currently trying to loop through each page (of blog posts excerpts) with an ajax request and then access the data as one whole.
I've come across two different methods to loop through the ajax requests, each using then when jquery statements. The first is using the method give in this SO answer, the other is simply putting the entire then when statement inside of an $.each statement.
Method 1:
var pageCount = 15;
var pageCountArray = [];
for (var i = 1; i != pageCount; ++i) pageCountArray.push(i);
var deferreds = [];
$(pageCountArray).each(function() {
var pageNumber = this;
deferreds.push(
$.get('/page/' + pageNumber)
)
$.when.apply($, deferreds)
.then(function(data){
console.log(data);
// this outputs data as a string from the first page, then a list of objects
console.log(typeof(data));
// string
// 13 - object
});
});
Slight aside: Any ideas as to why this is outputting one string and then objects?
Method 2:
var pageCount = 15;
var pageCountArray = [];
for (var i = 1; i != pageCount; ++i) pageCountArray.push(i);
$(pageCountArray).each(function(data) {
var pageNumber = this;
$.when(
$.get('/page/' + pageNumber)
).then(function() {
console.log(data);
// this outputs 14 strings of data
console.log(typeof(data));
// 14 - string
})
});
I haven't yet figured out how to incorporate the Ajaxed data into my Isotope filter function, but I think I'll need to parse this into HTML first. Still getting my footing with javascript... in this case is one of these data types (objects vs strings) easier to parse into HTML? I suppose that's the key to my answer?
Much obliged for insights.
PS: Bonus points for anyone who might know of a better way to achieve this in a different way that somehow dovetails into Isotope/Infinite Scroll nicely (perhaps in a way that's more intended to play nice with these plugins... I've been unsuccessful in my searching).
PPS: The second method feels much cleaner... anyone know of a reason that it's not a good approach (using when then inside of an .each loop)?
Wow, this is a largely scoped question no wonder there aren't any responses. This is a massive question so I will do my very best to help. I have created many sites that include the sort/filtering of Isotope while using AJAX preload's with infinite scrolling so here is one of the simplest examples I have already written out...
First I must mention that this whole thing works much better with David DeSandro's ImagesLoaded plugin. This is mostly because it allows you to place a callback function (function to be executed once an event occurs) attached to the loading event of the final image in a given container. Wow that was wordy. How to put that better... It basically asks the container, are you done loading yet? No? How about now? You're loaded? Ok please do this function now then...
With that being implemented I would start with this code in my onLoad event like so...
$(function() {
extendJQ_PreLoad(); //I Will Get To This Function In A Min
//Use ImagesLoaded Plugin To Control Load Time Sync
$(container).imagesLoaded(function() {
cont.isotope({
itemSelector: ".box", //This is the class I use on all my images to sort
layoutMode: "masonry",
isOriginLeft: true,
isFitWidth: true,
filter: "*",
masonry: {
columnWidth: ".box"
}
});
preLoadNextImgSet(); //I Will Get To This Function In A Min
});
});
Ok so let's break this down. The ImagesLoaded plugin stops the Isotope plugin instantiation from happening before there are images present to sort/filter/load and/or handle. This is step 1. Step 2 would be to then start looking at the actual isotope plugin instantiation. I am telling it to use Masonry plugin as its layout style and then I am passing in an object literal with options under the array key 'masonry'. The array key here that is named masonry is the same as any instantiation you would have normally done in the past with the stand alone Masonry plugin (non-isotope or isotope-2).
Step 3 to look at here would be my beginning call to extendJQ_PreLoad();. This function is the function I wrote to let JQuery know that I need to extend it's core functionality in order to capacitate preloading any images I give it, as an array. Like so...
function extendJQ_PreLoad() {
$.preloadImages = function(args) {
for (var i = 0; i < args.length; i++) {
$("<img />").attr("src", args[i]);
}
}
} //end function
This is just a simple iterator and nothing fancy, it allows the images to be preloaded by using a neat trick associated with the DOM. If you load images in this way it loads then into memory but not into the DOM meaning it is loaded and hidden. Once you then insert this image anywhere it will insert very quickly as it is now loaded in cache and awaiting placement. You can view more about this here.
Finally the last to look at is my call to my preload function. This is a very simple call to a php file that simply goes and looks for the next set of images in order, if there is any to find. If it gets some images then it begins adding it to a temporary div in memory (again not on the DOM to be seen) and is now setup for simple DOM traversal. Let's view the function to dissect its functionality...
function preLoadNextImgSet() {
$.post('AjaxController/ajaxPreload_Gallery.php', {currStart: start, currSize: loadSize}, function(data) {
if(data!="") {
var y = $(document.createElement("div")).append(data).find("a"),
found = [];
y.each(function() {
found[found.length] = "img/gallery/" + $(this).text();
});
$.preloadImages(found);
}
});
} //end function
In this example I have two global variables living in my browser window from JavaScript that I would have declared. A start and a loadSize variable. The start variable represents the current place in our list of images that we currently are at and the loadSize variable sets a limit on how many images to preload each time.
Now that the variables are set and sent in to the PHP file via the $.post function, we can use the PHP file to find the appropriate images in order and have them loaded into memory awaiting usage. Whatever is returned here to the y variable gets iterated over by the each function and then preloaded. Once this functions scope is exited the imaginary div will be deleted and sent to garbage as it is not used simple iterated over.
Ok, now. Its been a journey but we are almost ready to begin the final method here. Let's first go back and look at what the first imagesLoaded call was doing now that we know the new functionality added in these functions. The imagesLoaded call in the DOM-Ready event has a call in its very bottom piece that preloads the images.... why? This is because once the page loads and the initial images are loaded into the isotope container, we need the page to now use this idle time to begin already loading the next set. So in other words once the images are placed and sorted and happy to just sit there, the next loadSize amount of images will be loaded and waiting for you to place them.
Now for the final function. This function is a generic function thats sole purpose is to load in the current preloaded images into the DOM officially and then to ask for the next set to be loaded. However what on earth would be calling this function? This is where the lazyloading or infinitescroll becomes useful to us. Somewhere in your page you need to add this function in...
$(window).scroll(function(){
scrollTop = $(window).scrollTop(),
windowHeight = $(window).height(),
docuHeight = $(document).height();
//AJAX Data Pull
if(((scrollTop + windowHeight)+35) >= docuHeight){
getNextImages();
}
});
This function is the magic function that allows the infinitescroll effect to occur. I have added 35 pixels or so of padding (the +35 randomly in my code) because sometimes you want it to load close to the end of the page but not quite the actual end of the page.
Ok so now that this is setup when we reach the end of the page this function will want to get all of the next images generically like we had mentioned. The function of mine looks like this...
function getNextImages() {
cont = $(container);
$.post('AjaxController/ajaxPortfolio_Gallery.php', {currStart: start, currSize: loadSize}, function(data) {
if(data!="") {
//Append New Photos Inside <a> Element Tag
var y = $(document.createElement("div")).append(data).find("a");
cont.append(y);
//Fix Image Layouts
cont.imagesLoaded(function() {
//Feed Isotope Layout The New Items
cont.isotope("appended", y);
cont.find("a").css({"opacity":"1"});
});
} else { unFilled = false; }
});
}
I have included the unFilled variable simply so that there is a flag that can be set when you have reached the end of the images. You don't want it to keep trying to load forever if there are no images left to show.
Ok, so. This is a lot of information so I will try to keep answering as much as possible.

How to animate unchanged ng-repeat with AngularJS

I have a template that looks like this:
<p ng-repeat="item in myobj.items" class="toAnimate">{{item}}</p>
and I would like to use the animate module do a jQueryUI addClass/removeClass animation on the element using the JavaScript method described in the docs:
ngModule.animation('.toAnimate', function() {
return {
enter: function(element) {
element.addClass('pulse').removeClass('pulse', 2000);
}
};
});
This works beautifully, but the problem is that, since I want to use the p.toAnimate element to display status messages, it will not change the content according to angular.
To break it down a little further, say I have a name field. When I click Save the message Name was saved successfully. is displayed. Now if I modify the name and click save again, assuming the save was successful, the message should be re-displayed to give the user feedback of the newly edited name. The pulse does not happen, however, because the items in myobj.items didn't technically change.
I realize that I could remove the item after a period of time (and that is probably the route I will take to implement the real solution), but I'm still interested to see if this sort of thing can be done using AngularJS.
What I want to do is register with angular that the message should be treated as new even though it is not. Is there any way to do this?
A fiddle to go along with this: http://jsfiddle.net/Jw3AT/
UPDATE
There is a problem with the $scope.$$phase approach in my answer, so I'm still looking for the "right" way to do this. Basically, $scope.$$phase is always returning $digest, which causes the conditional to fail. Removing the conditional gives the correct result in the interface, but throws a $rootScope:inprog.
One solution I found is to add a $apply in the middle of the controller function:
$scope.updateThingy = function () {
$scope.myobj.items = [];
if (!$scope.$$phase) {
$scope.$apply();
}
$scope.myobj.items = ['Your name was updated.'];
};
Updated fiddle: http://jsfiddle.net/744Rv/
May not be the best way, but it's an answer.

Dynamically Binding Items in ListVIew

In my Windows 8 JavaScript application, I have a ListView. I need to add either a message or link to a row, depending on what the current user's status is. So I essentially need to show or hide items depending on some flag. How, using the JavaScript API of the ListView, do I parse items at an item level? There is no collection of items on the ListView control per the MSDN, and I need to have access to the data and the item at the row level.
I'm sure I'm missing it somehow, just getting into this....
I'm not completely clear on what your trying to do, but I'll give it a shot.
If you need to conditionally show or hide items (or certain parts of an item) there and a couple of ways you can go.
The first is to create an imperative template render function. First, tell you ListView that its item template is a function with something like myListView.itemTemplate = myCustomFunction. Then write a function like:
function myCustomFunction(itemPromise) {
//you have to return a promise
return itemPromise.then(function (item) {
//get the right item template (declared in your HTML),
//render the item data into it, and return the result
var itemTemplate;
if (item.data.key === "foo")
itemTemplate = q("#fooItemTemplate", element); //return foo template
else if (item.data.key === "bar")
itemTemplate = q("#barItemTemplate", element);
return itemTemplate.winControl.render(item.data);
});
}
If one of the item templates has explicit style code to show or hide part and the other doesn't then you'll get the result you're looking for.
Another way is to wait until the ListView is finished loading and then traverse and manipulate the DOM according to your conditions.
To capture the moment your ListView finishes loading do this:
myListViewControl.onloadingstatechanged = function (e) {
if (myListViewControl.loadingState == "complete") {
...
}
}
The ListView goes through a few loading states as it's loading and the last is "complete". When it's complete, you can use the awesome power of CSS selectors and the new querySelector/querySelectorAll method that ECMAScript 5 gives us to find all of the ListView items like this:
myListView.querySelectorAll(".win-item")
That would return a NodeList of all of the ListView items and you can use your ninja skills in DOM manipulation to have your way with them.
Hope that helps!!
P.S. Check out codeSHOW to learn more HTML/JS dev in Windows 8 (aka.ms/codeshowapp | codeshow.codeplex.com)

jQuery recursion

I have a problem with my code, some code does not work when I call recursive call of same function inside it. It has to be something with myGalleria = Galleria.get(0);, but I have no idea how to make it all work.
Document Ready (just to show when I call function for the first time, everything works fine for first time)
$(document).ready(function(){
$.getJSON('getImages.php', {
cat: "123"
}, function(imgData){
createGallery(imgData);
});
});
Now function itself, note that when I click on .galleria-menuButtons span that calls same function nothing is working, well galleria itself is creating, but nothing else.
function createGallery(imgData){
$("#gallery").galleria({
image_margin: 30,
clicknext: true,
transition: "fade",
dataSource: imgData
});
myGalleria = Galleria.get(0); // I don't think this works after recursive call
// Adding menu and menu buttons
myGalleria.addElement("menu").appendChild("container", "menu");
myGalleria.addElement("menuButtons").appendChild("menu", "menuButtons");
$.ajax({
url: "menuButtons.php",
success: function(data){
myGalleria.$("menuButtons").html(data);
}
});
// Menu button click events
$('.galleria-menuButtons span').live('click', function(){
alert(this.id);
// Getting jSon data
$.getJSON('getImages.php', {
cat: this.id
}, function(imgData) {
alert(imgData);
createGallery(imgData); // <- Recursive call
});
});
}
I have similar function on window.resize and it also does not work after recursive call.
$(window).resize(function(){
$(".galleria-container").css("width", $(window).width());
$(".galleria-container").css("height", $(window).height());
galleriaRescale = Galleria.get(0);
galleriaRescale.rescale(); // <- this is not working either
//sizeBG();
});
FYI - this isn't actually recursion in the traditional sense because you're calling createGallery from a click handler which launches a JSON request which then calls createGallery when that succeeds, both of which will occur after the previous call to createGallery finishes.
But you do have surviving function closures which could be confusing things or causing problems. One guess is that you may want to make sure that things you expect to be local variables (like myGalleria have a var in front of them so they really are local variables and not variables that might be scoped to a higher level and be influenced by a previous incarnation of this call or be influencing an earlier call that hasn't yet completed.
var myGalleria = Galleria.get(0);
Then, assuming imgData is some sort of data structure like an array or object, you have to make sure that there's either only one global version of that data structure that never changes or that each call of createGallery has the appropriate separate copy of that data structure. If it's getting changed along the way, then subsequent calls to createGallery may not be getting the data they want. If it's a read-only data structure (you don't change it), then you're probably OK on that one.
OK, let's talk through the pseudo code for what this does.
On page ready, you get some JSON image data.
When that succeeds, you call createGallery with that image data.
The createGallery call does some sort of operation in the DOM (perhaps an animation)
It then calls: myGalleria = Galleria.get(0); Because there is no var in front of myGalleria, this is a global variable declaration (bad news for recursion and closures)
You then use the myGalleria data structure to make some changes to the DOM (adding menus and menu items).
You then add a .live click handler on a pretty generic set of CSS classes (it's possible you have added this click handler more than once here).
You then fetch some JSON image data again.
When that image data is fetched, you start the whole process over again by called createGallery.
Summary
The two potential problems I see are that myGalleria is not a local variable and probably should be and you may be adding duplicate click handlers.
If neither of these fully solve the issue, then we probably need more information about what Galleria.get(0) is doing.
FYI, the resize clickHandler looks like it may have the same issue with not using var to make your variable declaration a local variable.
Round 2
OK, here are some more observations.
When you add the menu and menu buttons with this block of code, you aren't providing any unique identifiers to either the addElement or appendChild functions (you're providing "menu" and "menuButtons" to both). As such, I don't know how you can uniquely hook up to them in the subsequent click event. As far as your code looks, all the menu items look identical and none have unique state. I don't know the Galleria code, but I assume somebody has to make unique identifiers for these new items so that you can uniquely identify them in your subsequent click handler.
// Adding menu and menu buttons
myGalleria.addElement("menu").appendChild("container", "menu");
myGalleria.addElement("menuButtons").appendChild("menu", "menuButtons");
When you set up a click handler to presumably handle the clicks for just these menu items, you are using the exact same CSS selector every time so there's no way that this click handler is going to be uniquely assigned to just the newly create menu items (which is what I assume you want). I don't know the Galleria code, but I assume that you should create some sort of unique ID that you pass into addElement and appendChild for the newly created menu items and then reference that unique identifier when you install the click handler. Likewise, this function needs to uniquely target just the menu buttons you created by using unique identifiers myGalleria.$("menuButtons").html(data);
Lastly, I'd suggest you change the name of one of your variables just to avoid confusion. In your click handler, change the three occurrences of imgData to just data so there can be no confusion about closures and the value of imgData.
Round 3
Ultimately one of the fixes was this (embedded in the comments):
I think it might work if you just only install the .live click handler once outside the createGallery function rather than call it each time. Since it's .live it will automatically work for all future buttons you create so you should only call it once. I'd suggest putting it in the $(document).ready function block.

Categories

Resources