MediaWizards Grimoire

Feb 26, 2023 - Huw Reddick

Events Calendar Block - Custom View

Creating the Plugin

In this article I will explain how I created a custom view based on the fullcalendar js library.

Custom views give you complete control over how a Block is rendered in the backoffice and this enables you to give a better representation of the content.

As part of an Umbraco upgrade I needed to create a custom block view to display events using fullcalendar.io (The site was currently using it in the UI so needed it to do the same in the backoffice)

After some digging around I came across Angularjs Full Calendar Example - Plunker (plnkr.co) which explained how to display a calendar in Angular-JS using fullcalendar 2.1.1 while this was an older version of fullcalendar. I decided it would probably suffice as all I wanted was to display some events in the calendar.

Really the only thing I needed to do was write my own Angular-JS controller and work out how to get the other files loaded and registered so I could use them.

An App_Plugins folder was created for the files and a package manifest added.

{
  "name": "FullCalendar BlockItem",
  "version": "1.0.0",
  "javascript": [
    "~/App_Plugins/FullCalendar/scripts/fullcalendar.min.js",
    "~/App_Plugins/FullCalendar/scripts/moment.min.js",
    "~/App_Plugins/FullCalendar/scripts/ui-calendar.min.js",
    "~/App_Plugins/FullCalendar/fullcalendar-controller.js",
    "~/App_Plugins/FullCalendar/fullcalendar-resources.js"
  ],
  "css": [
    "~/App_Plugins/FullCalendar/css/fullcalendar.min.css"
  ]
}

The ui-calendar javascript file contains a module that needs to be imported into the Angular-JS app in order for it all to work, by some trial and error I managed to work out how to do that with the following code at the top of my controller.

angular.module('umbraco').requires.push('ui.calendar');

With that part figured out I could now create my controller (fullcalendar-controller.js).

angular.module('umbraco').requires.push('ui.calendar');

angular.module('umbraco').controller('fullCalendarController', 
    ['$scope', '$log', 'uiCalendarConfig', '$timeout','fullCalendarResource',
    function($scope, $log, uiCalendarConfig, $timeout,fullCalendarResource) {

        $scope.uiCalendarConfig = uiCalendarConfig;
        $scope.events = [];
        $scope.eventSources = [];
        $scope.calendarConfig = {
            selectable: true,
            selectHelper: true,
            editable: true,
            header:{
                left: $scope.block.data.left,
                center: $scope.block.data.center,
                right: $scope.block.data.right
            }
        };

        fullCalendarResource.getEventsFromApi($scope.block.data.dataSource).then(function(response) {

            angular.forEach(angular.fromJson(response.data), function(item) {

                    $scope.events.push({ id: $scope.generateGuid(), title: item.title, className:item.className, start: item.start, end: item.end, description: item.description, allDay: item.allDay });

            });
            $scope.eventSources.push($scope.events);

        });

        $scope.generateGuid = function() {
            return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {
                var r = Math.random() * 16 | 0,
                    v = c == 'x' ? r : (r & 0x3 | 0x8);
                return v.toString(16);
            });
        }
    }
]);

The Angular-JS controller calls an api which should return a JSON array of events to render for the displayed month. The event JSON should be the format below.

[
	{
		"title":"Example event",
		"description":"Some sort of event is happening",
		"start":"2022-10-19T23:00:00Z",
		"end":"2022-10-19T23:00:00Z",
		"id":"0f87be74-7321-ec11-a97f-0022483fd864",
		"url":null,
		"allDay":true,

		"daysOfWeek": "[2,5]",
		"startRecur":"2022-10-01",
		"endRecur":"2022-10-30",
		"startTime":"14:30",
		"endTime":"15:30"
	}
]

The custom view template is a simple HTML file written as Angular-JS template. Any HTML can be used, but we will use Angular-JS syntax for data binding.

This is the code for the view:
App_Plugins/FullCalendar/fullcalendar.html

<div ng-controller="fullCalendarController">
    <div ui-calendar="calendarConfig" ng-model="eventSources" calendar="myCalendar" style="margin: 1em;"></div>
</div>

Document Type

With the backoffice Angular-JS stuff done, it was time to fire up the website and add an element document type to hold the settings and add to the BlockGrid blocks.

The element contains some textstrings to hold various settings for the calendar.

fullcalendar Element Document Type
fullcalendar Element Document Type

DataSource - A Url to an api that returns a JSON array of events
left : title 
center : today
right : prevYear prev,next nextYear

Items to display in the Toolbar sections (left,center,right), each section can contain any of the following values and should be seperated by a comma or a space.

today,prev,next,prevYear,nextYear,title

The element was then added to the BlockGrid template and it's custom view and style sheet set to our new fullcalendar templates.

Assign custom view
Assign custom view

The new block item was added to a content page and the DataSource set to an api (/testevents/) success! cool The fullcalendar displayed and the events were showing, well some of them anyway undecided, the recurring events were not being displayed. It turns out this was because fullcalendar 2.1.1 did not support recurring events frown

Angular-JS controller

Given that the code was just for displaying the events and the event JSON contained all the data needed for the recurring event the easiest thing was to just generate the recurring events in the controller and add them to the array of events to display. I extended the controller as below.

angular.module('umbraco').requires.push('ui.calendar');

angular.module('umbraco').controller('fullCalendarController', 
    ['$scope', '$log', 'uiCalendarConfig', '$timeout','fullCalendarResource',
    function($scope, $log, uiCalendarConfig, $timeout,fullCalendarResource) {

        $scope.uiCalendarConfig = uiCalendarConfig;
        $scope.events = [];
        $scope.eventSources = [];
        $scope.calendarConfig = {
            selectable: true,
            selectHelper: true,
            editable: true,
            header:{
                left: $scope.block.data.left,
                center: $scope.block.data.center,
                right: $scope.block.data.right
            }
        };

        fullCalendarResource.getEventsFromApi($scope.block.data.dataSource).then(function(response) {

            angular.forEach(angular.fromJson(response.data), function(item) {

                if (item.daysOfWeek) { //recurring event so create instances
                    angular.forEach(angular.fromJson(item.daysOfWeek),
                        function(dayOfWeek) {
                            const date = moment(item.startRecur);
                            const dow = date.isoWeekday();
                            // if we haven't yet passed the day of the week that I need:
                            if (dow <= dayOfWeek) { 
                                // then just give me this week's instance of that day
                                date.isoWeekday(dayOfWeek);  
                            } else {
                                // give me next week's instance of that day
                                date.add(1, "w");
                                date.isoWeekday(dayOfWeek);
                            }

                            while (date < moment(item.endRecur)) {
                                item.start = date.format("YYYY-MM-DD") + "Z" + item.startTime;
                                item.end = date.format("YYYY-MM-DD") + "Z" + item.endTime;
                                $scope.events.push({ id: $scope.generateGuid(), title: item.title, className:item.className, start: item.start, end: item.end, description: item.description, allDay: item.allDay });
                                date.add(1, "w");
                            }
                        });

                } else {
                    $scope.events.push({ id: $scope.generateGuid(), title: item.title, className:item.className, start: item.start, end: item.end, description: item.description, allDay: item.allDay });
                }

            });
            $scope.eventSources.push($scope.events);

        });

        $scope.generateGuid = function() {
            return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {
                var r = Math.random() * 16 | 0,
                    v = c == 'x' ? r : (r & 0x3 | 0x8);
                return v.toString(16);
            });
        }
    }
]);

NuGet Package

The custom view has been released as a Nuget package

Install-Package
Umbraco.Community.FullCalendar.Block
dotnet add package
Umbraco.Community.FullCalendar.Block

In this blog post I explain how to implement an email validation flow for Member registration.

In part 2 of my Implementing a Forgot password for members I explain how to implement the IMemberMailService to send the reset password email.

How to implement a ForgotPassword process for Umbraco members in Umbraco 9+

Custom views give you complete control over how a Block is rendered in the backoffice and this enables you to give a better representation of the content. In this article I will explain how I created a custom Block view based on the fullcalendar.io javascript library to display events in the backoffice.

These are my experiences of creating an Umbraco package for the MediaWiz Forums, using package targets, razor class libraries, static web assets and template views.

Many thanks go to Kevin Jump and Luuk Peters without whose help I would probably have given up.