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', ...))
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
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
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 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()
}
}
}
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:
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).vm. I imagine this'll be easier for the Devtools to handle if nothing else.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.
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
Most helpful comment
It's not documented but yes it's possible to render using existing app context:
We can make this a method on the app instance: