Mobile Angular 1.2 Beta is here with many improvements and fixes, but also with many breaking changes.

Using the migrate module

If you want to move your application to 1.2 (you should), you have the opiton to mitigate the transition using the mobile-angular-ui.migrate module.

It will translate old syntax to new and reintroduce dropped features.

Be aware that this is intended to help transition and not to stay there in production. In particular it will introduce some performance overhead and will be discontinued soon.

So: use it only in the meanwhile you readapt your application to newer syntax.

To use migrate module include it in your head section:

<!-- Only required to import legacy syntax and directives. You won't need it 
unless you are attempting to move an app from Mobile Angular UI 1.1 to 1.2 -->
<link rel="stylesheet" href="mobile-angular-ui/dist/css/mobile-angular-ui-migrate.min.css" />
<script src="/dist/js/mobile-angular-ui.migrate.min.js"></script>

And then declare it as a dependency for your app:

var app = angular.module('MobileAngularUiExamples', [
  'ngRoute',
  'mobile-angular-ui',
  'mobile-angular-ui.migrate'
);

Moving to 1.2

In order to properly move your old 1.1 app to 1.2 you may wish to know what's changed. All of the functionalities are now separed in 3 modules: core, components and gestures.

Core functionalities

Core UI Functionalities are those any HTML UI built on Angular could use. They are not Mobile-specific nor depending on anything else that Angular itself, and you could use them with any css framework.

YieldTo/ContentFor

They are the same that 1.1 beside to be prefixed with ui- so use: ui-yield-to and ui-content-for.

Toggle/Toggleable to SharedState and ui-*

Toggle has been rewritten from scratch for 1.2. In place of that there is a new service with it's own isolated context.

It is called SharedState. It will act as a BUS between UI elements to share UI related state that is automatically disposed when all scopes requiring it are destroyed.

eg.

SharedState.initialize(requiringScope, 'myId');
SharedState.toggle('myId');

Using SharedState directives is very close to attach an ng-init in root element and interact through expression and ng-click, but it has handy features to avoid polluting controllers/scopes with ui-related stuffs.

SharedState methods are exposed through ui- directives. So you should never use it directly in your controller, and you can spare them to hold application logic.

Example: Tabs


<div class="tabs" ui-state="activeTab">

  <ul class="nav nav-tabs">
    <li ui-class="{'active': activeTab == 1)}">
      <a ui-set="{'activeTab': 1}">Tab 1</a>
    </li>
    <li ui-class="{'active': activeTab == 2)}">
      <a ui-set="{'activeTab': 2}">Tab 2</a>
    </li>
    <li ui-class="{'active': activeTab == 3)}">
      <a ui-set="{'activeTab': 3}">Tab 3</a>
    </li>
  </ul>

  <div ui-if="activeTab == 1">
    Tab 1
  </div>

  <div ui-if="activeTab == 2">
    Tab 2
  </div>

  <div ui-if="activeTab == 3">
    Tab 3
  </div>

</div>

NOTE: ui-toggle/set/turnOn/turnOff responds to click/tap without stopping propagation so you can use them along with ng-click too. You can also change events to respond to with ui-triggers attribute.

Any SharedState method is exposed through Ui object in $rootScope. So you could always do ng-click="Ui.turnOn('myVar')".

Since SharedState is a service you can initialize/set statuses through controllers too:

app.controller('myController', function($scope, SharedState){
  SharedState.initialize($scope, "activeTab", 3);
});

As well as you can use ui-default for that:

<div class="tabs" ui-state="activeTab" ui-default="thisIsAnExpression(5 + 1 - 2)"></div>

Outer Clicks

ui-outer-click and ui-outer-click-if are directives that allow to specifiy a behaviour when click/tap events happen outside an element. This can be easily used to implement eg. close on outer click feature for a dropdown.

  <div class="btn-group pull-right">
    <a ui-turn-on='myDropdown' class='btn'>
      <i class="fa fa-ellipsis-v"></i>
    </a>
    <ul 
      class="dropdown-menu"

      ui-state="myDropdown"
      ui-if="myDropdown"

      ui-outer-click="ui.turnOff('myDropdown')"
      ui-outer-click-if="ui.active('myDropdown')"

      ui-turn-off="myDropdown">

      <li><a>Action</a></li>
      <li><a>Another action</a></li>
      <li><a>Something else here</a></li>
      <li class="divider"></li>
      <li><a>Separated link</a></li>
    </ul>
  </div>

UI Components

General Considerations

Since 1.2 I've tried to retain bootstrap existing components look and feel as much as possible for two reason:

  1. People would expect to already know them and how customize their appearence through css.
  2. Mobile Angular UI dependance on bootstrap is more loose so it does not need to be updated each time BS3 changes.

As a consequence i've reduced special styles for panels/forms/modals to the bare minimum.

Pretty much the same. They plays well with ng-if now.

Scrollables

.scrollable component is backward compatible but also has some major improvements too.

  1. .scrollable-header/.scrollable-footer can be used to add fixed header/footer to a scrollable area without having to deal with css height and positioning to avoid breaking scroll.
  2. .scrollable-content controller now expose a scrollTo function.
  3. It plays nice with android keyboard and forms.
  4. You can use ui-scroll-bottom/ui-scroll-top directives handle that events and implement features like infinite scroll.
<div class="scrollable">
  <div class="scrollable-header"><!-- ... --></div>
  <div class="scrollable-content" ui-scroll-bottom="loadMore()">
    <ul>
      <li ng-repeat="item in items">
        <!-- ... -->
      </li>
    </ul>
  </div>
  <div class="scrollable-footer"><!-- ... --></div>
</div>

Modals and Overlay

Modal dialogs has been reintroduced from Bootstrap, overlays are now just modals styled different.


<div content-for="modals">

  <!-- A plain modal dialog -->
  <div class="modal" ui-if='modal1' ui-state='modal1'>
    <div class="modal-dialog">
      <div class="modal-content">
        <div class="modal-header">
          <button class="close" 
                  ui-turn-off="modal1">&times;</button>
          <h4 class="modal-title">Modal title</h4>
        </div>
        <div class="modal-body">
          <!-- ... -->
        </div>
        <div class="modal-footer">
          <button ui-turn-off="modal1" class="btn btn-default">Close</button>
          <button ui-turn-off="modal1" ng-click="doSomething()" class="btn btn-primary">Save changes</button>
        </div>
      </div>
    </div>
  </div>

  <!-- A blurred overlay dialog -->
    <div class="modal modal-overlay" ui-if='modal2' ui-state='modal2'>
      <div class="modal-dialog">
        <div class="modal-content">
          <div class="modal-header">
            <button class="close"
                    ui-turn-off="modal2">&times;</button>
            <h4 class="modal-title">Modal title</h4>
          </div>
          <div class="modal-body">
            <!-- ... -->
          </div>
          <div class="modal-footer">
            <button ui-turn-off="modal2" class="btn btn-default">Close</button>
            <button ui-turn-off="modal2" ng-click="doSomething()" class="btn btn-primary">Save changes</button>
          </div>
        </div>
      </div>
    </div>

</div>

Basically the same, but now they rely on SharedState and bindOuterClick to activate/inactivate them.

Syntax is basically the same:

<div class="sidebar sidebar-left">
  <!-- ... -->
</div>

Also you can now track sidebars status in url specifing uiTrackAsSearchParam attribute. This way you can manage to close a sidebar on back button too. It is optional cause it has a big caveat: you should not use query string part to update your view automatically. It is safe for many apps but not for all, so its optional.

To use this feature:

  1. enable it in sidebar setting uiTrackAsSearchParam to true:
<div class="sidebar sidebar-left" ui-track-as-search-param="true">
  <!-- ... -->
</div>
  1. Specify reloadOnSearch: false on your routes:
app.config(function($routeProvider) {
  $routeProvider.when('/',          {templateUrl: 'home.html', reloadOnSearch: false});
  $routeProvider.when('/scroll',    {templateUrl: 'scroll.html', reloadOnSearch: false}); 
  $routeProvider.when('/toggle',    {templateUrl: 'toggle.html', reloadOnSearch: false}); 
  $routeProvider.when('/tabs',      {templateUrl: 'tabs.html', reloadOnSearch: false}); 
  $routeProvider.when('/accordion', {templateUrl: 'accordion.html', reloadOnSearch: false}); 
  $routeProvider.when('/overlay',   {templateUrl: 'overlay.html', reloadOnSearch: false}); 
  $routeProvider.when('/forms',     {templateUrl: 'forms.html', reloadOnSearch: false});
  $routeProvider.when('/dropdown',  {templateUrl: 'dropdown.html', reloadOnSearch: false});
  $routeProvider.when('/drag',      {templateUrl: 'drag.html', reloadOnSearch: false});
  $routeProvider.when('/carousel',  {templateUrl: 'carousel.html', reloadOnSearch: false});
});

Carousel implementations (with $swipe or $drag) has been moved in an external file/plugin and not included by default.

Gestures

$swipe

A drop in replacement for ngTouch's $swipe. It is free from other ngTouch features that does duplicate/break fastclicks behaviour.

$drag

$drag Service wraps $swipe to extend its behavior moving target element through css transform according to the $swipe coords thus creating a drag effect.

$drag interface is very close to $swipe:

app.controller('MyController', function($drag, $element){
  var unbindDrag = $drag.bind($element, {
   // drag callbacks
   // - rect is the current result of getBoundingClientRect() for bound element
   // - cancelFn issue a "touchcancel" on element
   // - resetFn restore the initial transform
   // - undoFn undoes the current movement
   // - swipeCoords are the coordinates exposed by the underlying $swipe service
   start: function(rect, cancelFn, resetFn, swipeCoords){},
   move: function(rect, cancelFn, resetFn, swipeCoords){},
   end: function(rect, undoFn, resetFn, swipeCoords) {};
   cancel: function(rect, resetFn){},

   // constraints for the movement
   // you can use a "static" object of the form:
   // {top: .., lelf: .., bottom: .., rigth: ..}
   // or pass a function that is called on each movement 
   // and return it in a dynamic way.
   // This is useful if you have to constraint drag movement while bounduaries are
   // changing over time.

   constraint: function(){ return {top: y1, left: x1, bottom: y2, right: x2}; }, // or just {top: y1, left: x1, bottom: y2, right: x2}

   // instantiates the Trasform according to touch movement (defaults to `t.translate(dx, dy);`)
   // dx, dy are the distances of movement for x and y axis after constraints are applyied
   transform: function(transform, dx, dy, currSwipeX, currSwipeY, startSwipeX, startSwipeY) {},

   // changes the Transform before is applied to element (useful to add something like easing or accelleration)
   adaptTransform: function(transform, dx, dy, currSwipeX, currSwipeY, startSwipeX, startSwipeY) {}

  });

  // This is automatically called when element is disposed so it is not necessary
  // that you call this manually but if you have to detatch $drag service before
  // this you could just call:
  unbindDrag();
});

Main differences with $swipe are:

Example 1. Drag to dismiss

app.directive('dragToDismiss', function($drag, $parse, $timeout){
  return {
    restrict: 'A',
    compile: function(elem, attrs) {
      var dismissFn = $parse(attrs.dragToDismiss);
      return function(scope, elem, attrs){
        var dismiss = false;

        $drag.bind(elem, {
          constraint: {
            minX: 0, 
            minY: 0, 
            maxY: 0 
          },
          move: function(c) {
            if( c.left >= c.width / 4) {
              dismiss = true;
              elem.addClass('dismiss');
            } else {
              dismiss = false;
              elem.removeClass('dismiss');
            }
          },
          cancel: function(){
            elem.removeClass('dismiss');
          },
          end: function(c, undo, reset) {
            if (dismiss) {
              elem.addClass('dismitted');
              $timeout(function() { 
                scope.$apply(function() {
                  dismissFn(scope);  
                });
              }, 400);
            } else {
              reset();
            }
          }
        });
      };
    }
  };
});

Example 2. Touch enabled "deck of cards" carousel directive

app.directive('carousel', function(){
  return {
    restrict: 'C',
    scope: {},
    controller: function($scope) {
      this.itemCount = 0;
      this.activeItem = null;

      this.addItem = function(){
        var newId = this.itemCount++;
        this.activeItem = this.itemCount == 1 ? newId : this.activeItem;
        return newId;
      };

      this.next = function(){
        this.activeItem = this.activeItem || 0;
        this.activeItem = this.activeItem == this.itemCount - 1 ? 0 : this.activeItem + 1;
      };

      this.prev = function(){
        this.activeItem = this.activeItem || 0;
        this.activeItem = this.activeItem === 0 ? this.itemCount - 1 : this.activeItem - 1;
      };
    }
  };
});

app.directive('carouselItem', function($drag) {
  return {
    restrict: 'C',
    require: '^carousel',
    scope: {},
    transclude: true,
    template: '<div class="item"><div ng-transclude></div></div>',
    link: function(scope, elem, attrs, carousel) {
      scope.carousel = carousel;
      var id = carousel.addItem();

      var zIndex = function(){
        var res = 0;
        if (id == carousel.activeItem){
          res = 2000;
        } else if (carousel.activeItem < id) {
          res = 2000 - (id - carousel.activeItem);
        } else {
          res = 2000 - (carousel.itemCount - 1 - carousel.activeItem + id);
        }
        return res;
      };

      scope.$watch(function(){
        return carousel.activeItem;
      }, function(n, o){
        elem[0].style['z-index']=zIndex();
      });


      $drag.bind(elem, {
        constraint: { minY: 0, maxY: 0 },
        adaptTransform: function(t, dx, dy, x, y, x0, y0) {
          var maxAngle = 15;
          var velocity = 0.02;
          var r = t.getRotation();
          var newRot = r + Math.round(dx * velocity);
          newRot = Math.min(newRot, maxAngle);
          newRot = Math.max(newRot, -maxAngle);
          t.rotate(-r);
          t.rotate(newRot);
        },
        move: function(c){
          if(c.left >= c.width / 4 || c.left <= -(c.width / 4)) {
            elem.addClass('dismiss');  
          } else {
            elem.removeClass('dismiss');  
          }          
        },
        cancel: function(){
          elem.removeClass('dismiss');
        },
        end: function(c, undo, reset) {
          elem.removeClass('dismiss');
          if(c.left >= c.width / 4) {
            scope.$apply(function() {
              carousel.next();
            });
          } else if (c.left <= -(c.width / 4)) {
            scope.$apply(function() {
              carousel.next();
            });
          }
          reset();
        }
      });
    }
  };
});

Transform

Transform is the underlying service used by $drag to deal with css trasform matrix in a simpler and vendor-angnostic way.

<div id="myElem" style="transform: translateX: 20px;"></div>
var e = document.getElementById('myElem');
var t0 = Transform.fromElement(e);

console.log(t0.getTranslation().x);
// -> 20;

t0.rotate(90);

// Set t0 to element ignoring previous transform.
t0.set(e);

var t1 = new Transform();

t1.translate(12, 40);

// merges t1 with current trasformation matrix of element.
t1.apply(e);

Utils

panels and forms has been discontinued completely. This is due to: panels has a trivial markup as they are in bootstrap. Forms on the contrary has a lot of functionalities so in my opinion .form-* directives need an external plugin to be implemented and mantained correctly.


comments powered by Disqus