single-spa / single-spa-vue

a single-spa plugin for vue.js applications

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

problem mounting with `el` option after unmounted

karladler opened this issue · comments

When I add the el option the first time the instance is correctly rendered into the selected element.
But since vue replaces this element there is a problem mounting it again after unmounted, because the desired element does not exists anymore on the DOM.

The provided element merely serves as a mounting point. Unlike in Vue 1.x, the mounted element will be replaced with Vue-generated DOM in all cases. It is therefore not recommended to mount the root instance to or .

In my case I just want to layout the positions of the single applications.

  <div id="all-apps">
      <div id="navbar"></div>
      <div id="first" style="width: 50%;">
        <div></div>
      </div>
      <div id="second" style="width: 50%;">
        <div></div>
      </div>
    </div>
const vueLifecycles = singleSpaVue({
  Vue,
  appOptions: {
    render: (h) => h(App),
    router,
    el: '#first > div'
  },
});

The <div class="single-spa-container"> shown below is how this avoided when the el option is not provided:

// single-spa-vue@>=2 always REPLACES the `el` instead of appending to it.
// We want domEl to stick around and not be replaced. So we tell Vue to mount
// into a container div inside of the main domEl
if (!domEl.querySelector(".single-spa-container")) {
const singleSpaContainer = document.createElement("div");
singleSpaContainer.className = "single-spa-container";
domEl.appendChild(singleSpaContainer);
}

We could perhaps try to do something similar when #el is provided. Do you have any ideas about how to solve this?

Hi @karladler ! As you want to make a custom layout I think this is vue matter and not single-spa's.

I had to do a custom configuration in my root application and my single-spa-vue app for keep the anchor all time for un/mount so many times I want.

You can see this strategy in next files:

https://github.com/jualoppaz/single-spa-vue-app/blob/0723ce4b073c3e31b94bb39f134d8128f5d10902/src/singleSpaEntry.js#L21-L28

https://github.com/jualoppaz/single-spa-vue-app/blob/0723ce4b073c3e31b94bb39f134d8128f5d10902/src/App.vue#L2

https://github.com/jualoppaz/single-spa-login-example-with-npm-packages/blob/e73d0dd972726bcbfafe2700c5a62700cd3bf078/index.html#L16

The <div class="single-spa-container"> shown below is how this avoided when the el option is not provided:

// single-spa-vue@>=2 always REPLACES the `el` instead of appending to it.
// We want domEl to stick around and not be replaced. So we tell Vue to mount
// into a container div inside of the main domEl
if (!domEl.querySelector(".single-spa-container")) {
const singleSpaContainer = document.createElement("div");
singleSpaContainer.className = "single-spa-container";
domEl.appendChild(singleSpaContainer);
}

We could perhaps try to do something similar when #el is provided. Do you have any ideas about how to solve this?

Spent quite a lot time trying to understand why error happens on unmount stage, when parcel is mounted by single-spa-react/parcel. I am not common with Vue and did not know that it replaces the element instead of appending, which causes React to crash.

Ended up with similar solution:

const vueLifecycles = singleSpaVue({
  Vue,
  appOptions: {
    render: (h) => h(App)
  }
});

export const bootstrap = vueLifecycles.bootstrap;
export const mount = (props) => {
  if (props.domElement && props.append) {
    const el = document.createElement('div');
    props.domElement.appendChild(el);
    props.domElement = el;
  }
  return vueLifecycles.mount(props);
};
export const unmount = vueLifecycles.unmount;

I'm very open to figuring out a better way of handling these situations - perhaps automatically appending the wrapper container when the #el option is provided.

Hello, we have a lot of components, so we didn't want to add loads of empty wrapper divs in our html-template. We worked around the issue by creating a new component-div with a random id during the mount phase, which gets appended to our main-wrapper. The newly crated component-div gets replaced with the single-spa app by Vue.
Unfortunately, we can't access the newly created element for removal after it was replaced with the Vue app. So as a workaround, we empty the main-wrapper manually on unmount. The same logic is then repeated for the new app. If there was a way to just cleanly remove the newly created component-div without having to remove all siblings this would be great.

HTML

<body class="wrapper">
    <div class="sidebar-wrapper">
        <div id="sidebar"></div>
    </div>
    <main class="main-wrapper" id="main-wrapper"></main>
</body>

TS

...
import nanoid from 'nanoid'

const createComponentRootElement = (rootId: string): void => {
  const spaWrapper: HTMLElement = document.getElementById(rootId) || document.body

  const newComponent: HTMLElement = Object.assign(
    document.createElement('div'), {
      id: componentId
    }
  )
  spaWrapper.appendChild(newComponent)
}

const clearComponentRootElement = (rootId: string): void => {
  const spaWrapper: HTMLElement = document.getElementById(rootId) || document.body

  while (spaWrapper.firstChild) {
    spaWrapper.removeChild(spaWrapper.firstChild)
  }
}

const rootClass: string = 'main-wrapper'
const componentId: string = 'id' + nanoid().toLocaleLowerCase()

const vueLifecycles = singleSpaVue({
  Vue,
  appOptions: {
    render: (h:any) => h(ComponentName),
    router,
    el: `#${componentId}`
  }
})

export const bootstrap = [
  vueLifecycles.bootstrap
]

export const mount = [
  () => {
    createComponentRootElement(rootClass)

    return Promise.resolve()
  },
  vueLifecycles.mount
]

export const unmount = [
  vueLifecycles.unmount,
  () => {
    clearComponentRootElement(rootClass)

    return Promise.resolve()
  }
]

I just submitted a fix in #34