Skip to content

Instantly share code, notes, and snippets.

@jherr
Last active July 20, 2021 16:14
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save jherr/6f5bbd375a769ac3c3f6e968574e160e to your computer and use it in GitHub Desktop.
Save jherr/6f5bbd375a769ac3c3f6e968574e160e to your computer and use it in GitHub Desktop.
ang-pokemon-mfe process

Original article

First create the NX workspace and the initial application named search:

npx create-nx-workspace@latest ang-pokemon-mfe

Set it up as angular, with the name search and SCSS for the styling:

✔ What to create in the new workspace · angular
✔ Application name                    · search
✔ Default stylesheet format           · scss
✔ Use Nx Cloud? (It's free and doesn't require registration.) · No

Create another application called home:

nx generate @nrwl/angular:app home

Don't choose routing (unless you want to):

✔ Would you like to configure routing for this application? (y/N) · false

Add PrimeNG libraries to both applications:

cd apps/home
yarn add primeflex primeicons primeng
cd ../search
yarn add primeflex primeicons primeng

Update the CSS in both applications by removing all content from app.component.scss and adding this to styles.css

@import "primeng/resources/themes/saga-blue/theme.css";
@import "primeng/resources/primeng.min.css";
@import "primeflex/primeflex.css";
@import "primeicons/primeicons.css";

Change the app.module.ts in both applications to:

import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { FormsModule } from '@angular/forms';

import { PanelModule } from 'primeng/panel';
import { ButtonModule } from 'primeng/button';
import { CarouselModule } from 'primeng/carousel';
import { InputTextModule } from 'primeng/inputtext';

import { AppComponent } from './app.component';

@NgModule({
  declarations: [AppComponent],
  imports: [
    BrowserModule,
    FormsModule,
    PanelModule,
    ButtonModule,
    BrowserAnimationsModule,
    CarouselModule,
    InputTextModule,
  ],
  providers: [],
  bootstrap: [AppComponent],
  schemas: [],
})
export class AppModule {}

In home change app.component.html to:

<h2 class="p-5">Home</h2>

<div class="m-6">
  <div class="shadow-8 border-1 border-blue-500 p-5" style="border-radius: 1em;">
    <div class="grid">
      <div class="col-2">
        <img src="https://raw.githubusercontent.com/jherr/fower-pokemon-vue/master/public/pokemon/blastoise.jpg" style="width: 100%" />
      </div>
      <div class="col-10">
        <h2>Blastoise is really cool</h2>
        <div>
          It crushes its foe under its heavy body to cause fainting. In a pinch, it will withdraw inside its shell.
        </div>
      </div>
    </div>
  </div>
</div>

In search change app.component.html to:

<h2>Search</h2>

<div class="grid">
  <div class="col-2">
    <div class="box">
      <p-panel header="Search">
        <input type="text" pInputText [(ngModel)]="search" (ngModelChange)="onSearchChange($event)" />
      </p-panel>
    </div>
  </div>
  <div class="col-10">
    <div class="grid">
      <div class="col-2" *ngFor="let p of pokemon">
        <p-panel [header]="p.name.english">
          <img [src]="p.image" style="height: 150px;" />
        </p-panel>
      </div>
    </div>
  </div>
</div>

And change app.component.ts to:

import { Component, OnInit } from '@angular/core';

interface Pokemon {
  name: {
    english: string;
  };
  image: string;
}

...
export class AppComponent implements OnInit {
  pokemonSource: Pokemon[] = [];
  pokemon: Pokemon[] = [];
  search = '';

  onSearchChange(search: string) {
    this.pokemon = this.pokemonSource
      .filter((p) =>
        p.name.english.toLowerCase().includes(search.toLowerCase())
      )
      .slice(0, 20);
  }

  async ngOnInit() {
    fetch(
      'https://raw.githubusercontent.com/jherr/fower-pokemon-vue/master/public/pokemon.json'
    )
      .then((resp) => resp.json())
      .then((data) => {
        this.pokemonSource = data.map((p: Pokemon) => ({
          ...p,
          image: `https://raw.githubusercontent.com/jherr/fower-pokemon-vue/master/public/pokemon/${p.name.english.toLowerCase()}.jpg`,
        }));
        this.pokemon = this.pokemonSource.slice(0, 20);
      });
  }
}

Fire up the servers:

nx run home:serve:development
nx run search:serve:development --port 4201

Add a new PokemonCarousel component to the search project.

nx generate @nrwl/angular:component PokemonCarousel --project search

Add this to app.component.html

<ang-pokemon-mfe-pokemon-carousel></ang-pokemon-mfe-pokemon-carousel>

Change pokemon-carousel/pokemon-carousel.component.html to:

<div class="max-w-min">
  <p-carousel *ngIf="images.length > 0" [value]="images" [numVisible]="3" [numScroll]="1">
    <ng-template let-item pTemplate="item">
      <h2 [innerText]="item.name" class="center"></h2>
      <img [src]="item.image" style="width: 100%;" />
    </ng-template>
  </p-carousel>
</div>

And pokemon-carousel/pokemon-carousel.component.ts to:

import { Component, OnInit, Input } from '@angular/core';

interface Pokemon {
  name: {
    english: string;
  };
}
interface Image {
  image: string;
  name: string;
}

...
export class PokemonCarouselComponent implements OnInit {
  images: Image[] = [];
  @Input() search = '';

  ngOnInit(): void {
    fetch(
      'https://raw.githubusercontent.com/jherr/fower-pokemon-vue/master/public/pokemon.json'
    )
      .then((resp) => resp.json())
      .then((data) => {
        this.images = data
          .map((p: Pokemon) => ({
            image: `https://raw.githubusercontent.com/jherr/fower-pokemon-vue/master/public/pokemon/${p.name.english.toLowerCase()}.jpg`,
            name: p.name.english,
          }))
          .filter((p: Image) => p.name.toLowerCase().indexOf(this.search) > -1)
          .slice(0, 10);
      });
  }
}

Shut down servers and:

ng add @angular-architects/module-federation --project search --port 4201
ng add @angular-architects/module-federation --project home --port 4200

Change apps/search/webpack.config.js:

      // For remotes (please adjust)
        name: "search",
        filename: "remoteEntry.js",
        exposes: {
            './CarouselComponent': './apps/search/src/app/pokemon-carousel/pokemon-carousel.component.ts',
        },        

Change apps/home/webpack.config.js:

      // For hosts (please adjust)
      remotes: {
        search: 'search@http://localhost:4201/remoteEntry.js',
      },

Start servers:

nx run home:serve:development
nx run search:serve:development --port 4201
nx generate @nrwl/angular:component CarouselHost --project home

Remove everything from carousel-host/carousel-host.component.html and change carousel-host/carousel-host.component.ts to:

import {
  Component,
  OnInit,
  ComponentFactoryResolver,
  ViewContainerRef,
  ComponentRef,
} from '@angular/core';
import { loadRemoteModule } from '@angular-architects/module-federation';

@Component({
  selector: 'ang-pokemon-mfe-carousel-host',
  templateUrl: './carousel-host.component.html',
  styleUrls: ['./carousel-host.component.scss'],
})
export class CarouselHostComponent implements OnInit {
  constructor(
    private cfr: ComponentFactoryResolver,
    private vcref: ViewContainerRef
  ) {}

  async ngOnInit() {
    const { PokemonCarouselComponent } = await loadRemoteModule({
      remoteEntry: 'http://localhost:4201/remoteEntry.js',
      remoteName: 'search',
      exposedModule: './CarouselComponent',
    });
    const componentRef: ComponentRef<{
      search: string;
    }> = this.vcref.createComponent(
      this.cfr.resolveComponentFactory(PokemonCarouselComponent)
    );
    componentRef.instance.search = 'p';
  }
}

Add to app.component.html in home:

<ang-pokemon-mfe-carousel-host></ang-pokemon-mfe-carousel-host>

Change anything in pokemon-carousel.component.html and watch it update automatically.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment