Create a customized Javascript code documentation

Introduction

In this tutorial I will cover the basics of how you can generate a Javascript code documentation for your codebase using the Grunt-plugin grunt-jsdoc. It works pretty much like Java-doc. Comments are added to your code that covers information about classes and functions, jsdoc will then generate a full API documentation using the information from these comments that can be navigated through in your browser. I will also cover how you can customize the default template of this JsDoc-webpage to match your desired look. In this tutorial I will use Grunt as my build tool. Also the site I am generating JsDoc for is built using Angular, but obviously you could still use this tutorial for any Javascript codebase. To see an example how this may look please see the JsDoc I generated for my own portfolio website.

Setup

First of all let’s add jsdoc to our dependency list in package.json.

{
    "name": "myPage",
    "version": "1.0.0",
    "repository": {
        "type": "git",
        "url": "https://github.com/me/myRepo"
    },
    "license": "MIT",
    "devDependencies": {
        "angular": "1.3.15",
        "grunt": "^0.4.5",
        "load-grunt-config": "^0.17.2",
        "load-grunt-tasks": "^3.3.0",
        "grunt-jsdoc": "1.1.0"
    }
}

Next add jsdoc as a Grunt build job to your Gruntfile.js.

module.exports = function(grunt) {
    grunt.initConfig({
        pkg: grunt.file.readJSON('package.json'),
        jsdoc: {
            dist: {
                src: [
                    'js/angular/**/*.js'
                ],
                options: {
                    destination: 'docs',
                    recurse: true
                }
            }
        }
    });
    grunt.loadNpmTasks('grunt-jsdoc');
    grunt.registerTask('build-docs', [
        'jsdoc'
    ]);
};

Install npm modules:

npm install

Now try running the newly created Gruntjob ‘build-docs’ just to see that you don’t get any errors. The output should be generated into the folder ./docs (as configured in options). The jsdoc should cover all Javascript code in the angular-folder, however since we haven’t added any jsdoc-comments yet the generated jsdoc site should be empty.

grunt build-docs

If you browse to localhost/docs in your browser you now see an empty jsdoc template. Now you are ready to start adding the jsdoc comments to your code.

Jsdoc global definitions

First of all we will ad a file for jsdoc global definitions, references we will use in the comments we add to our js-files. Create a js-file called jsdoc-global-definitions.js to your js-folder. Then add this file to your sources in the jsdoc Grunt task.

module.exports = function(grunt) {
    grunt.initConfig({
        pkg: grunt.file.readJSON('package.json'),
        jsdoc: {
            dist: {
                src: [
                    'js/angular/**/*.js',
                    'js/jsdoc-global-definitions.js'
                ],
                options: {
                    destination: 'docs',
                    recurse: true
                }
            }
        }
    });
    grunt.loadNpmTasks('grunt-jsdoc');
    grunt.registerTask('build-docs', [
        'jsdoc'
    ]);
};

In the file jsdoc-global-definitions.js I have the following configuration:

/**
 * AngularJS is a structural framework for dynamic web apps. It lets you use HTML as your template language and lets you extend HTML's syntax to express your application's components clearly and succinctly. Angular's data binding and dependency injection eliminate much of the code you would otherwise have to write. And it all happens within the browser, making it an ideal partner with any server technology.
 * @external "Angular"
 * @see {@link https://docs.angularjs.org/guide|Angular documentation}
 */

/**
 * @namespace controllers
 * @description Controllers linked to a specific view.
 */

/**
 * @namespace directives
 * @description Reusable directives that can be used by multiple views.
 */

/**
 * @namespace filters
 * @description Reusable filters that can be used by {@link controllers}, {@link directives} and {@link services}.
 */

/**
 * @namespace services
 * @description Reusable services that can be used by {@link controllers}, {@link directives} and {@link filters}.
 */

As you can see the file contains only comments written with the jsdoc syntax. Here we define global namespaces that we will later use when commenting the javascript codebase. Obviously you will have to modify your global definition file to fit the design pattern and frontend framework you have chosen (if any). Like I said, in this tutorial I am assuming that that the website is Angular-based, which is why I have defined the following namespaces: controllers, directives, filters and services. Each namespace also have a fitting description. We also have one external library dependecy, namely Angular, which is why I also added the first comment that has a link to Angulars developer documentation. This will list Angular as an external library. Also note that in the description you can link to other namespaces by writing {@link namespace}. To read more about jsdoc links please read this.

Adding jsdoc comments to your code

Here is an example of jsdoc comments written in jsdoc syntax added to an Angular controller:

(function() {
    var app = angular.module('myPage');
    /**
     * @constructor MyController
     * @memberof controllers
     * @description Controller for my webpage
     * @param {$scope} $scope - See {@link https://code.angularjs.org/1.2.26/docs/api/ng/type/$rootScope.Scope}
     * @param {services.myService} myService - Services used for cool stuff
     */
    var MyController = ['$scope', 'myService', function($scope, myService) {
        /**
         * @function controllers.MyController#getFibonacciNumberByIndex
         * @description Gets a number in the Fibonacci sequence by index
         * @param {number} The index of the Fibonacci sequence
         * @returns {number} The calculated Fibonacci number
         */
        function getFibonacciNumberByIndex(index) {
            var list = [1, 1];
            for (var i = 0; i <= index; i++) {
                var num1 = list[list.length - 1];
                var num2 = list[list.length - 2];
                var next = num1 + num2;
                list.push(next);
            }
            return list[index];
        }
    }];
    app.controller('MyController', MyController);
}());

As you can see in the controller MyController the function MyController is treated as the constructor, therefore the @constructor comment. I have also added the comment @memberof controllers which is referring to the namespace “controllers” that we defined earlier. MyController has two dependencies, $scope and myService, therefore these are documented as @param. Since $scope is a built-in Angular module I add a link to the documentation of $scope using the {@link} syntax. The other dependency is to to an Angular service in OUR codebase, therefore it is referenced to as services.myService (services is the namespace), this will generate a link to the documenation of myService.

The controller contains one function that returns a Fibonacci number by give index. For this function we define it using @function controllers.MyController#getFibonacciNumberByIndex. The syntax goes: namespace.fileName#functionName. The function takes in one parameter of the expected type {number} and returns a response also of the type {number}. To learn more about other parameter/return types read this.

Now generate your jsdoc by yet again running

grunt build-docs

and then check the generated jsdoc in localhost/docs. You should now see a menu to the right with external sources (Angular), Internal sources (MyService) and defined namespaces (controllers, directives, filters and services). If you click on MyController you should be able to see detailed description of the constructor and the function getFibonacciNumberByIndex.

Custom startpage

Right now the startpage of your jsdoc documentation should be blank, but you can add a custom template by creating a file named jsdoc.md and add a reference to it in your Grunt task:

module.exports = function(grunt) {
    grunt.initConfig({
        pkg: grunt.file.readJSON('package.json'),
        jsdoc: {
            dist: {
                src: [
                    'js/angular/**/*.js',
                    'js/jsdoc-global-definitions.js',
                    'js/jsdoc.md'
                ],
                options: {
                    destination: 'docs',
                    recurse: true
                }
            }
        }
    });
    grunt.loadNpmTasks('grunt-jsdoc');
    grunt.registerTask('build-docs', [
        'jsdoc'
    ]);
};

The jsdoc.md file should be written using Markdown syntax, same syntax used for Github readme-files. To learn more about it read here.

For an example, see how my file looks.

Custom stylings

If you see my jsdoc for my portfolio website you’ll see that it is a bit more colorful than the regular grey template. If you also wan’t your jsdoc page to have a more fabolous look follow these steps.

After you installed the grunt-jsdoc module using npm you should then be able to find the following folder: /node_modules/jsdoc/templates/default.

Copy this folder to your repository root folder:

sudo cp -rf /myRepo/node_modules/jsdoc/templates/default /myRepo/myTemplate

Now add the following setting to your Gruntfile.js:

module.exports = function(grunt) {
    grunt.initConfig({
        pkg: grunt.file.readJSON('package.json'),
        jsdoc: {
            dist: {
                src: [
                    'js/angular/**/*.js',
                    'js/jsdoc-global-definitions.js',
                    'js/jsdoc.md'
                ],
                options: {
                    destination: 'docs',
                    recurse: true,
                    template: './myTemplate'
                }
            }
        }
    });
    grunt.loadNpmTasks('grunt-jsdoc');
    grunt.registerTask('build-docs', [
        'jsdoc'
    ]);
};

Jsdoc will now instead of the default template in node_modules use template found in the myTemplate directory. Now you can modify this template as much as you want, this template folder includes css, scripts and markup that can be modified as you please.

That’s it! I hope you enjoyed this tutorial 🙂 🙂

Advertisement

Create an image/video gallery using Angular and Photoswipe

Introduction

In this tutorial I will cover how you can create an art gallery for your website using Angular and Photoswipe. Angular is used for the presentation logic, we will handle a list of items as a scope-variable and an Angular controller for fetching the list, perhaps cleaning the data and so on. Photoswipe is the library used in this tutorial the “gallery functionality”, which includes displaying an image in fullscreen, swiping left and right and so on. The reason why I chose Photoswipe is because it is easy to use, very customizable and also works very well on mobile devices. You can read more about Photoswipe here.

To see a demo of how a gallery like this can look, please view my art gallery that I build using these technologies.

Displaying videos

When I first started working on this I had an idea that I also wanted to be able to show videos in the gallery, not just images. If an item in the gallery was a video a play-icon should be displayed over the thumbnail to indicate it is a video, and if opened the video should automatically start playing and be stopped when closing or swiping to next item. Unfortunately I found out that Photoswipe has no video support, it is just for images. However I managed to find a solution to this that I am happy with. In this tutorial I will cover what I did.

Setup Photoswipe

First of all, a default Photoswipe template must be added to your site. This template contains the markup used for the gallery interface. It should just be copypasgted, do not change classes and id’s in this template as it will mess up Photoswipe.

Here is how your index-file could look (in this case I am using php for the include function):

<!DOCTYPE html>
<html xmlns="http:/www.w3.org/1999/xhtml" lang="en">
    <head>
        <link rel="stylesheet" href="css/myCss.css">
        <title>My cool gallery</title>
    </head>
    <body ng-app="myApp" ng-controller="galleryController">
        <?php include("photoswipe.php"); ?>
    </body>
</html>

This is the contents of the file photoswipe.php:

<!-- Root element of PhotoSwipe. Must have class pswp. -->
<div class="pswp" tabindex="-1" role="dialog" aria-hidden="true">
    <!-- Background of PhotoSwipe.          It's a separate element as animating opacity is faster than rgba(). -->
<div class="pswp__bg"></div>
<!-- Slides wrapper with overflow:hidden. -->
<div class="pswp__scroll-wrap">
        <!-- Container that holds slides.             PhotoSwipe keeps only 3 of them in the DOM to save memory.             Don't modify these 3 pswp__item elements, data is added later on. -->
<div class="pswp__container">
<div class="pswp__item"></div>
<div class="pswp__item"></div>
<div class="pswp__item"></div>
</div>
<!-- Default (PhotoSwipeUI_Default) interface on top of sliding area. Can be changed. -->
<div class="pswp__ui pswp__ui--hidden">
<div class="pswp__top-bar">
                <!--  Controls are self-explanatory. Order can be changed. -->
<div class="pswp__counter"></div>
<button class="pswp__button pswp__button--close" title="Close (Esc)"></button>
                <button class="pswp__button pswp__button--share" title="Share"></button>
                <button class="pswp__button pswp__button--fs" title="Toggle fullscreen"></button>
                <button class="pswp__button pswp__button--zoom" title="Zoom in/out"></button>
                <!-- Preloader demo http://codepen.io/dimsemenov/pen/yyBWoR -->
                <!-- element will get class pswp__preloader--active when preloader is running -->
<div class="pswp__preloader">
<div class="pswp__preloader__icn">
<div class="pswp__preloader__cut">
<div class="pswp__preloader__donut"></div>
</div>
</div>
</div>
</div>
<div class="pswp__share-modal pswp__share-modal--hidden pswp__single-tap">
<div class="pswp__share-tooltip"></div>
</div>
<button class="pswp__button pswp__button--arrow--left" title="Previous (arrow left)">
            </button>
            <button class="pswp__button pswp__button--arrow--right" title="Next (arrow right)">
            </button>
<div class="pswp__caption">
<div class="pswp__caption__center"></div>
</div>
</div>
</div>
</div>

 

Like I said, this is Photoswipes default template markup, I have not created this.

Now, as you can read in the Photoswipe getting started tutorial you should add references to the files photoswipe.css, default-skin.css, photoswipe.js and photoswipe-ui-default.min.js.

<!DOCTYPE html>
<html xmlns="http:/www.w3.org/1999/xhtml" lang="en">
    <head>
        <link rel="stylesheet" href="css/myCss.css">
        <link rel="stylesheet" href="css/photoswipe.css">
        <link rel="stylesheet" href="css/default-skin.css">
        <script type="text/javascript" src="js/photoswipe.js"></script>
        <script type="text/javascript" src="js/photoswipe-ui-default.min.js"></script>
        <title>My cool gallery</title>
    </head>
    <body ng-app="myApp" ng-controller="galleryController">
        <?php include("photoswipe.php"); ?>
    </body>
</html>

Photoswipe should now be setup and ready to use!

The controller

Time to get going! I will start by working on the Angular controller and the corresponding view. Here’s how the controller that we will use will look like:

(function() {
    var app = angular.module('myApp');

    var galleryController = ['$scope', function($scope) {

        // Public variables
        $scope.loading = true; // Set loading to false when initialization is done
        // The list of items to use
        $scope.list = [{
            thumbnail: 'thumbnail_for_item_1.png',
            bigImage: 'image_1.png',
            width: 640,
            height: 640,
            index: 0
        }, {
            thumbnail: 'thumbnail_for_item_2.png',
            bigImage: 'image_2.png',
            width: 640,
            height: 640,
            index: 1
        }, {
            thumbnail: 'thumbnail_for_image_1.png',
            video: 'video.mp4',
            width: 640,
            height: 640,
            index: 2
        }];
        // Private variables
        // The list that Photoswipe will use
        var itemsForPhotoswipe = [];

        // Public methods
        $scope.openPhotoSwipe = openPhotoSwipe;

        function openPhotoSwipe(artItem) {
            var pswpElement = document.querySelectorAll('.pswp')[0];

            // define options (if needed)
            var options = {
                // history & focus options are disabled on CodePen
                history: false,
                focus: false,
                index: artItem.index,

                showAnimationDuration: 0,
                hideAnimationDuration: 0
            };

            var gallery = new PhotoSwipe(pswpElement, 
                                         PhotoSwipeUI_Default,
                                         itemsForPhotoswipe,
                                         options);
            gallery.init();
        }

        function init() {
            for (var j = 0; j < $scope.list.length; j++) {
                if ($scope.list[j].bigImage) {
                    itemsForPhotoswipe.push({
                        src: $scope.list[j].bigImage,
                        w: $scope.list[j].width,
                        h: $scope.list[j].height,
                        index: $scope.list[j].index
                    });
                } else if ($scope.list[j].video) {
                    itemsForPhotoswipe.push({
                        html: '<div class="videoSlide">' +
                              '<div class="videoSlideInner">' +
                              '<video width="100%" height="100%" id="videoPlayer' +
                              $scope.list[j].index + '" controls><source src="' +
                              $scope.list[j].video.url + '" type="video/mp4" />' +
                              '</video>' +
                              '</div>' +
                              '</div>',
                        index: $scope.list[j].index
                    });
                }
            }

            $scope.loading = false;
        }

        init();
    }];

    app.controller('galleryController', galleryController);
}());

As you can see in this example I have a hardcoded list called “list” that contains all the items to be displayed. You could also choose to load a list from a JSON-file or something if you like. As you can see items can either have a “bigImage” property if it’s an image or a “video” property if it’s a video.

In the init-function I loop through this list and create the list, itemsForPhotoswipe, that Photoswipe will use. This is because Photoswipe want’s the input list to have a specific format for each item. Properties like src, w, h, index and html are what Photoswipe expects.

The html-property allows the user to have a custom html template for displaying the current item. As you can see in the controller above I am using this to have a customt template for video items. With this a HTML5 video element will be shown for videos. Here is the CSS I used for the videoSlide container:

.videoSlide {
    width: 100%;
    height: 100%;
    text-align: center;
    display: table;
}
.videoSlide .videoSlideInner {
    height: 100%;
    display: table-cell;
    vertical-align: middle;
}
.videoSlide .videoSlideInner video {
    max-width: 100%;
}

This will center the video vertically and horizontally and make sure it never stretches outside the screen. Add this to your css-file.

The function openPhotoSwipe is the function to be called when clicking on a thumbnail in the gallery. The function takes the entire item object as inparamter and then initializes Photoswipe.

The view

Now it’s time for the view. It should look a little bit like this:

<div ng-show="loading">
    <h1>CONTENT IS LOADING, PLEASE WAIT</h1>
</div>
<div data-ng-show="!loading">
    <img class="galleryImg"
    src="{{ item.video ? 'img/play_icon.png' : '' }}"
    width="150"
    height="150"
    style="background-image: url({{item.thumbnail}});"
    data-ng-click="openPhotoSwipe(item)"
    data-ng-repeat="item in list"/>
</div>

This will generate a wall of thumbnail tiles for items in our list scope-variable. I use an img-element for this because I wan’t to be able to set a src-attribute for a play icon overlaying the thumbnail for video items. The video items will still have the thumbnail beneath the play icon. To see an example of this just view my gallery.

Almost done, minor tweaks next

Like I mentioned before, Photoswipe doesn’t really support videos right off the bat. We have made sure that video items now will we displayed with our custom template but we need to add some presentation logic for playing/pausing the video.

To do this I modified the file photoswipe.js as it was the simplest way to solve this. You can download my modified file from my Github repository here: https://github.com/ToWelie89/martinsweb/blob/master/js/libs/photoswipe/photoswipe.js

To see exactly what I did to the file you can analyze this commit: https://github.com/ToWelie89/martinsweb/commit/ff5f68448da553fd4288bb12985c9ea0c46a5c75#diff-b82b7d7b1784a22c00d03fa5d457ed51

The functions stopVideo and startVideo handles playback and pausing. As you can see identifiers such as videoPlayer + index is used to find the specific video element by matching with the current items index value. That is why I set id of the video element to videoPlayer + $scope.list[j].index in the controller when looping through the list.

With these additional changes Photoswipe will now automatically start a video when clicking on a video item, stopping when closing the item or swiping to the next.

Thank you for reading this tutorial, I hope you liked it 🙂