Upload
lukas-ruebbelke
View
1.559
Download
0
Embed Size (px)
Citation preview
Embrace the Angular 2 Ethosin Angular 1.x
Ethosthe distinctive character, spirit, and attitudes of a people, culture, era, etc
GAP!
GAP!
Angular 2 is not only a framework…
…but a set of very useful patterns.
essence details
A Brief History of Angular
tiny app == tiny view + tiny controller
Growing Application
Growing View
Growing Controller
Realistic Application
Growing View
Growing Controller
Uh oh!
Large 1.x Application
Named Route
Named Route
Named Route
Large 1.x Application
Directive
Directive
Directive
Any Angular 2 Application
Component
Component
Component
Still one small problem…
Structure
Communication
Let's step back a moment...
The Big Picture
viewcontroller
module
config
routes
$scope
service directive
It has gotten even simpler...
The Simplified Picture
component
module
config
routes
service
Angular 1.x Application
Component
Component
Component
The best way to become a great Angular developer
is to focus on becoming a great developer
Common sense
Established practices
JOHN PAPA'S STYLE GUIDE
Hello Progression
angular.module('app') .controller('CategoriesListCtrl', function($scope, CategoriesModel) { CategoriesModel.getCategories() .then(function(result){ $scope.categories = result; }); $scope.onCategorySelected = function(category) { CategoriesModel.setCurrentCategory(category); } });
Classic Controller
<div ng-controller="CategoriesListCtrl"> <!-- categories list markup --></div>
Classic View
angular.module('app') .controller('CategoriesListCtrl', function(CategoriesModel) { CategoriesModel.getCategories() .then(function(result){ this.categories = result; }); this.onCategorySelected = function(category) { CategoriesModel.setCurrentCategory(category); } });
Moving to controller as
<div ng-controller="CategoriesListCtrl as categoriesListCtrl"> <!-- categories list markup --></div>
Moving to controller as
function CategoriesListCtrl(CategoriesModel) { CategoriesModel.getCategories() .then(function(result){ this.categories = result; }); this.onCategorySelected = function(category) { CategoriesModel.setCurrentCategory(category); }}angular.module('app') .controller('CategoriesListCtrl', CategoriesListCtrl);
Extract the controller function
function CategoriesListCtrl(CategoriesModel) { CategoriesModel.getCategories() .then(function(result){ this.categories = result; }); this.onCategorySelected = function(category) { CategoriesModel.setCurrentCategory(category); }}var CategoriesList = { template: '<div><!-- categories list markup --></div>', controller: CategoriesListCtrl, controllerAs: 'CategoriesListCtrl' }angular.module('app') .component('categoriesList', CategoriesList);
Convert to component
<categories-list></categories-list>
Convert to component
class CategoriesListCtrl { constructor(CategoriesModel) { 'ngInject'; this.CategoriesModel = CategoriesModel; this.CategoriesModel.getCategories() .then(result => this.categories = result); } onCategorySelected(category) { this.CategoriesModel.setCurrentCategory(category); }}const CategoriesList = { template: '<div><!-- categories list markup --></div>', controller: CategoriesListCtrl, controllerAs: 'categoriesListCtrl'};angular.module('app') .component('categoriesList', CategoriesList);
Convert to ES6
class CategoriesListCtrl { constructor(CategoriesModel) { 'ngInject'; this.CategoriesModel = CategoriesModel; } $onInit() { this.CategoriesModel.getCategories() .then(result => this.categories = result); } onCategorySelected(category) { this.CategoriesModel.setCurrentCategory(category); }}const CategoriesList = { template: '<div><!-- categories list markup --></div>', controller: CategoriesListCtrl, controllerAs: 'categoriesListCtrl'};angular.module('app') .component('categoriesList', CategoriesList);
Use lifecycle hooks
@Component({ selector: 'categories-list', template: `<div>Hello Category List Component</div>`, providers: [CategoriesModel]})export class CategoriesList { constructor(CategoriesModel: CategoriesModel) { this.CategoriesModel = CategoriesModel; } ngOnInit() { this.CategoriesModel.getCategories() .then(result => this.categories = result); } onCategorySelected(category) { this.CategoriesModel.setCurrentCategory(category); }}
Angular 2 equivalent
@Component({ selector: 'categories-list', template: `<div>Hello Category List Component</div>`, providers: [CategoriesModel]})export class CategoriesList { constructor(CategoriesModel: CategoriesModel) { this.CategoriesModel = CategoriesModel; } ngOnInit() { this.CategoriesModel.getCategories() .then(result => this.categories = result); } onCategorySelected(category) { this.CategoriesModel.setCurrentCategory(category); }}
class CategoriesListCtrl { constructor(CategoriesModel) { 'ngInject'; this.CategoriesModel = CategoriesModel; } $onInit() { this.CategoriesModel.getCategories() .then(result => this.categories = result); } onCategorySelected(category) { this.CategoriesModel.setCurrentCategory(category); }}const CategoriesList = { template: '<div><!-- categories list markup --></div>', controller: CategoriesListCtrl, controllerAs: 'categoriesListCtrl' };angular.module('app') .component('categoriesList', CategoriesList);
Similar shapes
@Component({ selector: 'categories-list', template: `<div>Hello Category List Component</div>`, providers: [CategoriesModel]})
export class CategoriesList { constructor(CategoriesModel: CategoriesModel) { this.CategoriesModel = CategoriesModel; } ngOnInit() { this.CategoriesModel.getCategories() .then(result => this.categories = result); } onCategorySelected(category) { this.CategoriesModel.setCurrentCategory(category); }}
class CategoriesListCtrl { constructor(CategoriesModel) { 'ngInject'; this.CategoriesModel = CategoriesModel; } $onInit() { this.CategoriesModel.getCategories() .then(result => this.categories = result); } onCategorySelected(category) { this.CategoriesModel.setCurrentCategory(category); }}const CategoriesList = { template: '<div><!-- categories list markup --></div>', controller: CategoriesListCtrl, controllerAs: 'categoriesListCtrl' };angular.module('app') .component('categoriesList', CategoriesList);
Component configuration
@Component({ selector: 'categories-list', template: `<div>Hello Category List Component</div>`, providers: [CategoriesModel]})
export class CategoriesList { constructor(CategoriesModel: CategoriesModel) { this.CategoriesModel = CategoriesModel; } ngOnInit() { this.CategoriesModel.getCategories() .then(result => this.categories = result); } onCategorySelected(category) { this.CategoriesModel.setCurrentCategory(category); }}
class CategoriesListCtrl { constructor(CategoriesModel) { 'ngInject'; this.CategoriesModel = CategoriesModel; } $onInit() { this.CategoriesModel.getCategories() .then(result => this.categories = result); } onCategorySelected(category) { this.CategoriesModel.setCurrentCategory(category); }}const CategoriesList = { template: '<div><!-- categories list markup --></div>', controller: CategoriesListCtrl, controllerAs: 'categoriesListCtrl' };angular.module('app') .component('categoriesList', CategoriesList);
Component controller
@Component({ selector: 'categories-list', template: `<div>Hello Category List Component</div>`, providers: [CategoriesModel]})export class CategoriesList { constructor(CategoriesModel: CategoriesModel) { this.CategoriesModel = CategoriesModel; } ngOnInit() { this.CategoriesModel.getCategories() .then(result => this.categories = result); } onCategorySelected(category) { this.CategoriesModel.setCurrentCategory(category); }}
class CategoriesListCtrl { constructor(CategoriesModel) { 'ngInject'; this.CategoriesModel = CategoriesModel; } $onInit() { this.CategoriesModel.getCategories() .then(result => this.categories = result); } onCategorySelected(category) { this.CategoriesModel.setCurrentCategory(category); }}const CategoriesList = { template: '<div><!-- categories list markup --></div>', controller: CategoriesListCtrl, controllerAs: 'categoriesListCtrl' };angular.module('app') .component('categoriesList', CategoriesList);
Entry point into the application
Use ES6 for classes and modules
import {Injectable} from '@angular/core';@Injectable()export class MessageService { private message = 'Hello Message'; getMessage(): string { return this.message; }; setMessage(newMessage: string): void { this.message = newMessage; };}
Simple Angular 2 service
class MessageService { constructor() { this.message = 'Hello Message' } getMessage() { return this.message; }; setMessage(newMessage) { this.message = newMessage; };}export default MessageService;
Simple Angular 1.x service
import angular from 'angular';import BookmarksModule from './bookmarks/bookmarks';import CategoriesModule from './categories/categories';const ComponentsModule = angular.module('app.components', [ BookmarksModule.name, CategoriesModule.name]);export default ComponentsModule;
Importing in Angular 1.x
Create a top-level component
import { Component } from '@angular/core';@Component({ selector: 'app', templateUrl: './app.component.html', styleUrls: ['./app.component.css']})export class AppComponent {}
Top-level component
import { BrowserModule } from '@angular/platform-browser';import { NgModule } from '@angular/core';import { AppComponent } from './app.component';@NgModule({ declarations: [ AppComponent ], imports: [ BrowserModule ], bootstrap: [ AppComponent ]})export class AppModule {}
Module
import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';import { enableProdMode } from '@angular/core';import { environment } from './environments/environment';import { AppModule } from './app/';if (environment.production) { enableProdMode();}platformBrowserDynamic().bootstrapModule(AppModule);
Bootstrap
<body> <app>Loading…</app></body>
Entry point
import template from './app.html';import './app.styl';const AppComponent = { template};export default AppComponent;
Top-level component
import angular from 'angular';import appComponent from './app.component';import CommonModule from './common/common';import ComponentsModule from './components/components';angular.module('app', [ CommonModule.name, ComponentsModule.name]) .component('app', appComponent);
Module
<body ng-app="app" ng-strict-di ng-cloak> <app>Loading...</app></body>
Entry point
Organize your application with subcomponents
Directives vs Components
Components are simplified Directives
Components have a MUCH simpler configuration object
Components will generally accomplish everything you need
You still need directives when you need to do DOM manipulation or create a class based directive
Advanced functionality such as terminal, priority, etc require a directive
Basic Component Structure
The basic structure for a component is module.component('name',{});
Notice that unlike a directive, component takes a configuration object and not a function
The most common component configuration properties will be controller, template, templateUrl and bindings
Component Driven Architecture
Components only control their own View and Data
Components have a well-defined public API aka Inputs and Outputs
Components have a well-defined lifecycle
A well architected application is a tree of components
import {Component} from '@angular/core';@Component({ moduleId: module.id, selector: 'items', templateUrl: 'items.component.html', styleUrls: ['items.component.css']})export class ItemsComponent {}
Parent component
import {Component, Input} from '@angular/core';import {Item} from '../shared';@Component({ moduleId: module.id, selector: 'app-item-details', templateUrl: 'item-details.component.html', styleUrls: ['item-details.component.css']})export class ItemDetailsComponent { @Input() item: Item; addItem(): void { this.item.count += 1; };}
Child component
<div> <div> <h2>{{ title }}</h2> </div> <div> {{ body }} </div></div><item-details *ngFor="let item of items" [item]="item"></item-details>
Parent component
import template from './categories.html';import controller from './categories.controller';import './categories.styl';const categoriesComponent = { template, controller, controllerAs: 'categoriesListCtrl'};export default categoriesComponent;
Parent component
import template from './category-item.html';import './category-item.styl';const categoryItemComponent = { bindings: { category: '<', selected: '&' }, template, controllerAs: 'categoryItemCtrl'};export default categoryItemComponent;
Child component
<ul class="nav nav-sidebar"> <li ng-repeat="category in categoriesListCtrl.categories"> <category-item category="category" selected="categoriesListCtrl.onCategorySelected(category)"> </category-item> </li></ul>
Parent component
<div class="categoryItem" ng-click="categoryItemCtrl.selected({category:categoryItemCtrl.category})"> {{categoryItemCtrl.category.name}}</div>
Child component
Create a component controller in ES6
import {Component, OnInit} from '@angular/core';import {MessageService} from '../shared';@Component({ moduleId: module.id, selector: 'home', templateUrl: 'home.component.html', styleUrls: ['home.component.css']})export class HomeComponent {}
Basic component
export class HomeComponent implements OnInit { title: string = 'Home Page'; body: string = 'This is the about home body'; message: string; constructor(private messageService: MessageService) { } ngOnInit() { this.message = this.messageService.getMessage(); } updateMessage(m: string): void { this.messageService.setMessage(m); }}
Basic component class
import template from './categories.html';import './categories.styl';const categoriesComponent = { template};export default categoriesComponent;
Basic component
class CategoriesController { constructor(CategoriesModel) { 'ngInject'; this.CategoriesModel = CategoriesModel; } $onInit() { this.CategoriesModel.getCategories() .then(result => this.categories = result); } onCategorySelected(category) { this.CategoriesModel.setCurrentCategory(category); } isCurrentCategory(category) { return this.CategoriesModel.getCurrentCategory() && this.CategoriesModel.getCurrentCategory().id === category.id; }}export default CategoriesController;
Basic controller class
import template from './categories.html';import controller from './categories.controller';import './categories.styl';const categoriesComponent = { template, controller, controllerAs: 'categoriesListCtrl'};export default categoriesComponent;
Basic component with controller
Refactor controller logic to services
class CategoriesController { constructor() { this.categories = [ {"id": 0, "name": "Development"}, {"id": 1, "name": "Design"}, {"id": 2, "name": "Exercise"}, {"id": 3, "name": "Humor"} ]; }}export default CategoriesController;
Basic controller
class CategoriesModel { constructor() { this.categories = [ {"id": 0, "name": "Development"}, {"id": 1, "name": "Design"}, {"id": 2, "name": "Exercise"}, {"id": 3, "name": "Humor"} ]; }}export default CategoriesModel;
Basic service
@Injectable()export class CategoriesModel { private categories = [ {"id": 0, "name": "Development"}, {"id": 1, "name": "Design"}, {"id": 2, "name": "Exercise"}, {"id": 3, "name": "Humor"} ]; getCategories() { return this.categories; };}
Basic service
Dependency injection in ES6
class CategoriesModel { constructor($q) { // $q is scoped to the constructor only this.categories = [ {"id": 0, "name": "Development"}, {"id": 1, "name": "Design"}, {"id": 2, "name": "Exercise"}, {"id": 3, "name": "Humor"} ]; } getCategories() { return $q.when(this.categories); // wont work! }}export default CategoriesModel;
Scoped to function
class CategoriesModel { constructor($q) { 'ngInject'; // ng-annotate ftw this.$q = $q; // constructor assignment this.categories = [ {"id": 0, "name": "Development"}, {"id": 1, "name": "Design"}, {"id": 2, "name": "Exercise"}, {"id": 3, "name": "Humor"} ]; } getCategories() { return this.$q.when(this.categories); // now we can use $q }}export default CategoriesModel;
Local assignment
class CategoriesModel { constructor($q) {}}
var CategoriesModel = (function () { function CategoriesModel($q) { } return CategoriesModel;}());
Exhibit A: TypeScript
class CategoriesModel { constructor(private $q) {}}
var CategoriesModel = (function () { function CategoriesModel($q) { this.$q = $q; } return CategoriesModel;}());
Exhibit B: TypeScript
Initialize components with lifecycle hooks
Component Lifecycle Hooks
Components have well defined lifecycle hooks that allow us to perform specific operations during the lifespan of our component
We can use $onInit to know when our controller has been has been constructed and its bindings initialized
We can also use $onInit to know when a dependent component is available
We can use $onDestroy to perform clean when our component is removed
class CategoriesController { constructor(CategoriesModel) { 'ngInject'; this.CategoriesModel = CategoriesModel; this.CategoriesModel.getCategories() .then(categories => this.categories = categories); }}export default CategoriesController;
Logic in constructor
class CategoriesController { constructor(CategoriesModel) { 'ngInject'; this.CategoriesModel = CategoriesModel; } $onInit() { this.CategoriesModel.getCategories() .then(categories => this.categories = categories); }}export default CategoriesController;
Logic in lifecycle hook
export class HomeComponent implements OnInit { message: string; constructor(private messageService: MessageService) { } ngOnInit() { this.message = this.messageService.getMessage(); }}
Logic in lifecycle hook
Container and presentational components
Isolated Scope
Isolated scope secures the perimeter of your component so that you can control what goes in and out
Isolated scope is great for defining an API to your directive
There are now four ways to interact with isolated scope: via an attribute, one-way binding, two-way binding or an expression
Inputs and Outputs
Inputs are denoted with an < or @ symbol
Outputs are denoted with an & symbol
@ indicates an attribute binding which is one-way and string based
< indicates a one-way binding that is object based
& is an expression binding which fires a callback on the parent component
bindings: { hero: '<', comment: '@'},bindings: { onDelete: '&', onUpdate: '&'},
Inputs and outputs
<editable-field on-update="$ctrl.update('location', value)"></editable-field><button ng-click="$ctrl.onDelete({hero: $ctrl.hero})"> Delete</button>
Outputs
import template from './category-item.html';import './category-item.styl';let categoryItemComponent = { bindings: { category: '<', selected: '&' }, template, controllerAs: 'categoryItemCtrl'};export default categoryItemComponent;
Inputs and outputs via bindings
<div class="categoryItem" ng-click="categoryItemCtrl.selected({category:categoryItemCtrl.category})"> {{categoryItemCtrl.category.name}}</div>
Inputs and outputs in child template
<ul class="nav nav-sidebar"> <li ng-repeat="category in categoriesListCtrl.categories"> <category-item category="category" selected="categoriesListCtrl.onCategorySelected(category)"> </category-item> </li></ul>
Inputs and outputs in parent template
class CategoriesController { constructor(CategoriesModel) { 'ngInject'; this.CategoriesModel = CategoriesModel; } onCategorySelected(category) { console.log('CATEGORY SELECTED', category); }}export default CategoriesController;
Parent controller
import {Component, Input} from '@angular/core';import {Item} from '../shared';@Component({ moduleId: module.id, selector: 'app-item-details', templateUrl: 'item-details.component.html', styleUrls: ['item-details.component.css']})export class ItemDetailsComponent { @Input() item: Item; addItem(): void { this.item.count += 1; };}
Inputs in child component
<div> <div> <h2>{{ title }}</h2> </div> <div> {{ body }} </div></div><item-details *ngFor="let item of items" [item]="item"></item-details>
Inputs in parent template
Create lightweight controllers by binding to models
class BookmarksController { //... $onInit() { this.BookmarksModel.getBookmarks() .then(bookmarks => this.bookmarks = bookmarks); this.getCurrentCategory = this.CategoriesModel.getCurrentCategory.bind(this.CategoriesModel); // Lexical scope! :( }}export default BookmarksController;
Bind to model in controller
<div class="bookmarks"> <div ng-repeat="bookmark in bookmarksListCtrl.bookmarks | filter:{category:bookmarksListCtrl.getCurrentCategory().name}"> <button type="button" class="close">×</button> <button type="button" class="btn btn-link"> <span class="glyphicon glyphicon-pencil"></span> </button> <a href="{{bookmark.url}}" target="_blank">{{bookmark.title}}</a> </div></div>
Transparent in template
Isolating state mutations in components
import template from './save-bookmark.html';import controller from './save-bookmark.controller';let saveBookmarkComponent = { bindings: { bookmark: '<', save: '&', cancel: '&' }, template, controller, controllerAs: 'saveBookmarkCtrl'};export default saveBookmarkComponent;
Child component
<div class="save-bookmark"> <form ng-submit="saveBookmarkCtrl.save({bookmark:saveBookmarkCtrl.bookmark})" > <div class="form-group"> <label>Bookmark Title</label> <input type="text" ng-model="saveBookmarkCtrl.bookmark.title"> </div> <div class="form-group"> <label>Bookmark URL</label> <input type="text" ng-model="saveBookmarkCtrl.bookmark.url"> </div> <button type="submit">Save</button> <button type="button" ng-click="saveBookmarkCtrl.cancel()">Cancel</button> </form></div>
Child template
class SaveController { $onChanges() { this.editedBookmark = Object.assign({}, this.bookmark); }}export default SaveController;
Lifecycle hook FTW!
<div class="save-bookmark"> <form ng-submit="saveBookmarkCtrl.save({bookmark:saveBookmarkCtrl.editedBookmark})"> <div class="form-group"> <label>Bookmark Title</label> <input type="text" ng-model="saveBookmarkCtrl.editedBookmark.title"> </div> <div class="form-group"> <label>Bookmark URL</label> <input type="text" ng-model="saveBookmarkCtrl.editedBookmark.url"> </div> <button type="submit">Save</button> <button type="button" ng-click="saveBookmarkCtrl.cancel()">Cancel</button> </form> </div>
Updated template
Communicate state changes with an event bus
class CategoriesModel { constructor($q, $rootScope) { 'ngInject'; this.$q = $q; this.$rootScope = $rootScope; this.currentCategory = null; } setCurrentCategory(category) { this.currentCategory = category; this.$rootScope.$broadcast('onCurrentCategoryUpdated'); }}export default CategoriesModel;
Broadcast
class BookmarksController { constructor($scope, CategoriesModel, BookmarksModel) { 'ngInject'; this.$scope = $scope; this.CategoriesModel = CategoriesModel; this.BookmarksModel = BookmarksModel; } $onInit() { this.BookmarksModel.getBookmarks() .then(bookmarks => this.bookmarks = bookmarks;); this.$scope.$on('onCurrentCategoryUpdated', this.reset.bind(this)); } reset() { this.currentBookmark = null; }}export default BookmarksController;
Listen
Testing components with $componentController
describe('Categories', () => { let component, $componentController, CategoriesModel; beforeEach(() => { window.module('categories'); window.module($provide => { $provide.value('CategoriesModel', { getCategories: () => { return { then: () => {} }; } }); }); }); beforeEach(inject((_$componentController_, _CategoriesModel_) => { CategoriesModel = _CategoriesModel_; $componentController = _$componentController_; })); describe('Controller', () => { it('calls CategoriesModel.getCategories immediately', () => { spyOn(CategoriesModel, 'getCategories').and.callThrough(); component = $componentController('categories', { CategoriesModel }); component.$onInit(); expect(CategoriesModel.getCategories).toHaveBeenCalled(); }); });});
Testing a component controller
describe('Categories', () => { let component, $componentController, CategoriesModel; beforeEach(() => { window.module('categories'); window.module($provide => { $provide.value('CategoriesModel', { getCategories: () => { return { then: () => {} }; } }); }); }); beforeEach(inject((_$componentController_, _CategoriesModel_) => { CategoriesModel = _CategoriesModel_; $componentController = _$componentController_; })); describe('Controller', () => { it('calls CategoriesModel.getCategories immediately', () => { spyOn(CategoriesModel, 'getCategories').and.callThrough(); component = $componentController('categories', { CategoriesModel }); component.$onInit(); expect(CategoriesModel.getCategories).toHaveBeenCalled(); }); });});
Testing a component controller
https://github.com/simpulton/dashing
https://github.com/toddmotto/angular-1-5-components-app
http://ngmigrate.telerik.com/
https://www.angular2patterns.com/
https://egghead.io/courses/using-angular-2-patterns-in-angular-1-x-apps
https://ultimateangular.com/
Thanks!