Select Page

Some Real Talk

I am a programmer because ultimately I love to create things. And I find that my most gratifying creations come from taking a simple idea and iterating over and over on it and exploring different ideas around a single premise. I love that moment when you are able to create something new and cool with just a few small tweaks in the code or a tiny addition to the codebase. In this post we are going to do that by creating a realtime slideshow based on the previous slideshow posts here and here. We are going to introduce a master presenter role using Firebase to sync the current slide across all the connected clients. And my favorite part is that we are going to accomplish the realtime aspect of the slideshow in less than a dozen lines of code!

Before we get started, grab the source code from Github and create a Firebase endpoint to use for your own slideshow. Setting up a Firebase endpoint is free and easy and you can find all the information you need to do that at firebase.com.

The demo is not realtime as it would be chaos but I posted it so you could get a preview into what we are going to be working with.

Code Demo

The Basic Slideshow

he_man

We are not going to dig into every facet of the slideshow in this post as I have extensively covered them in previous posts but I will offer some commentary on what is specific to this implementation. The structure of the slides is fairly simple with a slide.title and a slide.subtitle property that we are displaying. Like the previous slideshows, we are iterating over the slides collection and keeping track of the current slide via $index.

<body ng-controller="MainCtrl">

<div class="slide slide-animation" 
            ng-if="isCurrentSlideIndex($index)"
            ng-repeat="slide in slides">

<div class="title">{{slide.title}}</div>
<pre><code>    <div class="subtitle">{{slide.subtitle}}</div>
</div>
</code></pre>
</body>

We are using the slide-animation class to wire up the transition animation from slide to slide. The interesting part about this particular animation is that we are using element.scope() to access the $scope object on the element to get the direction of the animation. We then dynamically define our animation based on that. I have hid the implementation details of where that property is stored by creating the getDirection method to return that value for us.

app.animation('.slide-animation', function ($window) {
    return {
        enter: function (element, done) {
            var scope = element.scope();

<pre><code>        var startPoint = $window.innerWidth;
        if(scope.getDirection() !== 'right') {
            startPoint = -startPoint;
        }
        TweenMax.fromTo(element, 0.5, {left: startPoint}, {left:0, onComplete: done });
    },

    leave: function (element, done) {
        var scope = element.scope();

        var endPoint = $window.innerWidth;
        if(scope.getDirection() === 'right') {
            endPoint = -endPoint;
        }

        TweenMax.to(element, 0.5, { left: endPoint, onComplete: done });
    }
};
</code></pre>
});

Because I knew that I wanted to synchronize an object across N connected clients, I defined the $scope.remoteSlide object to keep track of the current index and direction we need to animate. I then exposed $scope.remoteSlide.direction to slide-animation with the $scope.getDirection method as I previously stated.

app.controller('MainCtrl', function ($scope, $location, RemoteSlide) {
    //... OMITTED

<pre><code>function isCurrentSlideIndex(index) {
    return $scope.remoteSlide.currentIndex === index;
}

function prevSlide() {
    $scope.remoteSlide.direction = 'left';
    $scope.remoteSlide.currentIndex = ($scope.remoteSlide.currentIndex > 0)
        ? --$scope.remoteSlide.currentIndex : $scope.slides.length - 1;
}

function nextSlide() {
    $scope.remoteSlide.direction = 'right';
    $scope.remoteSlide.currentIndex = ($scope.remoteSlide.currentIndex < $scope.slides.length - 1)
        ? ++$scope.remoteSlide.currentIndex : 0;
}

function getDirection() {
    return $scope.remoteSlide.direction;
}

$scope.slides = [
    {id: 'slide00', title: 'Slide One', subtitle: 'With a supporting point!'},
    {id: 'slide01', title: 'Slide Two', subtitle: 'With a supporting point!'},
    {id: 'slide02', title: 'Slide Three', subtitle: 'With a supporting point!'},
    {id: 'slide03', title: 'Slide Four', subtitle: 'With a supporting point!'},
    {id: 'slide04', title: 'Slide Five', subtitle: 'With a supporting point!'}
];

$scope.remoteSlide = {
    currentIndex: 0,
    direction: 'left'
};

$scope.isCurrentSlideIndex = isCurrentSlideIndex;
$scope.getDirection = getDirection;

//... OMITTED
</code></pre>
});

And from here we are using isCurrentSlideIndex, prevSlide, and nextSlide to manipulate the remoteSlide object instead of using properties directly on $scope. Wrapping these properties in an object is going to make synchronization with Firebase a snap. Just wait and see…

Keyboard Navigation

orko

But! Before we get to the main event, I am going to add in a little extra bonus for you. Since we need to be able to navigate through our slides, some simple keyboard navigation is in order. There are a bunch of libraries out there to handle this but I opted for a straight-line from-problem-to-solved option in this case.

Since we need to capture keyboard events for the entire page, on the body tag I defined the ng-keyup directive that calls onKeyUp and passes in the key code for the key pressed.

<body ng-controller="MainCtrl" ng-keyup="onKeyUp($event.keyCode)" >
    <!-- OMMITTED -->
</body>

And from here, we can determine if the key pressed was the left or right arrow and then call prevSlide or nextSlide, respectively. I extracted out the key codes into the ‘constants’ LEFT_ARROW and RIGHT_ARROW so that the logic structure would read better.

app.controller('MainCtrl', function ($scope, $location, RemoteSlide) {
    //... OMITTED

<pre><code>var LEFT_ARROW = 37,
    RIGHT_ARROW = 39;

function onKeyUp(keyCode) {
    if (keyCode === LEFT_ARROW) {
        prevSlide();
    } else if (keyCode === RIGHT_ARROW) {
        nextSlide();
    }
}

//... OMITTED
</code></pre>
});

Did we really just add in keyboard navigation in less than 10 lines of code? YEP!

Firebase

firebase

It’s about to get real up in here! I am going to warn you that this may feel weird at how easy it is to actually make this work with Firebase. I think we are looking at about 5 minutes worth of grueling labor to get this up and running.

First we need to include the Firebase and AngularFire resources into our application.

<script src="https://cdn.firebase.com/js/client/1.0.21/firebase.js"></script>
<script src="https://cdn.firebase.com/libs/angularfire/0.8.2/angularfire.min.js"></script>

And then we need to include the firebase submodule into our application. I also like to extract out my Firebase endpoint into an ENDPOINT_URI constant in case I need to use it in more than one place.

var app = angular.module('slideshow', ['ngAnimate', 'firebase']);

app.constant('ENDPOINT_URI', 'https://<YOUR FIREBASE>.firebaseio.com/');

And now for the ‘hard’ part. We are going to create a RemoteSlide service that returns a synchronized object we will bind to in our MainCtrl. We will inject the $firebase service for creating the synchronized object and the ENDPOINT_URI to tell $firebase what to synchronize with. The $firebase service takes a single argument which is a Firebase reference that we create via new Firebase(ENDPOINT_URI). We are also telling Firebase to track this as an object with the $asObject method call.

app.factory('RemoteSlide', function($firebase, ENDPOINT_URI) {
    return function() {
        // create a reference to the current slide index
        var ref = new Firebase(ENDPOINT_URI);

<pre><code>    // return it as a synchronized object
    return $firebase(ref).$asObject();
}
</code></pre>
});

And now, the final step to making our slideshow realtime. We need to call RemoteSlide() to get the synchronized object returned from Firebase, and then call $bindTo to create three-way binding back to the Firebase servers. By default, Firebase does not automatically save changes done to a synchronized object; but in this case, this is exactly the behavior we want and $bindTo accomplishes this nicely. By calling $bindTo($scope, ‘remoteSlide’) we are telling the object returned by $firebase to bind the object at the remote server to the remoteSlide property on $scope and keep them synchronized automatically.

app.controller('MainCtrl', function ($scope, $location, RemoteSlide) {
    //... OMITTED

<pre><code>RemoteSlide().$bindTo($scope, 'remoteSlide');
</code></pre>
});

From the documentation.

Creates a three-way binding between a scope variable and Firebase data. When the scope data is updated, changes are pushed to Firebase, and when changes occur in Firebase, they are pushed instantly into scope. This method returns a promise that resolves after the initial value is pulled from Firebase and set in the scope variable.

And with that single line of code… we have the power!

power

Presenter

And one more loose end that was bothering me. Up to this point, anyone could control the slideshow, which means many masters, which means chaos. And so I needed a quick way to delineate the presenter aka master of the universe from the viewers. I did this by passing in a URL parameter a la index.html#/?presenter=true; and then, reading it off of the $location.$search object, if the client was not the presenter I would abort any key changes.

app.controller('MainCtrl', function ($scope, $location, RemoteSlide) {
    //... OMITTED

<pre><code>function onKeyUp(keyCode) {
    if(!$scope.isPresenter) return; // Only allow presenter to navigate

    if (keyCode === 37) {
        prevSlide();
    } else if (keyCode === 39) {
        nextSlide();
    }
}

$scope.isPresenter = ($location.search()).presenter;

//... OMITTED
</code></pre>
});

Easy? Yep! Could we extend this and make it more robust? Yep! Am I lazy? YEP!

Review

And so just a quick review of everything we covered in this blogpost.

  • We can expose the scope of the element being animated by calling element.scope(), which presents some interesting opportunities for doing dynamic, state-based animations.
  • We can use ng-keyup to create simple keyboard-based navigation functionality.
  • Using $firebase and a Firebase endpoint, we can easily create an object that we can synchronize in realtime.
  • Firebase objects do not automatically save back to the server but we can accomplish this using the $bindTo method and binding our realtime object to a property on $scope
  • We used $location.search() to grab a URL parameter to differentiate between a presenter and a viewer.

Big props to my BFF Kato @katowulf for letting me be an all around nuisance while managing to write amazing code.

Resources

AngularFire Documentation

Remastered Animation in AngularJS 1.2

Getting Started with the JavaScript Version of the GreenSock Animation Platform (GSAP)

Code Demo