single-spa / single-spa-angular

Helpers for building single-spa applications which use Angular
Apache License 2.0
196 stars 77 forks source link

Page Refresh causing items to load in body element instead of designated element. #330

Open TheColorRed opened 3 years ago

TheColorRed commented 3 years ago

Demonstration

  1. Load root /
  2. Navigate to page that triggers the microservice to load
  3. Reload the page F5

Expected Behavior

When reloading a page that has a microservice the service should load in the same spot.

Actual Behavior

When I navigate to a page single spa loads the microservice correctly:

image image

However, if I reload this page (F5), the microservice loads in the wrong location:

image image

Code

<div class="app-container">
  <mat-toolbar color="primary">
    <mat-toolbar-row fxLayoutAlign="space-between center">
      <!-- Top bar buttons -->
    </mat-toolbar-row>
  </mat-toolbar>

  <mat-sidenav-container style="flex: 1;" [hasBackdrop]="false">
    <mat-sidenav #drawer mode="side" [opened]="true">
      <!-- Sidebar buttons -->
    </mat-sidenav>
    <mat-sidenav-content>
      <div class="router-container">
        <div>
          <router-outlet></router-outlet>
        </div>
         <!--
         Single Spa Microproducts
         -->
        <div *ngIf="isLoggedIn">
          <div id="single-spa-application:file-manager"></div>
          <div id="single-spa-application:resume-builder"></div>
        </div>
      </div>
    </mat-sidenav-content>
  </mat-sidenav-container>
</div>

I am registering the apps like this:

const HOST = environment.production ? 'https://promote.red5js.com' : 'http://127.0.0.1:5500'
const APPS = `${HOST}/apps`

function registerApp(name: string, activeWhen: string | string[], location: string) {
  registerApplication({
    name,
    activeWhen: [].concat(activeWhen),
    customProps: {
      globalEventBus: new GlobalEventBus(),
      globalEventStore: new GlobalEventStore()
    },
    app: () => System.import(location)
  } as RegisterApplicationConfig)
}

// Register the core application navigation, etc.
registerApp('core', '/', `${HOST}/core/main.js`)

// Register all the other applications
registerApp('file-manager', '/file-manager', `${APPS}/file-manager.js`)
registerApp('resume-builder', '/resume-builder', `${APPS}/resume-builder.js`)

start({ urlRerouteOnly: false })

When refreshing the page the item is created in the body element, instead of using the existing div element.

Note: the whole project (including microservices) is using Angular 11.

TheColorRed commented 3 years ago

I have added the following to my main.single-spa.ts file:

const lifecycles = singleSpaAngular({
  domElementGetter: () => {
    return document.getElementById('file-manager')
  }
})

I then renamed the div (seen in the OP)

This works, however when reloading the page, it cannot find the element.

Uncaught Error: domElementGetter did not return a valid dom element

How would I get a dom element that is loaded lazily?

TheColorRed commented 3 years ago

If there currently isn't a way to do this, maybe something like this would work?

https://github.com/single-spa/single-spa-angular/blob/e582c4a356d7e23a994b54dc1ff20f21c4896866/libs/single-spa-angular/internals/src/dom.ts#L7

  if (ivyEnabled()) {
    const domElementGetter = chooseDomElementGetter(options, props);
    getContainerElement(domElementGetter).then(domElement => {
      while (domElement.firstChild) domElement.removeChild(domElement.firstChild);
    }).catch(err => throw new Error(err));
  }

https://github.com/single-spa/single-spa-angular/blob/e582c4a356d7e23a994b54dc1ff20f21c4896866/libs/single-spa-angular/internals/src/dom.ts#L36

function getContainerElement(domElementGetter: DomElementGetter): Promise<never | HTMLElement> {
  return new Promise<HTMLElement>((resolve, reject) => {

    let element: HTMLElement
    let count = 0
    const max = 10
    function watchDom() {
      element = domElementGetter()
      if (!element) {
        if (count < max) {
          count++
          setTimeout(watchDom, 100)
        } else {
          if (!element) {
            reject('domElementGetter did not return a valid dom element')
          }
        }
      } else {
        resolve(element)
      }
    }
    watchDom()
  })
}
daniloesk commented 3 years ago

Sorry for not having time to take a deep look into your problem, but by my quick peek I understand you are using an Angular app as root-config instead of an agnostic HTML/JavaScript one. And then I am betting on tag name conflicts: app-root and router-outlet tags both in root-config app and Angular microservice.