Как переписать крупный проект на Angular и (не) впасть в депрессию

Илья Таратухин / @darkilfa

Speaker

  • 7 years in frontend
  • 5,5 years in 2GIS
  • Knockoutjs -> own framework -> React
  • 1,5 years in Wrike
  • Dart + Angular
  • Last year work on UI-kit

What is the plan?

  • Project and its problems
  • What's wrong with developers?
  • What's wrong in Angular?
  • Lifehacks

What is Wrike for FE developers?

  • Project with 10 years history
  • Legacy code running on ExtJs + bicycles
  • 50+ FE developers
  • New code runs on Dart + Angular

Angular newcomers

What can go wrong?

Input

@Component({
  selector: 'custom-input'
})
class CustomInput {
  @Input() value: String;
  @Input() label: String;
  @Output() change: EventEmitter<any>;

  @Input() size: String; // s, m, l, xl
  @Input() type: String; // light, invisible
  @Input() fieldType: String; // email, password
  @Input() isReadonly: bool;
  @Input() isAutocomplete: bool;

  @Output() focus: EventEmitter<any>;
  @Output() blur: EventEmitter<any>;

  focus(): void {}
  ...
}
						

// custom input template
<input class="input-text"
  ref-input
  [type]="type"
  [disabled]="isDisabled"
  [placeholder]="placeholderText"
  [readonly]="isReadonly"
  [autocomplete]="autocomplete"
  (input)="onChange($event)"
  (focus)="onFocus($event)"
  (blur)="onBlur($event)"/>

<label *ngIf="isLabelNotEmpty"
  (click)="focus()">
	{{ label }}
</label>
						

@Component({
  selector: 'input[custom-input]'
})
class CustomInput {
  @Input() size: String; // s, m, l, xl
  @Input() skin: String; // light, invisible
}
                        
// Usage in template
<input-meta [label]="labelText">
  <input custom-input
  [type]="'password'"
  [size]="'m'" [skin]="'light'"/>
<input-meta/>


						

Conclusion

Don't wrap native elements

Tooltip


<tooltip
  [trigger]="'click'"
  [position]="'right'"
  [target]="buttonRef">
  Tooltip content
</tooltip>
						

class Tooltip {
  @Input() trigger: String;
  @Input() position: String;
  @Input() target: Element;
}
						

class Tooltip {
  @Input() trigger: String;
  @Input() position: String;
  @Input() target: Element;

  open(): void {}
  close(): void {}

  @Output() onOpen: EventEmitter<any>;
  @Output() onClose: EventEmitter<any>;
}
						

class Tooltip {
  @Input() position: String;
  @Input() target: Element;
}
						

<tooltip *ngIf="isOpened"
  [target]="target"
  [position]="'top'">
  Tooltip content
</tooltip>
						

Conclusion

  • Write stateless components
  • Write simple components

Angular, any problems?

How Angular render work?

NgZone.onStable

What does CD do?

  • Every time on NgZone.onStable
  • For each component
  • For each binding
  • Checks prevVal != nextVal

When can CD brake your app?

  • Your application is big
  • You have data calculation on render
  • You listen sockets in Angular Zone

How to fix it?

  • Subscribe on events out of zone
  • Use OnPush CD strategy

Default change detection

  • Each zone update
  • Checks all tree from root
  • Checks all bindings

OnPush change detection

  • Each zone update
  • Runs CD if @Input change
  • Runs CD if event was in component`s zone
  • Doesn't check subtree if component has no changes
  • You need immutable state tree

How to control CD manually?

  • markForCheck - for mark subtree up to root
  • detectChanges - for trigger CD
  • detach - for skip CD cycles
  • reattach - cancels detach

Dynamic components


<task-list
  [tasks]="tasksModel"
  [taskTemplate]="TaskComponent">
</task-list>
                        

@ContentChildren


<task-list [tasks]="tasksModel">
  <task
    *ngFor="let item of tasksModel.items"
    [model]="item">
  </task>
</task-list>
                        

@Component({
  selector: 'task-list',
  template: '<ng-content></ng-content>'
})
class TaskList implements AfterContentInit {
  @Input() tasks: TasksModel;
  @ContentChildren(Task) tasks: QueryList<Task>;

  ngAfterContentInit() {
    // contentChildren is set
  }
}
                        

@ContentChildren

  • Doesn't help with complicated structures
  • Looks like crunch

NgTemplateOutlet


<task-list
  [tasks]="tasksModel"
  [taskTemplate]="taskTemplate">
</task-list>

<ng-template ref-taskTemplate
  let-item>
  <task [model]="item">
</ng-template>
                        

// Task list template

<ng-container
  *ngTemplateOutlet=
  "taskTemplate; context: taskItem">
</ng-container>
                        

Avoid dynamic components

Lifehacks

Use sandbox

Angular can be really bad guy

  • Too much change detection
  • Need to trigger change detection manually

Do you have it on other tech stack?

Use external library!

Result

  • Don't wrap native elements
  • Stateless components
  • OnPush
  • Subscriptions out of zone
  • Avoid dynamic components
  • Use sandbox
  • Don't write bicycles

Thank you

- Follow me: @darkilfa
- : @wriketeam