I´d like to add the ap-angular2-fullcalendar to my application. The birthdays from my users and other events should be displayed in the calendar. This is my template:
<angular2-fullcalendar *ngIf="isInitialized; else loadingTemplate"
#calendar
id="calendar"
(onDateChanged)="onDateChanged($event)"
[options]="calendarOptions">
</angular2-fullcalendar>
Then, I tried to add the events in the component like so:
ngOnInit() {
let e = this.memberService.members$;
e.subscribe((members: IMember[]) => {
this.loadBirthdays(members).then((events: ICalendarEvent[]) =>
this.setEvents(events));
});
}
loadBirthdays(members: IMember[]): Promise<ICalendarEvent[]> {
let years = [];
years.push(moment().subtract('1', 'year').format('YYYY'));
years.push(moment().format('YYYY'));
years.push(moment().add('1', 'year').format('YYYY'));
members.forEach((member: IMember) => {
if (member.mainData.birthday) {
for (let i in years) {
let event: ICalendarEvent = {
title: 'Birthday ' + member.firstName + ' ' + member.lastName,
start: moment(member.birthday).set('year', years[i]).format('YYYY-MM-DD')
};
this.events.push(event);
}
}
});
return Promise.resolve(this.events);
}
setEvents(events: ICalendarEvent[]) {
const cal = $('calendar');
if (events && events.length > 0) {
events.forEach(el => {
cal.fullCalendar('renderEvent', el);
});
cal.fullCalendar('rerenderEvents');
}
this.isInitialized = true;
}
When I now try to open the calendar, no events are displayed and no errors are show. when I do a console.log for the "el" in the foreach I get an object with the name and birthday in format YYYY-MM-DD
What is wrong with my code?
The issue you've got is quite simple - fullCalendar is not preserving your event when the view or date range is changed.
The renderEvent method (https://fullcalendar.io/docs/renderEvent) has a 3rd optional parameter "stick", which is false by default. The documentation states
Normally, the event will disappear once the calendar refetches its event sources (example: when prev/next is clicked). However, specifying stick as true will cause the event to be permanently fixed to the calendar.
So in other words, since your example event is in December, and presumably your calendar is initially displaying the current month (March at the time of writing this), when you try to navigate to December, every press of "next" causes the event source to be refreshed, and your manually inserted event is lot.
All you need to do is change your renderEvent line to this to set the "stick" option to true:
call.fullCalendar('renderEvent', el, true);
You also should not need cal.fullCalendar('rerenderEvents'); at all, this can be removed.
P.S. You may be able to make your code a little more efficient. For instance instead of looping through events, you could use "renderEvents" (https://fullcalendar.io/docs/renderEvents) to add them all to the calendar at once:
if (events && events.length > 0) {
cal.fullCalendar('renderEvents', events, true);
}
You may also want to consider whether you need to use renderEvent(s) at all - the more normal way to set events on your calendar is to use the events option in the calendar initialisation options. You can either set it as a static array of event objects (https://fullcalendar.io/docs/events-array), or a JSON or other custom feed from a URL (https://fullcalendar.io/docs/events-json-feed or https://fullcalendar.io/docs/events-function). The latter two also have the advantage that the calendar dynamically updates itself from the remote data source when navigating between views and dates.
Related
I am struggling displaying the correct data from my database with FullCalendar. The database structure I've adopted is to store rrules as ReccurringEvent database objects, and individual deviations from those rrules as separate IndividualEvent objects, all of which have a "Done" property. The goal is to display each event's "Done" status individually even if they're all part of the same RRule.
ReccurringEvents are set up in FullCalendar as:
calendar.addEvent({
title: item['title'],
rrule: {
dtstart: item['startDate'],
freq: item['frequency']
},
allDay: item['allDay'],
backgroundColor: backgroundRgb,
textColor: computeTextColor(backgroundRgb),
extendedProps: {
recurringEventID: item['recurringEventID'],
description: item['description']
},
});
Then a separate javascript bit modifies those individual occurrences as "done" and attaches the appropriate IndividualEventID to the occurrence.
var events = calendar.getEvents();
individualEventList.forEach(function(item){
events.forEach(function(event){
var indivdate = new Date(item['startDate']).toLocaleDateString('en-CA');
var eventdate = event.start.toLocaleDateString('en-CA');
//if the event is on the correct date and is a part of the rrule...
if(event.extendedProps.recurringEventID == item['recurringID'] && eventdate == indivdate)
{
console.log("match!");
event.setExtendedProp('singleID',item['individualEventID'])
//then modify its 'done' state
event.setExtendedProp('done',item['done']);
}
})
})
The "match!" console log fires only once, as I am expecting, when I modify a single event instance to be done and then load my database in. However all events in the rrule show up as done when I click on them/investigate their extendedProps.
It seems that modifying one event's extendedProps will modify all events' extendedProps if they're in the same rrule. How do I control this behavior? Can I break an event out of its rrule? Do I need to just do this computation in a different way?
I had to completely avoid dealing with extendedProps that are per single event. What I ended up doing:
When a user clicks on the event and I want to render it as done or not, I will check that events recurringEventID extendedProp, then check all IndividualEventList events to see if there's one that came from the same recurring event, and then check that the dates match. Then I'll check an HTML checkbox based off of the IndividualEventList event's done state.
In other words, I'm completely bypassing using extendedProps for anything other than "tell me which recurring event the event that was clicked belongs to".
Since I am assuming that all events will be all-day events this will work for me, but it's not a very general solution.
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.
I have a dashboard, where people can put multiple fullcalendar widgets (imagine a team dashboard with a fullcalendar widget for every person in a team). The main issue with this is performance. One client has 16 such widgets (all quite full of events) and they are not limited in number.
Only the first calendar has header controls (others have them hidden using CSS).
What I need is to propagate the changes to view (switch basicWeek to month etc.), firstDay (today in basicWeek, Monday everywhere else) and the date (prev, next, today) from first calendar to other calendars.
The issue is performance. As I didn't really find a way how to do it (that wouldn't look really hacky) except in viewRender, which is called for every single change. That means, if you switch from basicWeek to agendaWeek the first calendar propagates the firstDay to other calendars (which calls viewRender) and after that propagates the view change, which basically re-renders all calendars (except the first one) twice.
Is there a way to propagate those changes and manually call render on other calendars (from what I see in the source code, there might not be one), or a better way to do it?
I am also thinking about just destroying the calendars and re-initializing them with new options, but that might cause flashing (instead of quite a lot of lag caused by multiple re-renders). One of the options I thought of was using my own buttons (or re-using default buttons just unbinding original events from them), but even then I would still have to re-render the calendars multiple times in some occasions.
Switching view with 6 almost empty calendars takes about 3 seconds, which is unacceptable.
This is how my viewRender code looks like (this is inside own fullcalendar function, so every widget has its own scope with cached variables and settings)
viewRender: function(view) {
console.log('viewRender', calendarid, lastView, view.type);
// Propagate all changes from first calendar to other calendars on the page
if ($('#' + calendarid).parents('.widgetCalendar').is(':first-child')) {
// Change the first day (basicWeek is always today, other views are always Monday)
if (view.type != 'basicWeek' && currFirstDay != 1) {
currFirstDay = 1;
$('.calendarDiv').fullCalendar('option', 'firstDay', 1);
return;
} else if (view.type == 'basicWeek' && currFirstDay != firstday) {
currFirstDay = firstday;
$('.calendarDiv').fullCalendar('option', 'firstDay', firstday);
return;
}
// Propagate the view change to other calendars
if (lastView != view.type) {
lastView = view.type;
$('.calendarDiv:not(#' + calendarid + ')').fullCalendar('changeView', view.type); // , view.intervalStart.valueOf());
}
// Propagate the date change to other calendars
if (lastDate != view.intervalStart.valueOf()) {
lastDate = view.intervalStart.valueOf();
$('.calendarDiv:not(#' + calendarid + ')').fullCalendar('gotoDate', view.intervalStart.valueOf());
}
}
},
Ps.: At first I thought this issue is mainly ajax requests to get new events, but I changed that to a function which uses single call and caches the results. The main reason I thought that were some delays between ajax and that they weren't concurrent (session locking). But changing it to new function shows that the issue are indeed the re-renders which take white a long time per calendar (about 250 - 350ms).
If there is any info missing, ask in the comments and I will update the question.
Fullcalendar version: 3.4.0
I'm developing an app in ng2 and I'm struggling with something. I'm building a calendar where you can pick a date-range and I need to react on click & mouseenter/mouseleave events on day cells. So I have a code (simplified) like this:
calendar.component.html
<month>
<day *ngFor="let day of days" (click)="handleClick()"
(mouseenter)="handleMouseEnter()"
(mouseleave)="handleMouseLeave()"
[innerHTML]="day"></day>
</month>
But this gives me hundreds of separate event listeners in the browser's memory (each day's cell gets 3 event listeners, and I can have up to 12 months displayed at a time so it would be over 1k of listeners).
So I wanted to do it "the proper way", using the method called "event delegation". I mean, attach a click event on the parent component (month) and when it receives a click event, simply check whether it occurred on Day component - and only then I would react to this click. Something like jQuery does in it's on() method when you pass it the selector parameter.
But I was doing it by referencing the DOM elements natively in the handler's code:
month.component.ts
private handleClick(event) {
if (event.target.tagName === 'DAY') {
// handle day click
} else {
// handle other cases
}
}
and my colleagues rejected my idea, since - as they said - "There must be a simpler, proper way of handling this in NG2; like there is in jQuery. Besides, it's getting out of control here - you're reacting to Day's clicks in Month's code."
So, my question is, is there a better way? Or am I trying to solve a problem which I shouldn't bother solving anymore, since users' devices get more and more memory/processing power everyday?
Thanks in advance!
Intro
I stumbled across this today, and I can really see the need for this implementation in a lot of applications. Now I can't vouch that this is 100% the best technique however I have gone out of my way to make this approach as angular inspired as possible.
The approach that I have come up with has 2 stages. Both stage 1 and stage 2 will add a total of years * months + years * months * days, so for 1 year you will have 12 + 365 events.
Stage Scope
Stage 1: Delegate events from when a month is clicked down into the actual day which was clicked without requiring an event on the day.
Stage 2: Propagate the chosen day back to the month.
Just before delving in, the application consists of 3 components which are nested in the following order: app => month => day
This is all the html that is required. app.component hosts a number of months, month.component hosts a number of days and day.component does nothing but display it's day as text.
app.component.html
<app-month *ngFor="let month of months" [data-month]="month"></app-month>
month.component.html
<app-day *ngFor="let day of days" [data-day]="day">{{day}}</app-day>
day.component.html
<ng-content></ng-content>
This is pretty stock standard stuff.
Stage 1
Let's have a look at the month.component.ts where we want to delegate our event from.
// obtain a reference to the month(this) element
constructor(private element: ElementRef) { }
// when this component is clicked...
#HostListener('click', ['$event'])
public onMonthClick(event) {
// check to see whether the target element was a child or if it was in-fact this element
if (event.target != this.element.nativeElement) {
// if it was a child, then delegate our event to it.
// this is a little bit of javascript trickery where we are going to dispatch a custom event named 'delegateclick' on the target.
event.target.dispatchEvent(new CustomEvent('delegateEvent'));
}
}
In both stage 1 and 2, there is only 1 caveat and that is; if you have nested child elements within your day.component.html, you will need to either implement bubbling for this, better logic in that if statement, or a quick hack would be.. in day.component.css :host *{pointer-events: none;}
Now we need to tell our day.component to be expecting our delegateEvent event. So in day.component.ts all you have to do (in the most angular way possible) is...
#HostListener('delegateEvent', ['$event'])
public onEvent() {
console.log("i've been clicked via a delegate!");
}
This works because typescript doesn't care about whether the event is native or not, it will just bind a new javascript event to the element and thus allows us to call it "natively" via event.target.dispatchEvent as we do above in month.component.ts.
That concludes Stage 1, we are now successfully delegating events from our month to our days.
Stage 2
So what happens if we say want to run a little bit of logic in our delegated event within day.component and then return it to month.component - so that it can then carry on with its own functionality in a very object oriented method? Well fortunately, we can very easily implement this!
In month.component.ts update to the following. All that has changed is that we are now going to pass a function with our event invocation and we defined our callback function.
#HostListener('click', ['$event'])
public onMonthClick(event) {
if (event.target != this.element.nativeElement) {
event.target.dispatchEvent(new CustomEvent('delegateEvent', { detail: this.eventDelegateCallback}));
}
}
public eventDelegateCallback(data) {
console.log(data);
}
All that is left is to invoke this function within day.component.ts...
public onEvent(event) {
// run whatever logic you like,
//return whatever data you like to month.component
event.detail(this.day);
}
Unfortunately our callback function is a little ambiguously named here, however typescript will complain about the property not being a defined object literal for CustomEventInit if named otherwise.
Multiple Event Funnel
The other cool thing about this approach is that you should never have to define more than this number of events because you can just funnel all events through this delegation and then run logic within day.component.ts to filter by event.type...
month.component.ts
#HostListener('click', ['$event'])
#HostListener('mouseover', ['$event'])
#HostListener('mouseout', ['$event'])
public onMonthEvent(event) {
if (event.target != this.element.nativeElement) {
event.target.dispatchEvent(new CustomEvent('delegateEvent', { detail: this.eventDelegateCallback }));
}
}
day.component.ts
private eventDelegateCallback: any;
#HostListener('delegateEvent', ['$event'])
public onEvent(event) {
this.eventDelegateCallback = event.detail;
if(event.type == "click"){
// run click stuff
this.eventDelegateCallback(this.day)
}
}
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/