Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature Request: Create app with same appContext? createChildApp #2097

Closed
07akioni opened this issue Sep 11, 2020 · 24 comments
Closed

Feature Request: Create app with same appContext? createChildApp #2097

07akioni opened this issue Sep 11, 2020 · 24 comments

Comments

@07akioni
Copy link
Contributor

07akioni commented Sep 11, 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)
@07akioni 07akioni changed the title Create app with same appContext? createChildApp Feature Request: Create app with same appContext? createChildApp Sep 11, 2020
@lawvs
Copy link
Contributor

lawvs commented Sep 11, 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, ...))

@07akioni
Copy link
Contributor Author

07akioni commented Sep 11, 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.

@07akioni
Copy link
Contributor Author

07akioni commented Sep 11, 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.

@lawvs
Copy link
Contributor

lawvs commented Sep 11, 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')

@07akioni
Copy link
Contributor Author

07akioni commented Sep 11, 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.

@07akioni
Copy link
Contributor Author

07akioni commented Sep 11, 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()
    }
  }
}

@posva
Copy link
Member

posva commented Sep 11, 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!

@posva posva closed this as completed Sep 11, 2020
@skirtles-code
Copy link
Contributor

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.

@hzyhbk
Copy link

hzyhbk commented Sep 22, 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.

@jw-foss
Copy link

jw-foss commented Sep 30, 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.

@yyx990803
Copy link
Member

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'))

@skirtles-code
Copy link
Contributor

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.

@mannymu
Copy link

mannymu commented Dec 2, 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);
         
    }
}

@zouhangwithsweet
Copy link
Contributor

I developed a small tool that allows me to use functions to mount VueComponent.
🚴‍♂️ vue-create-component

@WormGirl
Copy link

WormGirl commented Jul 5, 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>

@aabeborn
Copy link

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?

@nikolas223
Copy link

nikolas223 commented Oct 4, 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.

@infinite-system
Copy link

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 :(

@infinite-system
Copy link

infinite-system commented Dec 16, 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.

@nikolas223
Copy link

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

@infinite-system
Copy link

infinite-system commented Dec 20, 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.

@Fuzzyma
Copy link

Fuzzyma commented Jan 5, 2023

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

@eddyzhang1986
Copy link

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

@eddyzhang1986
Copy link

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

@github-actions github-actions bot locked and limited conversation to collaborators Sep 29, 2023
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

No branches or pull requests