Moving a page element without affecting its scope in AngularJS

It is often important to simultaneously show a fairly large number of elements on a web page to provide users with the most concise information possible. In order to make all those elements visible on a screen, their functionality usually has to be considerably limited. That is when the ability to open every separate element in a fullscreen view can be very helpful. This post shows how this can be achieved in AngularJS, with two custom directives which allow to move elements to a fullscreen view without changing their scope.

Here is an example of the scenario described above.

 Picture 1. Charts

As it can be seen on the Picture 1, there are three charts on the page, rendered side by side. Let’s assume that the page is designed to provide users with some overall statistical information. So, it is necessary to display all the charts on the same page at once. But, at the same time, we would like to allow users to magnify a specific chart and turn off / on a chart line (here, charts were taken just as an example, they could be replaced with any other elements, such as grids, or forms).

Ideally, there should be an ability to display each chart in a fullscreen mode without affecting the other charts or reloading the page. Another significant thing, it would be nice if the chart had the same scope regardless of its state (whether it is displayed in a fullscreen view or as a part of a page).

Writing the directives

Let’s get to writing the directives. The first directive, called fullscreenBlock, compiles as a button and serves as a trigger for executing the second modalWrapper directive which does the actual work of moving the element to a fullscreen view:

fullscreenBlock

directives.directive('fullscreenBlock', ['$compile', function ($compile) {
    var jqLite = angular.element;
    var link = function (scope, element, attrs) {
        // Compiles the default 'fullscreen' button for opening the fullscreen view.
        // This fullscreen button can be overridden by passing a custom html string to
        // the `fullscreen-block` attribute.
        var fullscreenButton = function () {
            return jqLite($compile(
                '<div class="row">' +
                    '<span class="pull-right">' +
                        // scope information
                        '<p>(scope id: <b>' + scope.$id + '</b>)</p>' +
                        '<p>(parent scope id: <b>' + scope.$parent.$id + '</b>)</p>' +
                        '<span name="fullscreen" ng-click="fullscreenMode($event)"' +
                                'tooltip="Expand" style="cursor: pointer">' +
                            '<i class="glyphicon glyphicon-fullscreen"></i>' +
                        '</span>' +
                    '</span>' +
                '</div>'
            )(scope));
        };
        // if there is no custom html passed to the attribute, default one is used
        element.prepend((attrs.fullscreenBlock.length > 0) ?
            attrs.fullscreenBlock :
            fullscreenButton());
    };
    // This function helps to find the first parent element which contains the
    // 'fullscreen-block' attribute. The function can be replaced with JQuery
    // `closest()` method if JQuery library is used.
    var closest = function (element) {
        return (jqLite(element).parent().attr('fullscreen-block') !== undefined) ?
            jqLite(element).parent() :
            closest(jqLite(element).parent());
    };
    var controller = function ($scope) {
        // handles the ng-click event
        $scope.fullscreenMode = function (event) {
            var modalWrapper = '<modal-wrapper></modal-wrapper>';
            jqLite(closest(event.target)).wrap(modalWrapper);
            $compile(modalWrapper)($scope);
        };
    };
    return {
        link: link,
        controller: ['$scope', controller]
    }
}]);Code language: PHP (php)

modalWrapper

directives.directive('modalWrapper',["$compile", "$document",
    function ($compile, $document) {
    var jqLite = angular.element;
    /*
     * The modal window template
     */
    var template =
        '<div id="modal-fullscreen" class="modal modal-fullscreen"' +
                'style="background: white">' +
            '<div class="modal-header">' +
                '<button type="button" class="close" ng-click="close()">&times;</button>' +
            '</div>' +
            '<div class="modal-body row" style="margin: 0; display: flex;"></div>' +
        '</div>';
    var chart;
    /*
     * The link function.
     */
    var link = function (scope, element) {
        scope.open();
        // watches if modal is opened
        scope.$watch('showModal', function (newVal) {
            var modalEl = jqLite('modal-wrapper');
            var fullscreenIcon = modalEl.find('[name="fullscreen"]');
            // if modal is opened
            if (newVal) {
                // hides the fullscreen icon for the modal
                fullscreenIcon.addClass("ng-hide");
                // inserts content from the page to the body of modal element.
                // JQuery's `insertBefore()` method can be used as well.
                modalEl[0].parentNode.insertBefore(element[0], modalEl[0]);
                // shows content a fullscreen view
                var content = modalEl.children();
                // puts the element inside the modal
                element.find(".modal-body").append(content);
                // removes the <modal-wrapper> tag
                modalEl.remove();
                // makes the modal visible
                jqLite('#modal-fullscreen').show();
                // changes the element width
                // (`expand` is a custom class that sets width to 100%)
                content.addClass('expand');
                // initializes the chart variable
                // (this line is specific to this concrete case)
                chart = content.scope()[element.find('canvas').attr('id')];
            // if modal is closed
            } else if (!newVal) {
                // shows the 'fullscreen' button after the modal is closed
                fullscreenIcon.removeClass("ng-hide");
                // looks up the element
                var modalBody = modalEl.find(".modal-body").children();
                // inserts content from modal back to the page.
                // JQuery's `insertBefore()` method can be used as well.
                modalEl[0].parentNode.insertBefore(modalBody[0], modalEl[0]);
                // removes the modal
                modalEl.remove();
                // changes the element width
                // (`expand` is a custom class that sets width to 100%)
                content.removeClass('expand');
            }
            // triggers the chart resizing
            // (this line is specific to this concrete case)
            chart.resize(chart.render, true);
        })
    };
    /*
     * The controller function.
     */
    var controller = function ($scope) {
        $scope.open = function () {
            $scope.showModal = true;
        };
        $scope.close = function () {
            $scope.showModal = false;
        };
        // closes the modal when the back button is clicked
        $scope.$on('$locationChangeStart', function(event) {
            if ($scope.showModal) {
                event.preventDefault();
                $scope.close();
            }
        });
    };
    return {
        restrict: 'E',
        scope: {},
        template: template,
        link: link,
        controller: controller
    }
}]);Code language: PHP (php)

Here is a description of the directives shown above:

  • fullscreenBlock – compiles and inserts the ‘fullscreen’ button — clicking this button wraps the enclosed element in the <modal-wrapper> directive. The directive is represented as an attribute which can receive an HTML string in order to render a custom button. This button should have the ng-click="fullscreenMode($event)" attribute defined, for instance:<div fullscreen-block='<input type="button" ng-click="fullscreenMode($event)"/>'>
  • modalWrapper – responsible for moving a selected element to the modal template, as a result the chart is displayed in a fullscreen without affecting the other elements in the DOM. The important part is that the chart is just moved from one place and to another, which allows you not to create a new scope, but to work with the initial one. Closing the modal window puts the chart back, leaving its state untouched.

Using directive in HTML

Next, let’s see how to use the fullscreen-block directive in html.

 <div class="row" ng-controller="FullscreenModeController">
        <div fullscreen-block ng-repeat="chart in data" class="col-md-4">
            <div class="row">
                <div class="col-md-10">
                    <canvas id="{{chart.name}}"></canvas>
                </div>
                <div ng-if="showLegend()" class="col-md-2">
                    <div ng-repeat="dataset in chart.datasets" class="row">
                        <label>{{dataset.label}}</label>
                        <input type="checkbox" ng-model="dataset.show"
                            ng-change="changeDataset(chart, dataset)" />
                    </div>
                </div>
            </div>
        </div>
    </div>Code language: HTML, XML (xml)

The key point here is putting the <fullscreen-block> directive on the top of the element that is supposed to be scaled (the directive is a marker that encloses the element which should be moved and displayed in a fullscreen view).

Result

Finally, let’s execute the code:

 Picture 2. A few charts that have the fullscreen-block attribute defined

 Picture 3. The fullscreen view that shows one of the charts

As you can see on the Picture 2, the fullscreen directive inserts the button (the top-right corner of the chart) which allows to magnify the chart element by displaying it in the fullscreen mode (Picture 3).

Note that Twitter Bootstrap’s modal-fullscreen class was used for the fullscreen view.

You can play around with the jsFiddle example, try to open one of the charts, turn off a chart line, and then close the preview window. You will see that all the changes that have been made in the fullscreen mode are applied because the chart scope is always the same.

The full source code is available on GitHub.

Lead JavaScript Developer