Practical
Component
Communication

with AngularJS & Angular 2

W3P Kafi #3, 02.11.2016

Mathis Hofer, hofer@puzzle.ch

Disclaimer

This is not about Flux* architectures etc.

UI Components

Good ol' components

Desktop GUI Widgets

Component-based software engineering (CBSE)

Arrived to Web Frontend Dev

Component Architecture

Smart vs. Dumb Components

Properties flow down; actions flow up

Unidirectional Dataflow

AngularJS  (1.x)

Scope

What is it?

Context where the model is stored so that
controllers, directives and expressions
can access it.

$rootScope

Two-way databinding

Everything global!

let scope1 = $rootScope.$new(),
    scope2 = scope1.$new();

Scope Inheritance

angular.module('blog', [])
  .component('articleList', () => {
    return {
      scope: true
    };
  });

Access parent's properties/functions

No encapsulation!

Scope Isolation

angular.module('blog', [])
  .component('articleList', () => {
    return {
      scope: {}
    };
  });

Default for 1.5+ Components

How to input/output?

Component Input Binding

1-way binding '<' (1.5+)

2-way binding '='

<contacts-list
  contacts="vm.myContacts">
</contacts-list>
angular.module('app')
  .component('contactsList', {
    bindings: {
      contacts: '<'
    },
    ...
  });

Input Changes

function ContactPanelController($scope) {
  $scope.$watch('contact', function(newContact, oldContact) {
    initFormModel();
  });
}

better: Component API $onChanges

Component Output Binding

<contacts-toolbar
  add="vm.createContact()">
</contacts-toolbar>
angular.module('app')
  .component('contactsToolbar', {
    bindings: {
      add: '&'
    },
    ...
  });

function ToolbarController() {
  var vm = this;

  vm.addAction = function() {
    vm.add();
  }
}

Let's build a component hierarchy!

Routing

Why using URLs?

Open in new tab

Reload (or reopen browser)

Share link

Bookmark

Difficulties with Routing

Doesn't work:

Input/Output Bindings (<, =, &)

 

Works:

State in URL Params

Scope Events

Require Controller

State in Shared Service

Scope Events

$scope.$broadcast()

$scope.$emit()

Require Controller

Access parent component's controller:

angular.module('app')
  .component('contactForm', {
    require: '^^contacts'
    ...
  });

function ContactFormController($element) {
  var contactsController = $element.controller('contacts');
}

Shared Service

Holds data/state

How to handle async?

Outputs

Use Scope Events

or require controller

Let's do some routing!

Angular 2

No "Scope" anymore!

No $rootScope

No "Scope inheritance"

No "Scope Events"

Element Reference

<button (click)="timer.start()">Start</button>
<button (click)="timer.stop()">Stop</button>

<div class="seconds">{{timer.seconds}}</div>

<countdown-timer #timer></countdown-timer>

ViewChild(ren)

export class ParentComponent implements AfterViewInit {
  @ViewChild(CountdownTimerComponent)
  private timerComponent: CountdownTimerComponent;

  ngAfterViewInit() {
    // Timer component is available from now on
    console.log(this.timeComponent.seconds);
  }

  start() {
    this.timerComponent.start();
  } 
}

Observables

RxJS 5

Component Bindings

Input Binding

@Component({ ... })
export class ContactsListComponent {
  @Input() contacts: ContactModel[];
}
<contacts-list
  [contacts]="myContacts">
</contacts-list>

Input Changes

$scope.$watch(...) from Angular 1

@Component({ ... })
export class MyComponent {
  _name: string;

  @Input() set name(name: string) {
    this._name = (name && name.trim());
  }

  get name() {
    return this._name;
  }
}
@Component({ ... })
export class ContactFormComponent
implements OnChanges {
  @Input() contact: ContactModel;

  ngOnChanges(changes) {
    if (changes.contact) {
      this.initFormModel();
    }
  }
}

Setter:

Lifecycle hooks:

Output Binding

@Component({ ... })
export class ContactsToolbarComponent {
  @Output() add = new EventEmitter();

  addAction() {
    this.add.emit();
  }
}
<contacts-toolbar
  (add)="createContact()">
</contacts-toolbar>

Two-way Binding

Banana in a box:

 

 

Convention: contactChange($event)

[(contact)]="myContact"

Let's build a component hierarchy!

Routing

Don't work:

@Input() and @Output()

Element Reference

@ViewChild()

 

Works:

State in URL Params

State in Shared Service

EventEmitters in Shared Service

Shared Service

@Injectable()
export class ContactsService {
  private contactsSource =
    new BehaviorSubject<ContactModel[]>(undefined);

  contacts$ = this.contactsSource.asObservable();

  updateContacts(contacts: ContactModel[]) {
    this.contactsSource.next(contacts);
  }
}

Holds data/state

Scope of service: parent component

BehaviorSubject caches result

Move Outputs to Service

@Injectable()
export class ContactsService {
  destroy: EventEmitter<ContactModel> =
    new EventEmitter<ContactModel>();
}
@Component({...})
export class ContactPanelComponent {
  contact: ContactModel;

  constructor(private service: ContactsService) {}

  destroy() {
    this.service.destroy.emit(this.contact);
  }
}
@Component({...})
export class ContactsComponent {
  constructor(service: ContactsService) {
    service.destroy.subscribe(contact => {
      this.destroy(contact);
    })
  }

  destroy() {...}
}

Let's do some routing!

So What?

Alternatives?

ng-redux

ngrx

...

Thanks!

Attribution: Mir Shuttle diagram by Orionist (CC BY-SA 3.0)

Made with Slides.com