Você está na página 1de 9

Building a custom Swipeable Card

UI with Ionic and AngularJS


By @maxlynch on January 25, 2014

One of the original goals of the Ionic Framework was to encourage custom mobile UI development with HTML5,
building UIs that were innovative, interactive, and fun, but weren't necessarily part of the core mobile UI
framework on a given platform.

We've seen native developers doing this for some time. Startups and companies like Jelly, Path, Facebook, and
Google have very custom mobile UIs filled with animations, lots of interesting gesture-based interactions, and
other unique effects. This helps create a potentially more fluid experience for the user performing an app-specific
task, but also serves as a way to make a design impact and draw more attention to the app itself.

I was playing with the Jelly app last week and fell in love with the swipeable card UI. I felt it was fun and
effective enough that tons of mobile developers would want to emulate it for their apps, much like the draggable
Side Menu made popular by Path and Facebook:

But to build this UI, it would have to be done from scratch, seeing as it's not a built in component in UI
frameworks available in any mobile SDKs. So, I spent a nice Saturday building a reusable Ionic and AngularJS
based card layout (demo and code here) and I want to show you how I did it and how you can build your own
Ionic components, too!

Views and View Controllers


Those familiar with native development will understand the View Controller design pattern. The idea is we have
a bunch of rectangles on the screen, these are what we call Views and they might be as simple as a button or
image, or as complicated as a slider. We then create complex, interesting UIs by using logic-only View
Controllers that act as the conductors by moving Views around, handling gestures and other events on the child
Views, and creating and destroying child views.

A perfect example of a View Controller is a Tab Bar Controller with three tabs and three pages. This controller
has to manage the three sub pages (which can be View Controllers on their own), as well as the Tab Bar which
manages its child Tab Items.

Mobile SDKs often come with several built-in Views and View Controllers. For example, iOS has the
UITabBarController, UINavigationController, UITableViewController, and a few others, and uses Tab Bars,
Buttons, and List items extensively. Most common mobile UIs can be built with just these core controllers and
views.

But when we want to create any non-standard UI, we need to build our own View Controllers and possibly Views
to make this work. And we shouldn't be afraid to, it's exactly how the pros do it!

Ionic Swipe Cards

With the background about View Controllers out of the way, let's look at the final UI we are going to build (play
the video demo):

Card stack
To start, we need to look at this UI as a stack of cards that the user can swipe between. This means we need some
sort of View Controller that manages the card stack, popping ones off the stack that are swiped, and letting the
user push new ones onto the stack. Let's call this theSwipeableCardController:

var SwipeableCardController = ionic.controllers.ViewController.inherit({


initialize: function(opts) {
this.cards = [];
// Initialize from the passed in options
},

pushCard: function(card) {
this.cards.push(card);
// Show the card
},
popCard: function() {
var card = this.cards.pop();
// Animate the card out
}
})
That's pretty much the core of this View Controller. Apart from managing the stack, the controller will also
initialize new cards and tell the cards to animate in our out, but that's about it!

The Card View, however, is more complicated. Since the cards are rendered to the screen (as opposed to
controllers which are pure-logic), they need to keep track of their DOM element and also attach event listeners to
that element.

In the example below I've included a small snippet of what happens in the drag event, but the full example is in
the repo:

var SwipeableCardView = ionic.views.View.inherit({


initialize: function(opts) {
// Store the card element
this.el = opts.el;
this.bindEvents();
},
bindEvents: function() {
var self = this;

ionic.onGesture('dragstart', function(e) {
// Process start of drag

}, this.el);

ionic.onGesture('drag', function(e) {
// Process drag
window.rAF(function() {
self._doDrag(e);
});
}, this.el);

ionic.onGesture('dragend', function(e) {
// Process end of drag
}, this.el);
},
_doDrag: function(e) {
// Calculate how far we've dragged, with a slow-down effect
var o = e.gesture.deltaY / 3;

// Get the angle of rotation based on the


// drag distance and our distance from the
// center of the card (computed in dragstart),
// and the side of the card we are dragging from
this.rotationAngle = Math.atan(o/this.touchDistance) * this.rotationDirection;

// Don't rotate if dragging up


if(e.gesture.deltaY < 0) {
this.rotationAngle = 0;
}

// Update the y position of this view


this.y = this.startY + (e.gesture.deltaY * 0.4);

// Apply the CSS transformation to the card,


// translating it up or down, and rotating

// it based on the computed angle


this.el.style[ionic.CSS.TRANSFORM] = 'translate3d(' + this.x + 'px, ' + this.y + 'px, 0) rotate(' +
(this.rotationAngle || 0) + 'rad)';
}
});
This is where Ionic's great gesture system really shines. We can listen for complex gesture events such as
dragging, dragging in a specific direction, swiping, pinch-to-zooming, and other cool gestures (powered by the
great Hammer.js)

The gesture system will also tell us how far we've dragged (e.gesture.deltaY in the example above), and can also
compute the velocity or rotation of the drag (useful for gestures that "throw" a view around).

AngularJS to the Rescue!

If we just had the above View and View Controller, we'd have to manage it all in Javascript. We'd create a few
DOM nodes to display each card, instantiate a few SwipeableCardView's for those nodes, put them in a
new SwipeableCardController and let the magic happen.

But that's annoying to have to code that by hand, and we miss out on using AngularJS to integrate this card stack
with our scope data. For that we need to build some custom directives. But first, let's assume we want the
directive markup to look like this:

<swipe-cards>
<swipe-card ng-repeat="card in cards" on-destroy="cardDestroyed($index)" on-swipe="cardSwiped($index)">
<!-- Card content here -->
</swipe-card>
</swipe-cards>
We use a custom element directive to specify that this entire directive will be powered by
our SwipeableCardController, with each child<swipe-card> being an instance of our SwipeableCardView.

Let's start with the <swipe-cards> directive as it's simpler:

// Our module, requiring the 'ionic' module


angular.module('ionic.contrib.ui.cards', ['ionic'])

.directive('swipeCards', ['$rootScope', function($rootScope) {


return {
restrict: 'E',
template: '<div class="swipe-cards" ng-transclude></div>',
replace: true,
transclude: true,
scope: {},
controller: function($scope, $element) {
// Instantiate the controller
var swipeController = new SwipeableCardController({
});

// We add a root scope event listener to facilitate interacting with the


// directive incase of no direct scope access
$rootScope.$on('swipeCard.pop', function(isAnimated) {
swipeController.popCard(isAnimated);
});

// return the object so it is accessible to child


// directives that 'require' this directive as a parent.
return swipeController;
}
}
}])
This means whenever AngularJS hits the <swipe-cards> directive, it goes and instantiates our controller and
makes it accessible to directives that inherit from it, as we see below in the <swipe-card>directive:

.directive('swipeCard', ['$timeout', function($timeout) {


return {
restrict: 'E',
template: '<div class="swipe-card" ng-transclude></div>',

// Requiring the swipeCards directive makes the controller available


// in the linking function
require: '^swipeCards',
replace: true,
transclude: true,
scope: {
onSwipe: '&',
onDestroy: '&'
},
compile: function(element, attr) {
return function($scope, $element, $attr, swipeCards) {
var el = $element[0];

// Instantiate our card view


var swipeableCard = new SwipeableCardView({
el: el,
onSwipe: function() {
$timeout(function() {
$scope.onSwipe();
});
},
onDestroy: function() {
$timeout(function() {
$scope.onDestroy();
});
},
});

// Make the card available to the parent scope, not necessary


// but makes it easier to interact with (similar to iOS exposing
// parent controllers and views dynamically to children)
$scope.$parent.swipeCard = swipeableCard;

// We can push a new card onto the controller card stack, animating it in
swipeCards.pushCard(swipeableCard);
}
}
}
}])
I love the simplicity of wrapping a complicated UI like the Swipeable Cards into a set of tiny AngularJS
directives that play nicely with the rest of your Angular app. In fact, this is how most of the controllers and views
are exposed in the Ionic code base.

One question we receive a lot is why we decided to use element directives instead of attribute or class name
directives. Perhaps the biggest reason is because we believe element directives should be used when
specifying components, and attributes or classes used to specifybehavior. As browsers evolve and features such
as Web Componentsare widely available, this will become the prodominant way of building reusable components
for the web.

If you are new to writing custom AngularJS directives, hopefully it's illuminating to see a real one in the wild!

Conclusion

The point of this article is to show you the process behind creating new, innovative mobile UIs from the ground
up, using Ionic utilities where possible. But one takeaway should be that no framework will have every UI built
in for you, and you should be open to creating new components in the spirit of the framework you choose to
develop with.

Você também pode gostar