Going Fullscreen with Angular Directives

I recently ran across a very nice post on the HTML5 Fullscreen API on David Walsh's Blog and saw an opportunity for incorporating this functionality into an Angular app, utilizing the notion of directive controllers and child directives. Considering David's post does such an excellent job explaining how the Fullscreen API works, please read it first if you're not familiar with this API.


"Parent" Directive and its Controller###

The first thing we need to understand is our strategy in using this API and why a directive (or set of directives, in this case) may be a good fit.

Angular directives are very powerful - they can add functionality to an existing scope, create their own isolated scope, and even communicate with each other by requiring each others' controller. For more detailed information on how this works, check out the Angular docs on directives; near the bottom of the page you'll see the section on "Creating Directives that Communicate".

Considering that we can create our directives in such a way that they can communicate with each other, one strategy we could use for incorporating the Fullscreen API is to apply a "parent" directive to the outer-most element of a particular section that should be able to go fullscreen, and have another element inside of that element that has a "child" directive applied to it for toggling fullscreen state. The parent directive will expose a function on its controller to initiate the fullscreen state, which the child directive will use when the toggle is activated. There will also be an event handler exposed from the parent directive so that the child directive can react whenever the container's fullscreen status changes.

Note: It's not necessary to break these up into two separate directives. But, it is simple enough functionality that it makes for a good example to show how this works.

(function(d,ng){
	ng.module('demo').directive('fullscreenContainer', fullscreenContainer);
    
    function fullscreenContainer(){
    	//Notice that we're not doing an isolate scope here - there's really no reason to.
    	var directiveCfg = {
        	restrict: 'EA',
            controller: fullscreenContainerCtrl
        };
        
        function fullscreenContainerCtrl($scope, $element, $attrs){
        	//get a handle to the native DOM element
            var el = $element[0];
            //alias "this" for use in event handlers, etc...
            var thisCtrl = this;
                        
            var fullScreenState = {
            	isFullScreen: false
            };
            
            //add in a hook in case the child directive wants to react
            //when this element enters/exits fullscreen state
            //note we're adding the property to "this" and not $scope
            thisCtrl.onFullScreenChange = null;
            
            //populate whether the fullscreen API is even supported
            fullScreenState.canDoFullScreen = d.fullscreenEnabled || 
            	d.webkitFullscreenEnabled || 
                d.mozFullScreenEnabled || 
                d.msFullscreenEnabled;
                
            if(fullScreenState.canDoFullScreen) {
            	//get the function on the element for making it fullscreen
                //currently, still varied by vendor
                var fullScreenFn = 
                	(el.requestFullscreen && 'requestFullscreen') ||
                    (el.mozRequestFullScreen && 'mozRequestFullScreen') ||
                    (el.webkitRequestFullscreen && 'webkitRequestFullscreen') ||
                    (el.msRequestFullscreen && 'msRequestFullscreen');
                                    
                //again, expose a method so that the child directive can access it
                thisCtrl.makeElementFullScreen = function() {
                	el[fullScreenFn]();
                };
                
                //document emits an event when fullscreen state changes
				//also still varied by vendor
                var fsChangeFns = [
                	'fullscreenchange', 
                    'webkitfullscreenchange', 
                    'mozfullscreenchange', 
                    'MSFullscreenChange'
                ];
                
                //event handler
				function fsChanged(evt){
					//Obtain the fullscreen element (if there is one)
					//again, varies by vendor
					var fullScreenEl = d.fullscreenElement ||
						d.webkitFullscreenElement ||
						d.mozFullScreenElement ||
						d.msFullscreenElement;

					var oldState = fullScreenState.isFullScreen;
					fullScreenState.isFullScreen = (fullScreenEl && fullScreenEl === el);
					if(oldState !== fullScreenState && typeof thisCtrl.onFullScreenChange === 'function') {
						//if the child directive provided a callback, invoke it and provide the new state
						thisCtrl.onFullScreenChange(fullScreenState.isFullScreen);
					}                    
             	}
                
                //add event listener for each of those events
				fsChangeFns.forEach(function addFullScreenChangeEvtListener(fsEvtName){
					d.addEventListener(fsEvtName, fsChanged);
				});
            } else {
            	//here, we could have a fallback for when the FullScreen API isn't supported
                //maybe absolutely position the element, add a backdrop, w/e
            }
        }
        
        return directiveCfg;
    }
})(document, angular);

"Child" Directive and Requiring the "Parent"###

Now that we have the directive that will take care of implementing how we toggle the fullscreen state on the DOM element, we need something that triggers when this happens. This is going to be a super simple directive, as it only does two things: 1) invoke the fullscreen state and 2) react when the fullscreen state changes.

(function(d, ng){
	ng.directive('fullscreenToggle', fullscreenToggle);
    
    function fullscreenToggle(){
    	return {
        	restrict: 'EA',
            //here's where we "require" the controller of the fullscreenContainer directive!
            //prefixed symbols, such as "^", are essentially telling angular where to look for the required controller 
            //i.e. on same element, on a parent element, etc...
            require: '^fullscreenContainer',
            //Now, if the required controller is found, it's passed into the arguments for the link function!
            link: function(scope, element, attrs, fullscreenContainerCtrl) {
            	//we only care to attach event handlers, etc.. 
                //if the parent ctrl has the "makeElementFullScreen" function defined
                if(typeof fullscreenContainerCtrl.makeElementFullScreen === 'function') {
                	//wire up event handler for when the element is clicked
                	element.on('click', function fullscreenToggleClicked() {
                    	fullscreenContainerCtrl.makeElementFullScreen();
                    });
                    //also populate the callback function for when fullscreen state changes
                    fullscreenContainerCtrl.onFullScreenChange = function fullscreenStateChanged(isFullScreen) {
                    	//if this element's ancestor is fullscreen, hide the toggle
                        if(isFullScreen){
                        	element.hide();
                        } else {
                        	element.show();
                        }                        
                    };                    
                } else {
                	//then there is no method for making the ancestor fullscreen - either API or fallback...
                    element.hide();
                }
            }
        };
    }
})(document, angular);

As I mentioned earlier, it isn't necessary to use two separate directives for this functionality. However, one benefit would be that you could potentially have multiple triggers that are simple to implement and just require the same parent directive. Maybe one piece of content is made fullscreen by a click, while another is made fullscreen by a specific key command, and another by pinch/zoom gestures on a touch device.

One other small detail I wanted to mention is with regards to styling the elements when they are in fullscreen state. This can be done with the following pseudo-selectors: :-webkit-full-screen, :-moz-full-screen, :-ms-fullscreen, :full-screen, and :fullscreen. One thing I found odd when styling these elements is that the styles wouldn't work if I comma-separated the selectors into a single CSS rule - I had to use them individually for them to work properly. Other than that, seems to work pretty nicely in modern browsers!

Check out the , and share your thoughts in the comments!