fullcalendar 4, callback when calendar is completely rendered - javascript

Fullcalendar version 3 used to have a callback function that would fire once all events have rendered. I would use this:
eventAfterAllRender: function( view ) {
load_call_list();
}
load_call_list is a function I made to count certain events based on their status. It also queries my database for other information.
Using fullcalendar 4, once the calendar has fully loaded and all the events have rendered, I want to call that function. Here's how my calendar is initialized now...
<script>
document.addEventListener('DOMContentLoaded', function() {
var calendar_full = document.getElementById('calendar_full');
var calendar = new FullCalendar.Calendar(calendar_full, {
height: 700,
selectMirror: true,
events: {
url: 'ajax_get_json.php?what=location_appointments'
},
....
I suppose my lack of understanding about javascript is limiting me in understanding where I can catch when the calendar events have all fully loaded.
I thought about using eventrender, but I don't want the load_call_list to be fired every time as my database is queried with that function too.
I tried using jquery $(document).ready(function(){}) but that fires before the events have been rendered.
Any advice on how to accomplish this?

Ok, so how about this? I can use the loading function. When the calendar is done loading, I can call my load_call_list function. I am using a bool variable to stop it from firing everytime the calendar loads without a page refresh (like when cycling through months or weeks, for instance).
When the page first loads, I have a var called initial_load = true. When the calendar has finished 'loading', if this is true, then I'll call my function and set my initial_load to false so it won't just fire off as I explained before.
<script>
//set initial_load so when the calendar is done loading, it'll get call list info
var initial_load = true;
document.addEventListener('DOMContentLoaded', function() {
var calendar1 = document.getElementById('calendar_mini');
var calendar_mini = new FullCalendar.Calendar(calendar1, {
....
....
loading: function(bool) {
if (bool) {
$('.loader').show();
$('#show_cancelled_appts').hide();
$('#show_rescheduled_appts').hide();
$('#print_calendar').hide();
} else {
$('.loader').hide();
$('#show_cancelled_appts').show();
$('#show_rescheduled_appts').show();
$('#print_calendar').show();
//once it's done loading, load call list with current date stuff; set
initial_load = false so it doesn't keep loading call list when calendar changes while cycling months/weeks/etc
var today = moment().format('YYYY-MM-DD');
if (initial_load) {
load_call_list(today);
initial_load = false;
}
}
},
It does actually work! Thoughts on that? Is this okay programming? I don't see any downside as of now.

Related

How show easyautocomplete dialog only if cursor is in input

I'm trying set easyautocomplete on my input. Dataset i am geting from ajax json and there is some delay. If user is too fast and writes for example "Adam" and pushes tab, cursor skips to next input, but after easyautocomplete shows dialog on previous input and doesn´t hide it. Is there any way how to show easyautocomplete dialog only when i have cursor in input?
var options = {
minCharNumber: 3,
url: function(phrase) {
return "data?q=" + phrase;
},
getValue: function(element) {
return element.surname + " " + element.name;
},
template: {
type: "description",
fields: {
description: "phone"
}
},
ajaxSettings: {
dataType: "json",
method: "POST",
data: {
dataType: "json"
}
},
list: {
onClickEvent: function() {
/*Some action*/
},
hideAnimation: {
type: "slide", //normal|slide|fade
time: 400,
callback: function() {}
}
},
requestDelay: 400
};
$(".autoComplete").easyAutocomplete(options);
Minimum, Complete, Verifiable, Example
In order to easily see this result, you'll have to open up your dev tools and throttle your network traffic so the ajax connection takes a little while
Here's a Demo of the issue in jsFiddle
Handling Library Events (Doesn't Work)
My initial thought was you could handle this during some of the EAC lifecycle events that fired, like the onLoadEvent or onShowListEvent:
var options = {
url: "https://raw.githubusercontent.com/KyleMit/libraries/gh-pages/libraries/people.json",
getValue: "name",
list: {
match: {
enabled: true
},
onLoadEvent: function() {
console.log('LoadEvent', this)
},
onShowListEvent: function() {
console.log('ShowListEvent', this)
}
},
};
However, these methods don't seem to provide an option to alter the control flow and prevent future events
Updating Source Code (Works)
Peeking into the library's source code, Easy AutoComplete does the following ...
Handles keyup events
Which then calls loadData
Which fires an AJAX request with the provided URL
depending on the network and server speed, any amount of time can pass before step 4, and the input could lose focus
Once the ajax promise is returned, will call showContainer()
Which triggers the "show.eac" event on the container
Which then opens the list with the selected animation
During step 6, we could add one last check to confirm the selected input still has focus before actually opening, which would look like this:
$elements_container.on("show.eac", function() {
// if input has lost focus, don't show container
if (!$field.is(":focus")) {return}
// ... rest of method ...
Here's a working demo in Plunker which modifies the library's source code in a new file
Unfortunately, that's not a great practice as it leaves you fragile to future changes and transfers ownership of the lib maintenance to your codebase. But it does work.
I created Pull Request #388 with the proposed changes, so hopefully a long term fix within the library itself will be available at some point in the future.
Wrapper (Recommended for now)
If you don't want to muck with third party code, there are some weird workarounds to mess with the internal execution. We can't modify the showContainer method since it's inside a closure.
We can add another listener on the show.eac event so we can be a part of the event pipeline, however there are some challenges here too. Ideally, we'd like to fire before the library handler is executed and conditionally stop propagation. However, initializing EAC will both a) create the container we have to listen to and also b) attach an event listener.
Note: Event handlers in jQuery are fired in the order they are attached!
So, we have to wait until after the lib loads to attach our handler, but that means we'll only fire after the list is already displayed.
From the question How to order events bound with jQuery, we can poke into the jQuery internals and re-order attached events so we can fire our handler before the library's handler is called. That'll look like this:
$._data(element, 'events')["show"].reverse()
Note: Both e.stopPropagation() and e.preventDefault() won't work since they prevent future events; instead we need to take more immediate action with e.stopImmediatePropagation()
So the wrapper would look like this:
$("#countries").easyAutocomplete(options);
$(".easy-autocomplete-container").on("show.eac", function(e) {
var inputId = this.id.replace('eac-container-','')
var isFocused = $("#"+inputId).is(":focus")
if (!isFocused ) {
e.stopImmediatePropagation()
}
});
$(".easy-autocomplete-container").each(function() {
$._data(this, 'events')["show"].reverse()
})
Here's a working demo in CodePen

How to make sure that all events are unattached when called multiple times in Javascript?

I'm using dragswipe, a plugin from here. It works perfectly fine. However, I have a requirement to do dynamic carousels. So, every time a user changes something on a page, the carousels get updated dynamically. I thought I could just recall the plugin to update the element, but somehow when I dragged the carousel the functional gets called multiple times.
For example, I do this to initialise the plugin.
function init() {
$('#carousel').dragswipe({
width: 320,
current_page_element: '#current_page',
total_pages_element:'#total_pages'
});
}
So after the page is updated via ajax I have a callback to call this method again like this
function callback() {
init();
}
Everything should is updated perfectly fine, but when I started dragging. The carousel skips some pages. I thought I had to unbind all the events so I tried this. $('#carousel').unbind() but the problem still persists.
Here's the source code when the plugin is initialised.
$.fn.dragswipe = function(options) {
options = $.extend({
offset: 0,
turn_threshold: 0.1,
current_page: 0
},options)
this.each(function() {
option = $(this).hammer({
drag_vertical: false,
swipe_time: 20
});
// start a drag.
// if we're moving left or right, set our initial distance to 0.
$(this).bind('dragstart',function(ev) {
console.log('dragstart');
if (ev.direction=='left' || ev.direction=='right') {
drag_distance = 0;
}
});
// while dragging, change the dragging distance based on how far a user has dragged.
$(this).bind('drag',function(ev) {
console.log('drag');
if (ev.direction=='left' || ev.direction=='right') {
drag_distance = ev.distanceX;
var options = CONFIGS[$(this).data('dragswipe_id')];
$(this).updateOffset(options.offset+drag_distance);
}
});
$(this).bind('dragend',function(ev) {
console.log('dragend');
if (ev.direction=='left' || ev.direction=='right') {
var options = CONFIGS[$(this).data('dragswipe_id')];
if (Math.abs(drag_distance / options.width) > options.turn_threshold) {
if (ev.direction=='left') {
options.current_page++;
}
if (ev.direction=='right') {
options.current_page--;
}
}
// store modified options
CONFIGS[$(this).data('dragswipe_id')] = options;
console.log(options.current_page);
$(this).gotoPage(options.current_page,true);
}
});
// set the dragswipe ID used to look up config options later.
$(this).data('dragswipe_id',CONFIGS.length);
// store config options.
CONFIGS[$(this).data('dragswipe_id')] = options;
});
}
Which I see nothing wrong with the plugin, but maybe I'm missing something obvious.
UPDATED
I have created the example in jsfiddle, but it's not working just in case anyone can fix the problem. Also, on the plugin site itself. The problem can be reproduce by running this code to initialise the plugin multiple times. After running the code twice, when you drag the page it goes to the last page instead of the second page.
$('#carousel').dragswipe({
width: 320,
current_page_element: '#current_page',
total_pages_element:'#total_pages'
});
Have you tried turning it off and on again?
Dragswipe has a removeDragswipe() method that does a little bit more than just unbinding, and it also explicitly only unbinds events that are related to dragswipe itself so it won't mess up any other events that may have been bound.
So instead of unbind(), do this:
function callback() {
$('#carousel').removeDragswipe();
init();
}
As discussed in the comments, this doesn't work because it seems that even after unbinding all the events, after re-binding them by activating the plugin again the events seem to fire an extra time.
What does seem to be a workaround is to actually rebuild the entire element so that no events can linger:
function callback() {
$('#carousel').removeDragswipe();
rebuildCarousel();
init();
}
function rebuildCarousel() {
var wrapper = $('#carousel').parent;
var html = parent.html();
$('#carousel').remove();
wrapper.html(html);
}

What is the preferred pattern for re-binding jQuery-style UI interfaces after AJAX load?

This always gets me. After initializing all lovely UI elements on a web page, I load some content in (either into a modal or tabs for example) and the newly loaded content does not have the UI elements initialized. eg:
$('a.button').button(); // jquery ui button as an example
$('select').chosen(); // chosen ui as another example
$('#content').load('/uri'); // content is not styled :(
My current approach is to create a registry of elements that need binding:
var uiRegistry = {
registry: [],
push: function (func) { this.registry.push(func) },
apply: function (scope) {
$.each(uiRegistry.registry, function (i, func) {
func(scope);
});
}
};
uiRegistry.push(function (scope) {
$('a.button', scope).button();
$('select', scope).chosen();
});
uiRegistry.apply('body'); // content gets styled as per usual
$('#content').load('/uri', function () {
uiRegistry.apply($(this)); // content gets styled :)
});
I can't be the only person with this problem, so are there any better patterns for doing this?
My answer is basically the same as the one you outline, but I use jquery events to trigger the setup code. I call it the "moddom" event.
When I load the new content, I trigger my event on the parent:
parent.append(newcode).trigger('moddom');
In the widget, I look for that event:
$.on('moddom', function(ev) {
$(ev.target).find('.myselector')
})
This is oversimplified to illustrate the event method.
In reality, I wrap it in a function domInit, which takes a selector and a callback argument. It calls the callback whenever a new element that matches the selector is found - with a jquery element as the first argument.
So in my widget code, I can do this:
domInit('.myselector', function(myelement) {
myelement.css('color', 'blue');
})
domInit sets data on the element in question "domInit" which is a registry of the functions that have already been applied.
My full domInit function:
window.domInit = function(select, once, callback) {
var apply, done;
done = false;
apply = function() {
var applied, el;
el = $(this);
if (once && !done) {
done = true;
}
applied = el.data('domInit') || {};
if (applied[callback]) {
return;
}
applied[callback] = true;
el.data('domInit', applied);
callback(el);
};
$(select).each(apply);
$(document).on('moddom', function(ev) {
if (done) {
return;
}
$(ev.target).find(select).each(apply);
});
};
Now we just have to remember to trigger the 'moddom' event whenever we make dom changes.
You could simplify this if you don't need the "once" functionality, which is a pretty rare edge case. It calls the callback only once. For example if you are going to do something global when any element that matches is found - but it only needs to happen once. Simplified without done parameter:
window.domInit = function(select, callback) {
var apply;
apply = function() {
var applied, el;
el = $(this);
applied = el.data('domInit') || {};
if (applied[callback]) {
return;
}
applied[callback] = true;
el.data('domInit', applied);
callback(el);
};
$(select).each(apply);
$(document).on('moddom', function(ev) {
$(ev.target).find(select).each(apply);
});
};
It seems to me browsers should have a way to receive a callback when the dom changes, but I have never heard of such a thing.
best approach will be to wrap all the ui code in a function -even better a separate file -
and on ajax load just specify that function as a call back ..
here is a small example
let's say you have code that bind the text fields with class someclass-for-date to a date picker then your code would look like this ..
$('.someclass-for-date').datepicker();
here is what i think is best
function datepickerUi(){
$('.someclass-for-date').datepicker();
}
and here is what the load should look like
$('#content').load('/uri', function(){
datepickerUi();
})
or you can load it at the end of your html in script tag .. (but i dont like that , cuz it's harder to debug)
here is some tips
keep your code and css styles as clean as possible .. meaning that for text fields that should be date pickers give them one class all over your website ..
at this rate all of your code will be clean and easy to maintain ..
read more on OOCss this will clear what i mean.
mostly with jquery it's all about organization ... give it some thought and you will get what you want done with one line of code ..
edit
here is a js fiddle with something similar to your but i guess it's a bit cleaner click here

What are the pros and cons of timeouts vs counting down with a content loading asynchronous call in JavaScript / jQuery

To my way of thinking, I might be over-engineering with the recursive solution.
Wait 2 seconds for first set of modules to be loaded:
function loadContents() {
$('[data-content]:not(.loaded)').each( function() {
$(this).load($(this).data('content'));
$(this).addClass('loaded');
});
}
loadContents();
setInterval(loadContents, 2000);
When all of first set of modules are loaded, check for new modules:
function loadContents() {
var notLoaded = $('[data-content]:not(.loaded)'),
notLoadedLength = notLoaded.length;
notLoaded.each( function(i,element) {
$(element).addClass('loaded');
$(element).load($(element).data('content'), function() {
// one more has been loaded
notLoadedLength--;
if (notLoadedLength == 0) {
alert("countdown good");
loadContents();
}
});
});
}
loadContents();
You should be able to do all of this with success handlers and no polling with timers.
You don't specify exactly what you want to do, but if you want to load multiple things in parallel and know when they are all loaded, then you can just keep some sort of state on how many have been loaded and when the count shows that they are all now loaded, you will know you're done.
If you want to load them sequentially, then you can just load the next one from each success handler. It's probably easiest to create a list of things to be loaded and just have a generic success handler that gets the next one in the list and kicks off it's load and removes it from the list. When the list of remaining items to load is empty, you're done.
Edit: Looking further at your code, it looks like you're loading them all in parallel. You can just create a success handler for each one that is loading, add the loaded class in that success handler and see how many more have not yet finished. I would suggest this:
function loadContents() {
$('[data-content]:not(.loaded)').each( function() {
var obj = $(this); // save in local variable in function closure so we can reference it in the success handler
obj.load(obj.data('content'), function() {
obj.addClass('loaded');
if ($('[data-content]:not(.loaded)').length == 0) {
// all pieces of content are now loaded
} else {
// some pieces of content are still loading
}
});
});
}
loadContents();
Edit 2: OK, based on your comments, I now understand the problem better. I would scope loadContents to a parent of the DOM tree and then call it on the newly loaded content from the success handler. This will work for inifinite levels and it's safe because it only ever calls itself once for any given parent of the DOM tree. When there is no new content to load, it just has nothing to do and thus doesn't call itself any more. Here's what I would recommend:
function loadContents(root) {
$('[data-content]:not(.loaded)', root).each( function() {
var obj = $(this); // save in local variable in function closure so we can reference it in the success handler
obj.load(obj.data('content'), function() {
obj.addClass('loaded');
loadContents(obj); // now load any contents from the newly loaded content
});
});
}
loadContents(document.body); // start it off by looking in the whole DOM tree

jQuery FullCalendar - create standard list of events from Gcal

I'm using FullCalendar in a project and the month view is great. But I also need to create a simple unordered list of the events gathered from 4 different Gcal feeds, and I haven't been able to do it. Anyone got any ideas? A quick response would be great.
Posting here the solution for adding a new view in fullcalendar.js. I needed to implement an Agenda or List view in fullcalendar.js for one of my projects. In this process, I got a chance to reverse engineer the code by Adam. I must say that this plugin uses some very good coding practices and javascript concepts.
I think that it would be useful for users if I share my findings and solution here. Included list view has following features:
Fully functional and customizable List/Agenda View
Pagination using the included arrow buttons
Click/Hover effects on the events
Dynamic calling of events for the list view using pagination
First of all, we CAN NOT do it without touching the fullcalendar sourcecode. Javascript does not allow that kind of extensibility. Howvever, I have kept things as simple as possible and I am also posting the steps to replicate it from scratch along with the sourcecode. This will be helpful in case of future updates of fullcalendar.js
First of all we need to define a new view for showing the list of events. Views are defined as objects in fullcalendar.js and can be added using constructor functions. You can find the construction function for list view on this URL https://gist.github.com/amitesh-m/5744528. This function defines and initializes a new view called "list". Inside it, renderEvents is the main member function that renders the available events on this view and attaches the click/hover event triggers.
Next we need to change the updateEvents function of Calendar object (around line# 500). This is done to unlink the default event calling behavior of fullcalendar.js for the list view. Modified function will look like this:
function updateEvents(forceRender) {
if (currentView.name == "list") {
currentView.visStart = new Date(0);
currentView.visEnd = new Date(currentView.page * 1000);
refetchEvents();
} else if (!options.lazyFetching || isFetchNeeded(currentView.visStart, currentView.visEnd)) {
refetchEvents();
}
else if (forceRender) {
rerenderEvents();
}
}
Everything will work as earlier for other views but now the calendar will send a slightly different request to the event server for the list view. Now, fullcalendar will set "start=0" and "end=1" when somebody clicks on the list view. Number of items to show on the page is to be managed by the server.
Next, we need to make a change in renderView() function of the calendar object (around line#374). This is to enable pagination on our list based on the arrow buttons which are alreaedy included in fullcalendar.js. This function should look like this:
function renderView(inc) {
if (elementVisible()) {
ignoreWindowResize++; // because renderEvents might temporarily change the height before setSize is reached
unselect();
if (suggestedViewHeight === undefined) {
calcSize();
}
var forceEventRender = false;
if (!currentView.start || inc || date < currentView.start || date >= currentView.end) {
// view must render an entire new date range (and refetch/render events)
currentView.render(date, inc || 0); // responsible for clearing events
setSize(true);
forceEventRender = true;
}
else if (currentView.sizeDirty) {
// view must resize (and rerender events)
currentView.clearEvents();
setSize();
forceEventRender = true;
}
else if (currentView.eventsDirty) {
currentView.clearEvents();
forceEventRender = true;
}
if (currentView.name == "list") {
forceEventRender = true;
if (inc == 1) {
currentView.page++;
currentView.title = "Page " + currentView.page;
} else if (inc == -1) {
currentView.page--;
currentView.title = "Page " + currentView.page;
}
}
currentView.sizeDirty = false;
currentView.eventsDirty = false;
updateEvents(forceEventRender);
elementOuterWidth = element.outerWidth();
header.updateTitle(currentView.title);
var today = new Date();
if (today >= currentView.start && today < currentView.end) {
header.disableButton('today');
} else {
header.enableButton('today');
}
ignoreWindowResize--;
currentView.trigger('viewDisplay', _element);
}
}
Now, when somebody clicks on previous or next buttons, the calendar will send a request for new event data to the server. Value of "start" will remain 0 throughout for the list view, while the value for "end" will represent the subsequent page numbers.
That's it. All you need to do now is call the list view in your full calendar options. This can be done by adding "list" to the "right" of header object as follows
header: {
left: 'prev,next today',
center: 'title',
right: 'list,month,agendaWeek,agendaDay'
}
The demo is available on this URL:
http://tas.co.in/fullcalendar-with-listview/
the clientEvents method will retrieve all the events fullCalendar has fetched & has in memory. not sure if this will help you all the way
http://arshaw.com/fullcalendar/docs/event_data/clientEvents/

Categories

Resources