Creating JavaScript/HTML5 Sitefinity Widgets

Today I am going to introduce to you a new way of building Sitefinity widgets. A couple of them are actually live on our new home page. The image carousel and blog rotator are JavaScript widgets stored in shared content blocks. It's only a few lines of script and very elegant. See for yourself:

<div id="myCarousel" class="carousel slide hidden-phone"> <div class="carousel-inner"> <div class="loading-page"></div> </div> <!-- Carousel nav --> <a class="carousel-control left" href="#myCarousel" data-slide="prev">‹</a> <a class="carousel-control right" href="#myCarousel" data-slide="next">›</a> </div>
 
<script id="home_slider_template" type="text/x-kendo-template">
# for (var i = 0; i < data.length; i++) { #
<div class="item">
  #if (data[i].Link) { #
    <a href="#= data[i].Link #">
  #}#
<img src="#= data[i].Url #" class="bigImage" alt="#= data[i].AlternativeText #" />
  #if (data[i].Link) { #
    </a>
  #}#
  #if (data[i].Description) { #
    <div class="carousel-caption">
       <h4>#= data[i].AlternativeText #</h4>
        <p>#= data[i].Description #</p>
    </div>
  #}#
</div>
# } #
</script>
 
<script type="text/javascript">
require([
  'jquery',
  'kendoweb',
  'bootstrap'
], function ($, kendo) {
  $.getJSON('/api/images/getbyalbum?name=home slider', function (data) {
    var template = kendo.template($('#home_slider_template').html());
    $('#myCarousel .carousel-inner')
      .html(template(data))
      .parent()
      .carousel({
        interval: 6000
      })
      .find('.item:first-child')
      .addClass('active');
  });
});
</script>

The widget can be split up into three parts. The first section is HTML that Bootstrap told us to write for an image carousel. The next section is a Kendo template for our data. And the last section is our RequireJS JavaScript module that calls a web service for the Image Library.

What's really interesting is the JavaScript libraries like jQuery and Kendo are added to the page simply by including it in the array parameter of the "require" function. The second parameter is the callback function where you get to use your dependencies after they have been loaded asynchronously. Loading scripts asynchronously has vast implications (good ones):
  • Handle dependencies without duplicating or conflicts with the rest of the page (such as Sitefinity's jQuery or a legacy script)
  • Request scripts via the "require" function instead of in the page head or footer
  • Load scripts only when needed instead of all upfront
  • Nice bite-sized chunks of modules to pass around (self-contained!!)
Let's take a look underneath the hood of this architecture to see what's happening. Once you have this foundation in place, you will be able to whiz through new widgets (and sites) like butter! Something we've been calling "FalafelJS" around here :)

Web API Web Services

Think of web services as being the data layer. This will completely decouple the database from the interface in your mind (as it should). For the type of web service to use, you can use the built-in web services in Sitefinity or create your own via Web API. I have outlined the steps in a previous post for going the Web API route, so I will just show you the code for the Images web service here:

namespace SitefinityWebApp.Mvc.Api
{
    /// <summary>
    /// REST service for images
    /// </summary>
    public class ImagesController : ApiController
    {
        private LibrariesManager _manager = null;
 
        /// <summary>
        /// Gets the libraries manager.
        /// </summary>
        /// <value>
        /// The manager.
        /// </value>
        public LibrariesManager Manager
        {
            get
            {
                return _manager ??
                    (_manager = LibrariesManager.GetManager());
            }
        }
 
        /// <summary>
        /// Gets images by album.
        /// </summary>
        /// <returns></returns>
        public IEnumerable<ImageModel> GetByAlbum(string name)
        {
            var output = new List<ImageModel>();
 
            this.Manager.GetImages()
                .Where(s => s.Album.Title.Equals(name, StringComparison.OrdinalIgnoreCase)
                    && s.Status == ContentLifecycleStatus.Live)
                .OrderBy(i => i.Ordinal)
                .ToList()
                .ForEach(i => output.Add(new ImageModel(i)));
 
            return output;
        }
    }
}
 
namespace SitefinityWebApp.Mvc.Models
{
    public class ImageModel
    {
        public Guid Id { get; set; }
        public string Title { get; set; }
        public string Description { get; set; }
        public int Width { get; set; }
        public int Height { get; set; }
        public string Url { get; set; }
        public string AlternativeText { get; set; }
        public string Author { get; set; }
        public float Ordinal { get; set; }
        public string Thumbnail { get; set; }
        public long TotalSize { get; set; }
        public string Album { get; set; }
 
        //CUSTOM FIELDS
        public string Link { get; set; }
 
        public ImageModel()
        {
        }
 
        public ImageModel(Image sfImage)
        {
            if (sfImage != null)
            {
                this.Id = sfImage.Id;
                this.Title = sfImage.Title;
                this.Description = sfImage.Description;
                this.Width = sfImage.Width;
                this.Height = sfImage.Height;
                this.Url = sfImage.Url;
                this.AlternativeText = sfImage.AlternativeText;
                this.Author = sfImage.Author;
                this.Ordinal = sfImage.Ordinal;
                this.Thumbnail = sfImage.ThumbnailUrl;
                this.TotalSize = sfImage.TotalSize;
                this.Album = sfImage.Album.Title;
 
                //SET CUSTOM FIELDS
                if (sfImage.DoesFieldExist("Link"))
                    this.Link = sfImage.GetValue<string>("Link");
            }
        }
    }
}

Now when you call "~/api/images/getbyalbum?name=home slider" you get this scrumptious JSON data:

[
    {
        "Id": "a8ebaf2a-36e3-459c-99d9-91583e4ade8a",
        "Title": "telerik10HomeBanner",
        "Description": "",
        "Width": 1200,
        "Height": 435,
        "AlternativeText": "",
        "Author": "",
        "Ordinal": 0.5,
        "TotalSize": 121637,
        "Album": "Home Slider",
        "Link": "/consulting/telerik-services"
    },
    {
        "Id": "a6586fee-a81a-44a4-9e5a-a13db04bf05d",
        "Title": "sitefinityHomeBannerMetro",
        "Description": "",
        "Width": 1200,
        "Height": 435,
        "AlternativeText": "",
        "Author": "",
        "Ordinal": 0.75,
        "TotalSize": 129452,
        "Album": "Home Slider",
        "Link": "/consulting/sitefinity-services"
    },    {
        "Id": "c388f8d8-19af-47c3-a268-9ad26a3dfb50",
        "Title": "mobileHomeBannerMetro",
        "Description": "",
        "Width": 1200,
        "Height": 435,
        "AlternativeText": "",
        "Author": "",
        "Ordinal": 131,
        "TotalSize": 129628,
        "Album": "Home Slider",
        "Link": "/consulting/mobile-services"
    }
]

Add RequireJS and CSS to Base Template

Next I want to discuss how the JavaScript AMD is working. As documented in RequireJS, I simply add require.js and my main.js to the page template to fire up AMD for all your pages. Remember, from here everything will be asynchronously loaded… even jQuery!

These are the files to add to your template:
~/Custom/FalafelJS/libs/bootstrap/css/bootstrap.min.css
~/Custom/FalafelJS/libs/bootstrap/css/bootstrap-responsive.min.css
~/Custom/FalafelJS/libs/require/require.js
~/Custom/FalafelJS/main.js

The main.js file is essentially a config file for RequireJS. Below is what it looks like:

// Require.js allows us to configure shortcut alias
require.config({
    paths: {
        jquery: [
            '//ajax.googleapis.com/ajax/libs/jquery/1.8.3/jquery.min',
            //If the CDN location fails, load from this location
            'libs/jquery/jquery.min'
        ],
        underscore: 'libs/underscore/lodash.min',
        can: 'libs/canjs/can.jquery.min',
        kendoweb: 'libs/kendoui/js/kendo.web.min',
        bootstrap: 'libs/bootstrap/bootstrap.min',
        innerfade: 'libs/innerfade/jquery.innerfade'
    },
 
    // The shim config allows us to configure dependencies for
    // scripts that do not call define() to register a module
    shim: {
        underscore: {
            exports: '_'
        },
        can: {
            deps: [
'jquery'
            ],
            exports: 'can'
        },
        kendoweb: {
            deps: ['jquery'],
            exports: 'kendo'
        },
        bootstrap: ['jquery'],
innerfade: ['jquery']
    }
});

I am simply giving aliases to my libraries so I can call them by shortcut names in my modules. Then I am handling the non-AMD compliant scripts in the shim section (jQuery 1.7+ is already AMD-compliant and Kendo will also be in the next Q1 release!). For more details about JavaScript AMD and Sitefinity, read my previous post that covers this.

Once require.js and main.js are called, I can now create JavaScript modules in my Sitefinity pages without juggling scripts in the page templates or even compiling or redeploying!!

Show Me the Money!

Creating JavaScript widgets from here is fast and easy. In the admin, go to Content > Content blocks and create a new content block with the JavaScript module and HTML:



The next step is to add this shared content block on any page you would like to use it:








That is how easy it is to create JavaScript/HTML5 widgets from here! No compiling, no deploying, no JavaScript conflicts!! And you get to use all your favorite jQuery plugins and JavaScript libraries by just specifying it in the first "require" parameter.

Caveats

Nothing is perfect, but I think most of the caveats have to do with the administration side of things. The widgets themselves are robust and reusable which are key points. Here are some caveats worth mentioning:

  • The first one is with the RadEditor. The HTML editor isn't as pretty as the Widget Template editor in Sitefinity. It could possible be integrated which would be awesome. For now, you must allow scripts in the RadEditor by going Administration > Settings > Advanced > Content > Controls > ContentBackend > Views > ContentBackendInsert(ContentBackendEdit) > Sections - MainSection > Fields > Content > Rad Editor's content filters. Instead of using the default, we will manually load the defaults minus the "RemoveScripts" filter like this: EncodeScripts,FixUlBoldItalic,FixEnclosingP,IECleanAnchors,MozEmStrong,ConvertFontToSpan,ConvertToXhtml, IndentHTMLContent,ConvertCharactersToEntities,PdfExportFilter
  • The JavaScript widgets does not run in the page editor. The RadEditor is just blank or displays the raw HTML templates. You have to explain to your admins that there is JavaScript and to preview the page to see it.
  • There is no designer for the JavaScript widgets for the admins. The HTML source has to modified to update the JavaScript widget properties. I believe one can put a designer in place via HTML/CSS and have it activated if viewed in the RadEditor's designer, but I have not explored this (leave comments if you do :)

Conclusion

Creating JavaScript/HTML5 widgets for Sitefinity is something that can evolve despite the small caveats. The benefits are huge and is on the path for the next generation of the web. Windows 8 has taken heed and allows apps to be built with HTML5/JavaScript. Now you can do the same with Sitefinity!

Happy Coding!!
comments powered by Disqus

Get weekly updates in your inbox!