@cburgdorf @PascalPrecht

The Best Angular Yet!

By Christoph Burgdorf and Pascal Precht

Christoph

Christoph Burgdorf

@cburgdorf

Pascal

Pascal Precht

@PascalPrecht

thoughtram brain pulse

There's a lot of stuff happening in Angular 2.0

So my 1.x code is not gonna work anymore?

And I should wait until 2.0 is out?

4 Topics to talk about

  • bindToController
  • ngModelOptions
  • One-time Bindings
  • Surprise! The new Router

bindToController

{{defaultName}}
{{defaultName}}
{{defaultName}}
app.controller('AppController', ($scope) => {
  $scope.defaultName = 'Pascal Precht';
});

app.controller('OtherController', ($scope) => {
  $scope.defaultName = 'Christoph Burgdorf';
});

app.controller('YetAnotherController', ($scope) => {
  $scope.defaultName = 'thoughtram';
});
{{defaultName}}
{{defaultName}}
{{defaultName}}
Pascal Precht
Christoph Burgdorf
thoughtram

Accessing outer scope's properties

There are basically two ways to access an outer scopes properties that are primitives.

  • Using the scope's $parent property
  • By creating a new object on the outer scope and defining properties there

$parent property

{{defaultName}}
{{defaultName}}
{{defaultName}}

foo

$parent property

{{defaultName}}
{{$parent.defaultName}}
{{$parent.$parent.defaultName}}

Considered bad practice.

Object property on scope

app.controller('AppController', ($scope) => {
  $scope.defaultName = 'Pascal Precht';
});

app.controller('OtherController', ($scope) => {
  $scope.defaultName = 'Christoph Burgdorf';
});

app.controller('YetAnotherController', ($scope) => {
  $scope.defaultName = 'thoughtram';
});

Object property on scope

app.controller('AppController', ($scope) => {
  $scope.app = { defaultName: 'Pascal Precht' };
});

app.controller('OtherController', ($scope) => {
  $scope.other = { defaultName: 'Christoph Burgdorf' }
});

app.controller('YetAnotherController', ($scope) => {
  $scope.another = { defaultName: 'thoughtram' }
});

Object property on scope

{{defaultName}}
{{$parent.defaultName}}
{{$parent.$parent.defaultName}}

Object property on scope

{{app.defaultName}}
{{app.defaultName}}
{{app.defaultName}}

Object property on scope

{{app.defaultName}}
{{other.defaultName}}
{{another.defaultName}}

Best Practice

Always have a . in your binding expressions.

But both solutions feel more like a workaround.

controllerAs

{{app.defaultName}}
{{other.defaultName}}
{{another.defaultName}}

controllerAs

app.controller('AppController', ($scope) => {
  $scope.app = { defaultName: 'Pascal Precht' };
});

app.controller('OtherController', ($scope) => {
  $scope.other = { defaultName: 'Christoph Burgdorf' }
});

app.controller('YetAnotherController', ($scope) => {
  $scope.another = { defaultName: 'thoughtram' }
});

controllerAs

app.controller('AppController', () => {
  this.defaultName = 'Pascal Precht';
});

app.controller('OtherController', () => {
  this.defaultName = 'Christoph Burgdorf';
});

app.controller('YetAnotherController', () => {
  this.defaultName = 'thoughtram';
});

Works for routing...

app.config(($routerProvider) => {
  $routerProvider
    .when('/', {
      controller: 'BookListController',
      controllerAs: 'bookList',
      templateUrl: 'book-list.tpl.html'
    });
});
  • ...
  • ...and Directives!

    app.directive('myComponent', () => {
      return {
        scope: {},
        templateUrl: 'my-component.tpl.html',
        controller: 'MyComponentController',
        controllerAs: 'myComponent'
      };
    })

    ...and Directives!

    app.controller('MyComponentController', () => {
      this.doSomething = () => {
        ...
      }
    });

    Problem solved!

    So what about bindToController then?

    app.controller('MyComponentController', () => {
      this.myValue = 'My Component';
    });
    {{myComponent.myValue}}

    What if myValue should be configurable from the outside world?

    app.directive('myComponent', () => {
      return {
        scope: {
          myValue: '='
        },
        ...
      };
    })

    Changes in scope properties are not reflected to the controller's this.

    app.controller('MyComponentController', ($scope) => {
      this.myValue = 'My Component';
    
      $scope.$watch('myValue', (newValue) => {
        this.myValue = newValue;
      });
    });

    Super sad :(!

    bindToController tells our directive to bind scope properties to the directive's controller.

    app.directive('myComponent', () => {
      return {
        scope: {
          myValue: '='
        },
        templateUrl: 'my-component.tpl.html',
        controller: 'MyComponentController',
        controllerAs: 'myComponent',
        bindToController: true
      };
    })

    Sweet!

    Even better in 1.4

    Able to be specified as convenient object notation:

    bindToController: {
      text: '@text',
      obj: '=obj',
      expr: '&expr'
    },
    scope: {}
    

    Even better in 1.4

    Also when creating child scopes!

    bindToController: {
      text: '@text',
      obj: '=obj',
      expr: '&expr'
    },
    scope: true
    

    ngModelOptions

    
    

    Hello {{name}}!

    Are you serious?

    ngModelOptions allows us to control how ngModel updates are done.

    <input
      ng-model="name" 
      ng-model-options="{ updateOn: 'blur' }">
    

    Hello {{name}}!

    We can always re-apply default events.

    <input
      ng-model="name" 
      ng-model-options="{ updateOn: 'blur' }">
    

    Hello {{name}}!

    We can always re-apply default events.

    <input
      ng-model="name" 
      ng-model-options="{ updateOn: 'default blur' }">
    

    Hello {{name}}!

    Debouncing model updates

    var debounceSearch = debounceFactory(searchTask, debounceMs);
    
    function searchTask() {
      // do actual search
    };
    
    function performSearch(searchStr) {
      var deferred = $q.defer();
      debounceSearch(deferred, searchStr);
      return deferred.promise;
    };
    
    function debounceFactory(task, ms) {
      var timeout, result;
      return function () {
          var context = this, args = arguments;
          var later = function () {
            timeout = null;
            result = task.apply(context, args);
          };
          clearTimeout(timeout);
          timeout = setTimeout(later, ms);
          return result;
      };
    };
    

    It doesn't have to be like that!

    We can delay model updates with ngModelOptions and debounce.

    <input 
      type="search" 
      ng-model="searchQuery" 
      ng-model-options="{ debounce: 300 }">
    

    Search results for: {{searchQuery}}

    As soon as we use ngModelOptions, model and view can get out of sync.

    Synchronizing with $rollbackViewValue

    app.controller('AppController', function ($scope) {
      $scope.name = 'thoughtram';
    
      $scope.cancel = function (e) {
        if (e.keyCode == 27) {
          $scope.userForm.userName.$rollbackViewValue();
        }
      };
    });
    

    One-time Bindings

    Databinding in Angular

    app.controller('MyController', () => {
      this.greeting = 'Hello ng-nl crowd!';
    });
    {{ctrl.greeting}}

    How does that even work?

    Runtime

    Runtime

    Runtime

    Runtime

    Runtime

    Runtime

    Runtime

    Runtime

    Runtime

    Best Practice

    Be careful with the number of bindings

    What if I told you...

    ...not everything has to be bound forever

    We can deregister watchers for data that won't change

    bindonce prior to 1.3

    <p bo-class="{'fancy':Person.isNice}" bo-html="Person.story"> </p>

    Lot's of new directives

    bindonce root directive

    Super sad :(

    One-time bindings

    {{::Person.lastname}} <img ng-src="::Person.picture" alt="{{::Person.title}}"> <p ng-class="::{'fancy':Person.isNice}" ng-bind-html="::Person.story"> </p>

    Super happy :)

    Also with ng-repeat

    • {{item.name}}

    Live Example

    How it works

    • Expression starts with ::
    • If expression evaluates not to undefined schedule deregistration
    • Only deregister, if it still doesn't evaluate to undefined at deregistration time

    You were talking about a surprise right?

    Futuristic Routing

    There's going to be a new Router in 1.4!

    var app = angular.module('app', ['ngRoute']);
    
    app.config(($routeProvider) => {
      $routerProvider
        .when('/', {
          redirectTo: '/welcome'
        })
        .when('/welcome', {
          templateUrl: 'welcome.html',
          controller: 'WelcomeController'
        });
    });
    
    <div ng-app="app">
      <ng-view></ng-view>
    </div>
    

    New Router works with components

    • Component's template can have "view ports"
    • Component's controller can have a router
    • Component's router tells component what goes inside view ports
    <div ng-app="app" ng-controller="AppController">
      <ng-viewport></ng-viewport>
    </div>
    
    var app = angular.module('app', ['ngNewRouter']);
    
    app.controller('AppController', AppController);
    
    function AppController($router) {
      $router.config([
        { path: '/', component: 'welcome' }
      ]);
    }
    

    Welcome Component

    Welcome!

    Hello {{welcome.name}}!

    function WelcomeController() {
      this.name = 'Pascal';
    }

    components/welcome/welcome.html is loaded asynchronously.

    Configure $componentLoader

    app.config($componentLoaderProvider) => {
      $componentLoaderProvider
        .setCtrlNameMapping((name) => {
          // return controller name
        })
        .setTemplateMapping((name) => {
          // return template url
        });
    });
    

    We can have mutiple view ports too!

    function AppController($router) {
      $router.config([
        { 
          path: '/',
          components: {
            left: 'left',
            right: 'right'
          }
        }
      ]);
    }
    
    <div ng-app="app">
      <div ng-viewport="left"></div>
      <div ng-viewport="right"></div>
    </div>
    

    Component Lifecycle Hooks

    • canActivate - Runs before switching to next component
    • activate - Fires just before navigation finishes
    • canDeactivate - Fires before any new component is instantiated
    • deactivate - Fires for each component that is removed as part of navigation

    Example

    function MyController(user, $http) {
      this.user = user;
      this.$http = $http;
    }
    
    MyController.prototype.canActivate = function() {
      return this.user.isAdmin;
    };
    

    Still not convinced that this is the best Angular yet?

    Thanks!

    Christoph Burgdorf and Pascal Precht

    More at blog.thoughtram.io/exploring-angular-1.3