Опыт миграции от AngularJS к Angular6 ngrx/store в условиях стартапа

Юрий Дадычин

FE team-lead

Samples

All given in this presentation

has

no unique value

and

no any intellectual property,

it's just a compilation of

known facts  

Entry point

AngularJS

  • Tone of existing controllers, routes, components, styles
  • API's(not so interesting)
  • 100500 reasons to move from AngularJS to Angular5 or React(according to my co-speakers)

Problem 1

Add Angular5 in same project with AngularJS

Lerna

  • https://github.com/lerna/lerna
  • helps to keep and init multiple npm packages in same repo
  • Able to have cross-packages links 
  • Install's everything from repo root 
lerna init && lerna bootstrap --loglevel verbose

cat lerna.json =>
{
  "lerna": "2.11.0",
  "packages": [
    "packages/*"
  ],
  "version": "0.0.0"
}

cd packages
npm install -g @angular/cli

ng new ng-demo
cd ng-demo

pwd =>
<project-root>/packages/ng-demo

Problem 2

Have AngularJS and Angular5 on the same page

html:

<!-- bootstrap point for AngularJS -->
<div ng-app="demo-app" class="ng-cloak">
    <ng-view></ng-view>
</div>

<!-- bootstrap point for Angular5 -->
<ng-demo></ng-demo>

Friendly Routing

AngularJS

Angular5

(function (angular) {
  angular.module('demo-app').config([
    '$routeProvider',
    function ($routeProvider) {
      $routeProvider
        .when('/test', {
          templateUrl: 'route-one.html',
          controller: 'FirstController',
          controllerAs: 'ctrl',
          bindToController: true
        })
        // and many more
        ;
    },
  ]);
}(window.angular));
import { Routes } from '@angular/router';
import { EmptyComponent } from 
'./shared/components/empty/empty.component';

export const appRoutes: Routes = [
  {
    path: '',
    redirectTo: 'dashboard',
    pathMatch: 'full',
    canActivate: [
    ],
  },
  {
    path: '**', // to be friendly with others 
    component: EmptyComponent, // does nothing
  },
];

What if html?:

<!-- bootstrap point for AngularJS -->
<div ng-app="demo-app" class="ng-cloak">
    <ng-view></ng-view>
</div>

<!-- bootstrap point for Angular5 -->
<ng-demo></ng-demo>
<ng-demo-one-more></ng-demo-one-more>
// custom boostrapper
const bootstrapRootComponent = (app, selector, component) => {
  // check root element existance
  if (window.document.querySelector(selector)) {
    app.bootstrap(component);
  }
};
// components registry selector: component
const entryComponentsRegistry = {
  'ng-demo': NgDemoComponent,
  'ng-demo-one-more': OneMoreRootNgDemoComponent
};

// ...
export class AppModule {
  ngDoBootstrap(app) {

    for (const selector in entryComponentsRegistry) {
      bootstrapRootComponent(app, selector, entryComponentsRegistry[selector]);
    }
  }
}

Problem 3

Angular5 is useless as AngularJS in glance 

ngrx/store

  • No way to integrate by yourself 
  • Does not work with Angular6 yes(beta only)
  • copy/pasta from example app https://github.com/ngrx/platform/tree/master/example-app
  • Tired to copy but made it...

Problem 4

It came, instantly... How to bind store selection to form?

Just forget about the template driven forms

Reactive forms

  • https://angular.io/guide/reactive-forms
  • Looks like a many things to read
  • Even more to code around

AbstractReactiveForm

// created in constructor
  formGroup: FormGroup;
  protected formGroupValuesChanges: Subscription;

  @Input() value = {};
  @Output() onChangeCallback: EventEmitter<any>;
  @Output() onSubmitValue: EventEmitter<any>;

  protected debounceTime = 300;
  protected patchOptions = { emitEvent: false };

  ngOnInit() {
    const patch = Object
      .keys(this.formGroup.controls)
      .reduce(this.getFormGroupValuesReducer(), {});

    this.formGroup.patchValue(patch, this.patchOptions);

    this.formGroupValuesChanges = this.formGroup.valueChanges
    .debounceTime(this.debounceTime)
    .subscribe(() => {
      this.onChangeCallback.emit(this.getAggregatedValue());
    });
  }
getFormGroupValuesReducer() {
    return (acc: Object, field: string) => {
      acc[field] = this.value[field];

      return acc;
    };
  }

  onSubmit() {
    if (this.formGroup.valid) {
      this.onSubmitValue.emit(this.getAggregatedValue());
    }
  }

  getAggregatedValue() {
    return {
      ...this.value,
      ...this.formGroup.value,
    };
  }

Problem5

Something wrong with containers

export class DemoContainerComponent implements OnInit {

  items$: Observable<Items[]>;

  constructor(private store: Store<fromItems.State>) {
    this.items$ = store.select(fromItems.getItems);
  }

  ngOnInit() {
    // silly place to do it 
    this.store.dispatch(new itemsActions.Load());
  }
}

It's a Resolvers job

{
  path: '',
  component: DemoContainerComponent,

  resolve: {
    doesNotMetterWhatKey: DemoResolver,
  },
  children: [
    ...
  ...
}

In a router

DemoResolver

@Injectable()
export class DemoResolver implements Resolve<Item[]> {

  constructor(
    private store: Store<itemsReducers.State>,
  ) { }
  // carefull with the return value, if you are not 
  // returning void resolver will wait for end of Obs..
  resolve(route: ActivatedRouteSnapshot, 
    state: RouterStateSnapshot): Observable<Item[]> {

    return this.store
      .select(itemsReducers.selectItems)
      .do((items: Item[]) => {
        if (items.length === 0) {
          this.store.dispatch(new itemsActions.Load());
        }
      })
      .take(1);
  }
}

Problem 6 

Guards 

Problem with guards is that you have to wait

export const waitForUserToLoad = (store): 
Observable<boolean> => {
  return store
    .select(fromAuth.authInProgress)
    .map((isLoading: boolean) => isLoading)
    .filter(isLoading => !isLoading)
    .first();
};

And you can chain waits

@Injectable()
export class UserLoadedGuard 
implements CanActivate {

  constructor(
    private store: Store<fromAuth.State>,
  ) {}

  canActivate(route: ActivatedRouteSnapshot): 
  Observable<boolean> {
    return waitForUserToLoad(this.store)
      .switchMap(() => {
        return Observable.from([true]);
      });
  }
}

Problem 7

We are hosted on AWS and we have different static assets root 

__webpack_public_path__

// main.ts

import { AppModule } from './app/app.module';
import { environment } from './environments/environment';

const meta = document
  .querySelector('meta[name="static-base-url"]');

const baseUrl = meta ? meta.getAttribute('content') : '/';

declare let __webpack_public_path__;

if (environment.production) {
  __webpack_public_path__ = `${baseUrl}js/`;
  enableProdMode();
} else {
  __webpack_public_path__ = `/`;
}

platformBrowserDynamic()
  .bootstrapModule(AppModule)
  .catch(err => console.error(err));

Problem 8

AngularJS wants(should) notify Angular6 about it's status

Actions Bridge

AngularJS

(function(angular) {
  function PubSubService() { return window.PubSub; }

  angular.module('demo-app')
    .factory('PubSubService', PubSubService);
})(window.angular);


(function(angular) {
  function StoreBridgeService(PubSubService) {
    this.PubSubService = PubSubService;
  }

  angular.extend(StoreBridgeService.prototype, {
    dispatch: function(action) {
      this.PubSubService.publish('Store::dispatch', action);
    }
  });

  StoreBridgeService.$inject = ['PubSubService'];

  angular.module('demo-app')
    .service('StoreBridgeService', StoreBridgeService);
})(window.angular);

Angular5

@Injectable()
export class PubsubService {
  public instance: any = null;

  constructor() {
    this.instance = PubSub;
  }
}

@Injectable()
export class StoreBridgeService {

  constructor(
    private pubsub: PubsubService,
    private store: Store<appReducers.State>,
  ) {
    this.pubsub.instance.subscribe('Store::dispatch', 
    (msg, data) => {
      this.store.dispatch(data);
    });
  }
}

Problem 9

Tooooooooo many scaffolding creating ngrx/store things 

Schematics 

  • https://blog.angular.io/schematics-an-introduction-dc1dfbc2a2b2
  • Those extend ng generator functionality with your custom generators 
  • You can bootstrap new ngrx/store driven modules with one command 

Benefits 

Single
Way
Data flow

Universal design scheme

Questions?

Made with Slides.com