Vuejs root and component structure - vue.js

In the documents, the root element is created as follows:
const RootComponent = {
/* options */
}
const app = Vue.createApp(RootComponent)
const vm = app.mount('#app')
However, soon I realized createApp() can be left blank:
const app = Vue.createApp({})
app.component('component-a', {
/* ... */
})
app.mount('#app')
In the second example, what is the root element? A default one?
A side question, I understand it is better to use props and $emit to pass data around, but is there any way to directly access a component through its $data?

Root Component is always app instance.
RootComponent variabile contains global options (property) for app Component. (Component Instance Properties)
About the side question, You should consider if the component handle application data (shared between different component inside your page) or local data between the component and it's parent.
In the first case is better to use a State Management Library as Vuex, otherwise you should use $emit to change parent data passed via props to the child component.

Related

Vue.js 3 : Accessing a dynamically created component

I have a vue.js 3 application that needs to dynamically create components and access them.
The proper way to dynamically create them and injected them in the DOM seems to be th wrap them in an app using createApp, and this part works, when I call test() a new copy of the components appears at the bottom of the DOM.
Now I need to be able to get a reference to that component son I can call public (exposed) methods on it.
In my (actual) code below, what would allow me to get a reference to my component ?
import { createApp, h } from "vue";
import ConfirmModal from '#/views/components/ui/confirm-modal.vue';
function test()
{
let componentApp = createApp({
setup() {
return () => h(ConfirmModal, { title: "Fuck yeah!", type: "primary" });
}
});
const wrapper = document.createElement('div');
wrapper.setAttribute('id', 'confirm-modal-0');
componentApp.mount(wrapper);
document.body.appendChild(wrapper);
let _confirmModal: ConfirmModal = ???????????????????
_confirmModal.show();
}
EDIT : Here's my use-case: I have a helper function in a service which is used to confirm actions (like deletes) using a Modal dialog. This helper/service is pure TS, not a Vue component, but needs to instanciate the modal (which is a vue component), have it injected in the DOM and have its public methods callable (by my helper/service).
Right now, the only thing that works is to have a sungle copy of the modal component in my root layout and have to root layout foward the ref to my service, which can then use the component. But this isn't great because I need multiple instances of the dialog, and isn't good SOC to have to root layout handle a modal dialog.

watch props update in a child created programmatically

I created the child using:
const ComponentClass = Vue.extend(someComponent);
const instance = new ComponentClass({
propsData: { prop: this.value }
})
instance.$mount();
this.$refs.container.appendChild(instance.$el);
When this.value is updated in the parent, its value doesn't change in the child. I've tried to watch it but it didn't work.
Update:
There's an easier way to achieve this:
create a <div>
append it to your $refs.container
create a new Vue instance and .$mount() it in the div
set the div instance's data to whatever you want to bind dynamically, getting values from the parent
provide the props to the mounted component from the div's data, through render function
methods: {
addComponent() {
const div = document.createElement("div");
this.$refs.container.appendChild(div);
new Vue({
components: { Test },
data: this.$data,
render: h => h("test", {
props: {
message: this.msg
}
})
}).$mount(div);
}
}
Important note: this in this.$data refers the parent (the component which has the addComponent method), while this inside render refers new Vue()'s instance. So, the chain of reactivity is: parent.$data > new Vue().$data > new Vue().render => Test.props. I had numerous attempts at bypassing the new Vue() step and passing a Test component directly, but haven't found a way yet. I'm pretty sure it's possible, though, although the solution above achieves it in practice, because the <div> in which new Vue() renders gets replaced by its template, which is the Test component. So, in practice, Test is a direct ancestor of $refs.container. But in reality, it passes through an extra instance of Vue, used for binding.
Obviously, if you don't want to add a new child component to the container each time the method is called, you can ditch the div placeholder and simply .$mount(this.$refs.container), but by doing so you will replace the existing child each subsequent time you call the method.
See it working here: https://codesandbox.io/s/nifty-dhawan-9ed2l?file=/src/components/HelloWorld.vue
However, unlike the method below, you can't override data props of the child with values from parent dynamically. But, if you think about it, that's the way data should work, so just use props for whatever you want bound.
Initial answer:
Here's a function I've used over multiple projects, mostly for creating programmatic components for mapbox popups and markers, but also useful for creating components without actually adding them to DOM, for various purposes.
import Vue from "vue";
// import store from "./store";
export function addProgrammaticComponent(parent, component, dataFn, componentOptions) {
const ComponentClass = Vue.extend(component);
const initData = 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];
}
});
const instance = new ComponentClass({
// store,
data,
propsData,
...componentOptions
});
instance.$mount(document.createElement("div"));
const dataSetter = data => {
Object.keys(data).forEach(key => {
instance[key] = data[key];
});
};
const unwatch = parent.$watch(dataFn || {}, dataSetter);
return {
instance,
update: () => dataSetter(dataFn ? dataFn() : {}),
dispose: () => {
unwatch();
instance.$destroy();
}
};
}
componentOptions is to provide any custom (one-off) functionality to the new instance (i.e.: mounted(), watchers, computed, store, you name it...).
I've set up a demo here: https://codesandbox.io/s/gifted-mestorf-297xx?file=/src/components/HelloWorld.vue
Notice I'm not doing the appendChild in the function purposefully, as in some cases I want to use the instance without adding it to DOM. The regular usage is:
const component = addProgrammaticComponent(this, SomeComponent, dataFn);
this.$el.appendChild(component.instance.$el);
Depending on what your dynamic component does, you might want to call .dispose() on it in parent's beforeDestroy(). If you don't, beforeDestroy() on child never gets called.
Probably the coolest part about it all is you don't actually need to append the child to the parent's DOM (it can be placed anywhere in DOM and the child will still respond to any changes of the parent, like it would if it was an actual descendant). Their "link" is programmatic, through dataFn.
Obviously, this opens the door to a bunch of potential problems, especially around destroying the parent without destroying the child. So you need be very careful and thorough about this type of cleanup. You either register each dynamic component into a property of the parent and .dispose() all of them in the parent's beforeDestroy() or give them a particular selector and sweep the entire DOM clean before destroying the parent.
Another interesting note is that in Vue 3 all of the above will no longer be necessary, as most of the core Vue functionality (reactivity, computed, hooks, listeners) is now exposed and reusable as is, so you won't have to $mount a component in order to have access to its "magic".

How to change value in main vuetify app from component vue

I have two vue files, app.vue and logincomponent.vue.
I use logincomponent.vue to make template that does login box and uses scripts to communicate with go backend in wails, the code itself works, but I'm trying to change value in main app.vue but i cant get it working.
The question is:
"How do I change value of variable in main vue app from component?"
Import:
import LoginScreen from "./components/LoginScreen.vue"
Variable:
data: () => ({
drawer: false,
currentScreenID: 0,
logged: false
Setter:
sendLogin: function () {
var self = this;
if (this.$refs.login_form.validate()) {
self.dialog = true;
self.loadingCircleLogin = true;
self.login_dialog_title = self.login_dialog_logging_title;
window.backend.sendLoginToBackend(self.email, self.password, self.remember_email).then(result => {
if (result === false) {
self.loadingCircleLogin = false;
self.loginFailText = true;
self.login_dialog_title = self.login_dialog_error_title;
} else {
self.dialog = false;
self.currentScreenID = 3;
}
})
}
},
The short answer to if the state of the parent component (or main Vue instance) can be changed from a child component, is no or at least it should not be done. It's an anti-pattern and can produce bugs in your code.
But you have two choices here.
To emit an event from child component, and handling it from parent. So the parent is responsible of changing its own state with its own logic.
When you need to change a value from the main instance from a child component, you emit the event, even with a value you can pass to the emit function, and you program your main instance to listen that event and respond accordingly.
More info here: listening to child component events.
To add a Vuex store to your app. In this way you abstract the state of the app that's common to several components. So your child component could ask the store to change certain state.
More info here: Vuex
Using Vuex is more complex dough, if your app is simple I'd go with first option.

Pass the value of a variable from child component to two parent components, vuejs? nuxt

How to store the state value of a variable?
For example, I have three components: home.vue and map.vue, as well as a common sidebar.vue component, which is used inside home.vue and map.vue.
I pass some data from sidebar.vue (a variable storing sidebar width state). And this data should change dynamically in both components. That is, when switching from home.vue to map.vue, the state of the variable for changing the width of sidebar.vue should not change.
You can store the width of the sidebar in Vuex store.
Vuex is a state management pattern + library for Vue.js applications. It serves as a centralized store for all the components in an application, with rules ensuring that the state can only be mutated in a predictable fashion.
Create the store:
const store = new Vuex.Store({
state: {
width: 50
},
mutations: {
setWidth (state, value) {
state.width = value
}
}
})
"Inject" the store into all child components from the root component with the store option (enabled by Vue.use(Vuex)):
const app = new Vue({
el: '#app',
// provide the store using the "store" option.
// this will inject the store instance to all child components.
store,
...
})
By providing the store option to the root instance, the store will be injected into all child components of the root and will be available on them as this.$store. When the width is changed update it by calling mutation method in the methods of your components:
this.$store.commit('setWidth', value)
To read the value of the width from the store in any method of your vue-components use this:
this.$store.state.width

How to update properties of mounted Vue instance?

I would like to bind the properties of a Vue instance that is manually mounted to several DOM elements (mapbox markers, in this case):
const makerContent = Vue.extend(Marker)
features.forEach(function(feature, index) {
var parent = document.createElement('div')
parent.classList.add("mapboxgl-marker")
var child = document.createElement('div')
child.classList.add("marker")
parent.appendChild(child)
const marker = new mapboxgl.Marker(parent)
.setLngLat(feature.geometry.coordinates)
.addTo(self.map)
new makerContent({
store: store,
propsData: {
feature: feature,
}
}).$mount(child);
})
Changes to the store (eg: feature.geometry.coordinates) do not trigger changes on the Marker component without a reload.
How can I bind the features property to each marker, so the markers react to data changes? Probably not the right use of propsData. Perhaps a shared Vuex store module? I think I need to track the changes of ES6 map state, which Vue does not currently support
See my Mappy.vue component on GitHub