Feature Request: Create app with same appContext? createChildApp
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)
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, ...))
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.
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.
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')
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.
This is one way to implement the same features in vue3. No need to use a new app.
Sorry for using jsxuse 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()
}
}
}
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!
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.
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.
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
.
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 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:
- We need a way to remove the newly rendered content.
- This API for
render
is more similar tocreateApp
andmount
, albeit combined into one (I've glossed over SSR but I don't see any reason that couldn't be supported). - This keeps explicit VNodes out of it.
- 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. - The new
vm
is returned byrender
. 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.
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);
}
}
I developed a small tool that allows me to use functions to mount VueComponent. :biking_man: vue-create-component
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>
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?
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.
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 :(
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.
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 callingchildApp.mount(el)
, the expose variable gets assigned fromnull
to the context, from where you can accessexpose.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
@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.
@yyx990803 are there any considerations around this topic? You mentioned this could be a documented feature and not just a hack.
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:
- We need a way to remove the newly rendered content.
- This API for
render
is more similar tocreateApp
andmount
, albeit combined into one (I've glossed over SSR but I don't see any reason that couldn't be supported).- This keeps explicit VNodes out of it.
- 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.- The new
vm
is returned byrender
. 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