Subscribe on changes!

Feature Request: Create app with same appContext? createChildApp

avatar
Sep 11th 2020

What problem does this feature solve?

Somethings I need to created detached components.

For example I may call this.$Message.info(content) in which content may be a render function and the component created will be mounted on document.body.

For example: Before calling $Message.info

<body>
  <div id="app />
</body>

After calling $Message.info

<body>
  <div id="app">
  <div class="message" />
</body>

Calling createApp(MessageComponent).mount(document.body) inside $Message.info may render the component in body. However the render function will use the new appContext rather than the original appContext which has already been registered with custom components. For example:

this.$Message.info(() => h('component-registered-on-app', ...))

What does the proposed API look like?

const app = createApp(rootComponent)
app.mount(document.body)
const childApp = app.createChildApp(detachedComponentWhichNeedSameContext)
childApp.mount(document.body)
avatar
Sep 11th 2020

Maybe you should use resolveComponent or Teleport?

import { resolveComponent } from 'vue'

this.$Message.info(() => h(resolveComponent('component-registered-on-app'), ...)) // Error ❌

// Edited
const messageContent = resolveComponent('component-registered-on-app')
this.$Message.info(() => h(messageContent, ...))
avatar
Sep 11th 2020

Maybe you should use resolveComponent or Teleport?

import { resolveComponent } from 'vue'

this.$Message.info(() => h(resolveComponent('component-registered-on-app'), ...))

resolveComponent works if it is available in the current application instance. However it's called in new app.

Teleport won't work. Message won't be rendered in the current component tree. What I called message is a toast in material design.

avatar
Sep 11th 2020

Let me explain more percisely.

pseudo code in vue 2/3

index

Vue2
import otherComponents from 'ui'
import MessagePlugin from 'message'
import Vue from 'vue'

Vue.use(otherComponents)
Vue.use(MessagePlugin)

Vue3
import otherComponents from 'ui'
import MessagePlugin from 'message'
import { createApp } from 'vue'

const app = createApp(root)

app.use(otherComponents)
app.use(MessagePlugin)

MessagePlugin

Vue2
Vue.prototype.$Message = {
  info (content) {
    document.body.appendChild((new Vue(MessageComponent, {
      propsData: { content }
    })).$mount())
  }
}
Vue3
app.config.globalProperties.$Message = {
  info (content) {
    createApp(MessageComponent, {
      content
    }).mount(document.body)
  }
}

use it

...
Vue2
  this.$Message.info(h => h('other-component'))
Vue3
  this.$Message.info(() => h(???)) // 'other-component' won't be resolved since it's in another app
...

However in vue-next. (new Vue(root)).$mount() is replace by createApp(root).mount(). The original MessageComponent's content render function will resolve components from Vue so different root can share the same installed components. However the new Message component will resolve components from app which means the originally installed components won't be resolved in new Message component.

avatar
Sep 11th 2020

This is one way to implement the same features in vue3. No need to use a new app. Sorry for using jsx

// Message HOC
import { Teleport, defineComponent, provide, ref } from 'vue'

const Message = ({ open, text }) => (
  <Teleport to="body">{open && <div className="modal">{text}</div>}</Teleport>
)

// HOC
const withMessage = (Comp) =>
  defineComponent({
    setup() {
      const show = ref(false)
      const text = ref('Message')
      provide('message', (t) => {
        show.value = true
        text.value = t
      })

      return () => (
        <>
          <Comp></Comp>
          <Message open={show.value} text={text.value}></Message>
        </>
      )
    },
  })

use it

import { createApp, defineComponent, inject } from 'vue'

const App = defineComponent({
  setup() {
    // Use
    const useMessage = inject('message')
    const onClick = () => useMessage(<div>new message</div>)

    return () => <button onClick={onClick}>show message</button>
  },
})

createApp(withMessage(App)).mount('#app')
avatar
Sep 11th 2020

This is one way to implement the same features in vue3. No need to use a new app. Sorry for using jsx

// Message HOC
import { Teleport, defineComponent, provide, ref } from 'vue'

const Message = ({ open, text }) => (
  <Teleport to="body">{open && <div className="modal">{text}</div>}</Teleport>
)

// HOC
const withMessage = (Comp) =>
  defineComponent({
    setup() {
      const show = ref(false)
      const text = ref('Message')
      provide('message', (t) => {
        show.value = true
        text.value = t
      })

      return () => (
        <>
          <Comp></Comp>
          <Message open={show.value} text={text.value}></Message>
        </>
      )
    },
  })

use it

import { createApp, defineComponent, inject } from 'vue'

const App = defineComponent({
  setup() {
    // Use
    const useMessage = inject('message')
    const onClick = () => useMessage(<div>new message</div>)

    return () => <button onClick={onClick}>show message</button>
  },
})

createApp(withMessage(App)).mount('#app')

It's reasonable.

However it seems the API app.use(MessagePlugin) won't work in the case. Is there any possibility to keep the API?

createApp(withMessage(withNotification(withConfirm(app))) is an ugly api composition.

avatar
Sep 11th 2020

This is one way to implement the same features in vue3. No need to use a new app. Sorry for using jsx

use it

What's more, when using sfc the usage is too complicate for a component library user. despite of message-privider, new usage

export default {
  setup () {
    return {
      message: inject('message')
    }
  },
  methods: {
    do () {
      this.message.info()
    }
  }
}

original

export default {
 methods: {
    do () {
      this.message.info()
    }
  }
}
avatar
Sep 11th 2020

If you want your library to render elements, give the user a component to put in their app where you pass the messages. The solution with a hoc by @lawvs also works


Remember to use the forum or the Discord chat to ask questions!

avatar
Sep 11th 2020

I think this deserves further consideration.

The suggestion in the documentation to 'create a factory function' to share application configuration is much less straightforward than it sounds.

I've seen people ask about this several times on the forum and Stack Overflow, so it seems to be a common problem, and so far I haven't seen a really compelling answer. Perhaps it's just a documentation problem but either way it is a problem that needs addressing properly.

avatar
Sep 22nd 2020

I've meet same issue here when I try to upgrade my vue plugin libriary to vue3. It seems like there is no better way except create a factory function or hoc, but the usage is too complicate for a component library user.

Hope the vue team can porvide some other ways to slove this problem.

avatar
Sep 30th 2020

If you want your library to render elements, give the user a component to put in their app where you pass the messages. The solution with a hoc by @lawvs also works

Remember to use the forum or the Discord chat to ask questions!

Indeed @lawvs's solution is a good solution, what about writing 3rd party library? When 3rd party library exports Message method for user to use, the user will have to do things like withMessage(app) in order to get the message component injected into the app. I am working on a library ElementPlus, my implementation here works for now, but the component itself is sandboxed from the main app, which means it cannot fetch any global data from the app itself, this could potentially be a problem when user need to access global data like config.

avatar
Oct 13th 2020

It's not documented but yes it's possible to render using existing app context:

import { h, render } from 'vue'

const childTree = h('div', 'some message')
treeToRender.appContext = app._context
render(childTree, document.getElementById('target'))

We can make this a method on the app instance:

app.render(h('div', 'some message'), document.getElementById('target'))
avatar
Oct 16th 2020

I did some experimenting using the idea @yyx990803 suggested. Here is a crude demo:

https://jsfiddle.net/skirtle/94sfdLvm/

I changed the API slightly:

// To add
const vm = app.render(Component, props, el)

// To remove
app.unrender(vm)

The reasoning behind my changes was:

  1. We need a way to remove the newly rendered content.
  2. This API for render is more similar to createApp and mount, albeit combined into one (I've glossed over SSR but I don't see any reason that couldn't be supported).
  3. This keeps explicit VNodes out of it.
  4. It ensures that all VNodes and elements have an associated vm. I imagine this'll be easier for the Devtools to handle if nothing else.
  5. The new vm is returned by render. It isn't clear how it would be available otherwise.

I ran into a problem trying to render multiple things to the same parent, which I think is important for the use cases here. In my demo I bodged around it by adding in an extra <div> for each item but that isn't ideal as it pollutes the DOM with extra junk.

avatar
Dec 2nd 2020
    import { createVNode ,render} from 'vue'
const body = document.body;
const root = document.createElement("div");
body.appendChild(root);
root.className = "custom-root";
export default {
    install(app){
        let div = document.createElement("div");
        root.appendChild(div);
        // youCom 为自己写的组件,  SoltChild 可以是自己的子组件 ,也可以不传
        let vm = createVNode(youCom,{},{
            // slots
            default:()=>createVNode(SoltChild)
        });
        vm.appContext = app._context; // 这句很关键,关联起了上下文
        render(vm,div);
         
    }
}
avatar
Dec 2nd 2020

I developed a small tool that allows me to use functions to mount VueComponent. :biking_man: vue-create-component

avatar
Jul 5th 2022

It's not documented but yes it's possible to render using existing app context:

import { h, render } from 'vue'

const childTree = h('div', 'some message')
treeToRender.appContext = app._context
render(childTree, document.getElementById('target'))

We can make this a method on the app instance:

app.render(h('div', 'some message'), document.getElementById('target'))

Why childTree.component cannot get the methods at component, it is undefind. I do something like this

import ShowErrorDialog from './src/errorDialog.ts';
import type { Plugin, App } from 'vue';

export type SFCWithInstall<T> = T & Plugin;

const _showErrorDialog = ShowErrorDialog as SFCWithInstall<
  typeof ShowErrorDialog
>;

_showErrorDialog.install = (app: App) => {
  _showErrorDialog._context = app._context;
  app.config.globalProperties.$systemError = _showErrorDialog;
};

export default _showErrorDialog;


// errorDialog.ts
import ErrorDialogConstruct from './index.vue';
import { isEmpty, isFunction } from 'lodash-es';
import { h, render } from 'vue';

interface Option {
  title: string;
}
let instance;
const stack: Option[] = [];

const genContainer = () => {
  return document.createElement('div');
};

const showErrorDialog = (opts, appContext) => {
  const { title, description, errorCode, traceCode } = opts;
  if (!instance) {
    const props = {};
    const vnode = h(ErrorDialogConstruct, props);
    const container = genContainer();

    vnode.appContext = appContext ?? showErrorDialog._context;
    render(vnode, container);
    // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
    document.body.appendChild(container!);
    instance = vnode.component;
  }
  const options = {
  };
  stack.push(options);
  // $open is undefined, how to get the methods?
  instance.$open(stack);
 // this can work
 // instance.ctx.$open(stack);
  instance.onOk('ok', () => {
    if (isFunction(opts.onOk)) {
      opts.onOk(instance);
    }
    instance = null;
  });
};
showErrorDialog._context = null;
export default showErrorDialog;

// index.vue
<template>
  <a-modal v-model:visible="visible">demo</a-modal>
</template>

<script lang="ts">
import { defineComponent } from 'vue';
import { message } from 'ant-design-vue';

export default defineComponent({
  name: 'ErrorDialog',
  emits: ['open', 'close', 'closed', 'opened'],
  data() {
    return {
      visible: false
    };
  },
  methods: {
    $open(stack) {
      // TODO
    }
  }
});
</script>
avatar
Jul 11th 2022

It's not documented but yes it's possible to render using existing app context:

import { h, render } from 'vue'

const childTree = h('div', 'some message')
treeToRender.appContext = app._context
render(childTree, document.getElementById('target'))

We can make this a method on the app instance:

app.render(h('div', 'some message'), document.getElementById('target'))

I tested this approach but using the render method I lose props' reactivity. Is that normal?

avatar
Oct 4th 2022

It's not documented but yes it's possible to render using existing app context:

import { h, render } from 'vue'

const childTree = h('div', 'some message')
treeToRender.appContext = app._context
render(childTree, document.getElementById('target'))

We can make this a method on the app instance:

app.render(h('div', 'some message'), document.getElementById('target'))

I tested this approach but using the render method I lose props' reactivity. Is that normal?

Yes i tested this approach too and props are not reactive. The only way i found to make them reactive was

const treeToRender = createApp(() => h('div', { reactiveProp: true }); treeToRender._context = app._context; treeToRender.mount('#target');

but i cannot bind the context that way, whereas with the first approach the context is bound successfully. This would be very useful if can happen.

avatar
Dec 15th 2022

It's not documented but yes it's possible to render using existing app context:

import { h, render } from 'vue'

const childTree = h('div', 'some message')
treeToRender.appContext = app._context
render(childTree, document.getElementById('target'))

We can make this a method on the app instance:

app.render(h('div', 'some message'), document.getElementById('target'))

I tested this approach but using the render method I lose props' reactivity. Is that normal?

Yes i tested this approach too and props are not reactive. The only way i found to make them reactive was

const treeToRender = createApp(() => h('div', { reactiveProp: true }); treeToRender._context = app._context; treeToRender.mount('#target');

but i cannot bind the context that way, whereas with the first approach the context is bound successfully. This would be very useful if can happen.

@nikolas223

Exactly my discovery at the moment, second way you showed cannot bind context, but it has reactivity, I really need context :(

avatar
Dec 16th 2022

Guys! I figured it out! the full solution to maintain full props reactivity and get the defineExpose() interface! Here is how:

function createComponent ({ app, component, props, el }) {
  
  let expose = null
  
  const childApp = createApp({ render: () => expose = h(component, props) })

  Object.assign(childApp._context, app._context) 

  childApp.mount(el)

  return expose.component.exposed
}

By supplying expose variable into the render function, and then calling childApp.mount(el), the expose variable gets assigned from null to the context, from where you can access expose.component.exposed param to get the exposed interface of your component!

:-)

Now you can use reactive({ props }) as props and they will all be reactive.

avatar
Dec 20th 2022

Guys! I figured it out! the full solution to maintain full props reactivity and get the defineExpose() interface! Here is how:

function createComponent ({ app, component, props, el }) {
  
  let expose = null
  
  const childApp = createApp({ render: () => expose = h(component, props) })

  Object.assign(childApp._context, app._context) 

  childApp.mount(el)

  return expose.component.exposed
}

By supplying expose variable into the render function, and then calling childApp.mount(el), the expose variable gets assigned from null to the context, from where you can access expose.component.exposed param to get the exposed interface of your component!

:-)

Now you can use reactive({ props }) as props and they will all be reactive.

Yes, it works. Thanks! Why does it work tho with object.assign this way and with simple assignment the other way is close to paranormal activity hah

avatar
Dec 20th 2022

@nikolas223 Haha, I know right, that's how it felt when I managed to do it, but then now that I thought about it more and it makes sense, since childApp is a parent of the child component defined by the h() function, and render function gets called only on childApp.mount(el) not before.

avatar
Jan 5th 2023

@yyx990803 are there any considerations around this topic? You mentioned this could be a documented feature and not just a hack.

avatar
Sep 14th 2023

the providers and the plugins seems lost, i must reinstall and reprovides them

avatar
Sep 14th 2023

I did some experimenting using the idea @yyx990803 suggested. Here is a crude demo:

https://jsfiddle.net/skirtle/94sfdLvm/

I changed the API slightly:

// To add
const vm = app.render(Component, props, el)

// To remove
app.unrender(vm)

The reasoning behind my changes was:

  1. We need a way to remove the newly rendered content.
  2. This API for render is more similar to createApp and mount, albeit combined into one (I've glossed over SSR but I don't see any reason that couldn't be supported).
  3. This keeps explicit VNodes out of it.
  4. It ensures that all VNodes and elements have an associated vm. I imagine this'll be easier for the Devtools to handle if nothing else.
  5. The new vm is returned by render. It isn't clear how it would be available otherwise.

I ran into a problem trying to render multiple things to the same parent, which I think is important for the use cases here. In my demo I bodged around it by adding in an extra <div> for each item but that isn't ideal as it pollutes the DOM with extra junk.

the providers and the plugins seems lost, i must reinstall and reprovides them