How to change value in main vuetify app from component vue - vue.js

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.

Related

Passing data to an ancestor component vue3

I have a multi-step form in Vue3 that has the following structure:
<Formroot />
...
<formPage2 />
...
<postcodeWidget />
I have a data structure in my formRoot that holds all the field values from the child components and then uses them to make an external API call and present a result.
I use Props to pass the data down to the child components and then emit from the children to the parent.
The issue is, my autocomplete widget - which pulls from an external api - does all the autocomplete in the setup() function. I cannot figure out the best way to communicate input from that widget back up to the formRoot component.
I tried emitting from the widget but I can't access the instance from within setup, and I can't seem to access the data from setup variables within an instance method.
For example, I have a function called changePostcode that fires on input to the field:
methods: {
changePostcode(e){
//I have tried calling the input event:
this.$emit('update:postcode', e.target.value)
//I have tried accessing my setup variable:
this.$emit('update:postcode', this.selectedPostcode) //or postcode.value this is the actull value I want to emit.
//these dont work.They return nothing.
},
}
my selectedPostcode variable is set in the setup() function as follows:
setup() {
...
let selectedPostcode = ref('')
let searchTerm = ref('')
...
// searchTerm is used in a filter with data from an external API to offer suggestions. This is the ultimate source of the "location" object
const selectPostcode = (location) => {
selectedPostcode.value = location.postcode
searchTerm.value = location.locality
}
return {
searchTerm,
...
selectPostcode,
selectedPostcode,
...
}
}
I have a locality and a postcode variable because I want to populate the input with a "locality" that includes the full name of the suburb while I want to emit only the post/zip code.
My setup does a bunch of other work including calling and api for a list of suburb and filtering on user input to make suggestions. That all works fine.
In summary,
A multi step form
One step includes a nested component that needs to pass data up to the root ancestor
I cannot seem to access/emit data from setup() back up to the ancestor element
What is the right way to do this? It seems like it should be a pretty common use case.
I looked into provide/inject as well but I also couldn't understand how to send data back up to the ancestor only down to the child.
The ancestor could provide a function (e.g., a setter) that the nested component could inject to communicate a value back to the ancestor:
// RootAncestor.vue
<script setup>
import { ref, provide } from 'vue'
const postCode = ref('initial value')
const setPostCode = value => {
postCode.value = value
}
provide('setPostCode', setPostCode)
</script>
// NestedComponent.vue
<script setup>
import { inject } from 'vue'
const setPostCode = inject('setPostCode')
const save = () => {
setPostCode('12345')
}
</script>
demo

Vuejs root and component structure

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.

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 can a dynamically instantiated Vue Component be cached?

In my application I instantiate a dialog component programmatically. The dialog can (does) contain a child component to show content. I accomplish this thus:
// Create the containing dialog component.
// I don't care if this is cached or not, and it's easy
// to recreate.
//
var DialogComponent = Vue.extend(Dialog);
var instance = new DialogComponent({
parent: parent,
data: {...}
propsData: {...}
});
// Compile the subcomponent string. In practice this would
// be built dynamically before being passed to
// Vue.compile()
//
const template = '<ComponentName :binding="" ... />'
const x = Vue.compile(template);
const slotContent = {
data() { return data},
render: x.render,
staticRenderFns: x.staticRenderFns
}
// Create a vnode from the compiled render functions. This
// is the part I am less confident in, as it appears the
// render function returned will always instantiate a new
// child component, and it feels like involving `instance`,
// i.e. the dialog, is incorrect.
//
const vnode = instance.$createElement(slotContent);
instance.$slots.default = [ vnode ];
// Mount the dialog component to a DOM element.
//
var tmp = document.createElement('div');
document.body.appendChild(tmp);
instance.$mount(tmp);
// Hook the dialog close event to clean up the instance.
// In practice, when caching I would like to pull the
// child component out and only destroy the dialog instance.
// I can successfully set the child's vnode.data.keepAlive and
// persist the instance, but a new one gets created nonetheless.
//
instance.$on('close', (e) => {
instance.$el.parentNode.removeChild(instance.$el);
instance.$destroy();
})
I believe this is the accepted way of accomplishing this, and it works flawlessly. However, each time the dialog goes away it is of course $destroyed and the component resets when it's shown again.
I now need a method of maintaining the subcomponent's state when the dialog closes and reopens. So imagine a dialog that shows a new Date() on creation— I need that same date to show up after the dialog is closed and reopened. I have tried various sprinklings of <keep-alive> to no avail, but I don't think this is the appropriate use of that component.
Caching the vnode successfully avoids the compile call, but because (I think) the returned render function instantiates a component instance (ComponentName in the example) it does not reuse the original ComponentName, even if I successfully avoid destroying it (vm._isDestroyed == false).
Ultimately I think I'd like to hit line 73 in vue/create-component.js when inserting the child component, but a breakpoint there never gets hit (which may be an unrelated webpack/source-maps thing).
Is there a sane way of accomplishing the caching of a programmatically instantiated Vue component for later reuse similar to how <keep-alive> works?

how can component delete itself in Vue 2.0

as title, how can I do that
from offical documentation just tell us that $delete can use argument 'object' and 'key'
but I want delete a component by itself like this
this.$delete(this)
I couldn't find instructions on completely removing a Vue instance, so here's what I wound up with:
module.exports = {
...
methods: {
close () {
// destroy the vue listeners, etc
this.$destroy();
// remove the element from the DOM
this.$el.parentNode.removeChild(this.$el);
}
}
};
Vue 3 is basically the same, but you'd use root from the context argument:
export default {
setup(props, { root }){
const close = () => {
root.$destroy();
root.$el.parentNode.removeChild(root.$el);
};
return { close };
}
}
In both Vue 2 and Vue 3 you can use the instance you created:
const instance = new Vue({ ... });
...
instance.$destroy();
instance.$el.parentNode.removeChild(instance.$el);
No, you will not be able to delete a component directly. The parent component will have to use v-if to remove the child component from the DOM.
Ref: https://v2.vuejs.org/v2/api/#v-if
Quoted from docs:
Conditionally render the element based on the truthy-ness of the expression value. The element and its contained directives / components are destroyed and re-constructed during toggles.
If the child component is created as part of some data object on parent, you will have to send an event to parent via $emit, modify (or remove) the data and the child component will go away on its own. There was another question on this recently: Delete a Vue child component
You could use the beforeDestroy method on the component and make it remove itself from the DOM.
beforeDestroy () {
this.$root.$el.parentNode.removeChild(this.$root.$el)
},
If you just need to re-render the component entirely you could bind a changing key value to the component <MyComponent v-bind:key="some.changing.falue.from.a.viewmodel"/>
So as the key value changes Vue will destroy and re-render your component.
Taken from here