Reputation: 16958
Typically, when using modals within Vue.js components, it is the norm to create a reusable modal
component, and then control the state of this component using events from child components.
For example, consider the following code:
App.vue
<div id="app">
<!-- Main Application content here... -->
<!-- Place any modal components here... -->
<modal ref="ContactForm"></modal>
</div>
ChildComponent.Vue
To open the modal from a child component, we would simply trigger the following event:
bus.$emit('open-modal', 'ContactForm');
Note 1: bus
is a separate Vue instance that allows events to be fired between all components regardless of their relation.
Note 2: I have intentionally left out my modal
component code as it is not relevant to the question.
Whilst the above works absolutely fine, there is one key issue...
In order to add a modal
to my application, instead of placing the modal
component within the child component that references it, I have to place all modals within App.vue
as this ensures they are as high up the DOM tree as possible (to ensure they appear above all content).
As a result, my App.vue
has the potential to end up looking like this:
<div id="app">
<!-- Main Application content here... -->
<!-- Place any modal components here... -->
<modal ref="SomeModal1"></modal>
<modal ref="SomeModal2"></modal>
<modal ref="SomeModal3"></modal>
<modal ref="SomeModal4"></modal>
<modal ref="SomeModal5"></modal>
<modal ref="SomeModal6"></modal>
<modal ref="SomeModal7"></modal>
</div>
It would be much cleaner to be able to place the modal
component within the DOM of the child component.
However, in order to ensure that the modal appears above all content within the DOM (specifically items with a set z-index
), I can't see an alternative to the above...
Can anyone suggest a way in which I can ensure my modals will work correctly, even if they are placed within the child components?
I did think about the following solution but it seems very dirty...
open-modal
eventmodal
component to the parent App.vue
componentIn case the above isn't clear, I am trying to avoid defining all modals in my App.vue
component, and allow defining of my modals within any child component.
The reason I am not able to do this, at present, is because the HTML for the modals must appear as high in the DOM tree as possible in order to ensure they appear above all content.
Upvotes: 2
Views: 2861
Reputation: 90068
Here's what I was talking about:
Create an addProgrammaticComponent
function in a helper, along these lines:
import Vue from 'vue';
export function addProgrammaticComponent(parent, component, dataFn, extraProps = {}) {
const ComponentClass = Vue.extend(component);
// this can probably be simplified.
// It largely depends on how much flexibility you need in building your component
// gist being: dynamically add props and data at $mount time
const initData = dataFn ? dataFn() : {};
const data = {};
const propsData = {};
const propKeys = Object.keys(ComponentClass.options.props || {});
Object.keys(initData).forEach((key) => {
if (propKeys.includes(key)) {
propsData[key] = initData[key];
} else {
data[key] = initData[key];
}
});
// add store props if you use Vuex
// extraProps can include dynamic methods or computed, which will be merged
// onto what has been defined in the .vue file
const instance = new ComponentClass({
/* store, */ data, propsData, ...extraProps,
});
instance.$mount(document.createElement('div'));
// generic helper for passing data to/from parent:
const dataSetter = (data) => {
Object.keys(data).forEach((key) => {
instance[key] = data[key];
});
};
// set unwatch on parent as you call it after you destroy the instance
const unwatch = parent.$watch(dataFn || {}, dataSetter);
return {
instance,
update: () => dataSetter(dataFn ? dataFn() : {}),
dispose: () => {
unwatch();
instance.$destroy();
},
};
}
... and now, where you use it:
Modal.vue
is a typical modal component, but you can beef it up with close on Esc or Del keyspress, etc...
Where you want to open a modal:
methods: {
openFancyModal() {
const component = addProgrammaticComponent(
this,
Modal,
() => ({
title: 'Some title',
message: 'Some message',
show: true,
allowDismiss: true,
/* any other props you want to pass to the programmatic component... */
}),
);
document.body.appendChild(component.instance.$el);
// here you have full access to both the programmatic component
// as well as the parent, so you can add logic
component.instance.$once('close', component.dispose);
// if you don't want to destroy the instance, just hide it
component.instance.$on('cancel', () => {
component.instance.show = false;
});
// define any number of events and listen to them: i.e:
component.instance.$on('confirm', (args) => {
component.instance.show = false;
this.parentMethod(args);
});
},
/* args from programmatic component */
parentMethod(args) {
/* you can even pass on the component itself,
and .dispose() when you no longer need it */
}
}
That being said, nobody stops you from creating more than one Modal/Dialog/Popup component, either because it might have a different template or because it might have significant additional functionality which would pollute the generic Modal
component (i.e: LoginModal.vue
, AddReportModal.vue
, AddUserModal.vue
, AddCommentModal.vue
).
Point here being: they are not added to the app (to DOM), until you actually $mount
them. You don't place the markup in the parent component. And you can define in the opening fn
what props to pass, what to listen to, etc...
Except for the unwatch
method, triggered on the parent, all events are bound to the programmaticComponent instance, so there's no garbage.
This is what I was saying when I said no actual hidden modal instance is lurking on DOM until you open it.
Not even saying this approach is necessarily better than others (but it has some advantages). From my POV, it is just inspired by Vue's flexibility and core principles, it is clearly possible, and it allows the flexibility to .$mount
and dispose of any component (not only modals) onto/from any component.
It's especially good when you need to open the same component from multiple corners of the same complex app and you're serious about DRY.
See vm.$mount
docs.
Upvotes: 1
Reputation: 64312
I place my modals in my child components and it works great. My modal implementation is fundamentally similar the modal example from the docs. I also added in basic a11y features including vue-focus-lock, but the idea is the same.
No event bus, shared state, or refs - just v-if
the modal into existence when needed.
Upvotes: 1