Vue3 split form into components avoiding mutating props - vue.js

I've created a component which does a lot of work:
Receives data from API
Populate so many inputs with this data
Manages the form submission doing the API post with data inside inputs
So the component is working but I'd like to split into small components, my idea was something like:
ViewForm: Receives Data from API and pass down to sub components. Submit form doing the POST call.
SubFormA: Show a group of inputs related with "section A" and populate with props passed from ViewForm.
SubFormB: Same but with "Section B"
can be more subForms...
viewForm
<template>
<sub-form-a v-model="formA"></sub-form-a>
<sub-form-b v-model="formB"></sub-form-b>
<button #click="submitHandler">Send</button>
</template>
<script setup>
import {ref} from 'vue'
let formA = ref({ //This data would come from an API call, but for showing the example.
a: "aaaa",
b: "bbbb",
})
let formB = ref({ c: "cccc", d: "dddd" })
function submitHandler(){
// Here I should have variables formA and formB updated with that user entered on the inputs.
}
</script>
subFormA // subFormB
<template>
<input v-model="modelValue.a" type="text" />
<input v-model="modelValue.b" type="text" />
</template>
<script setup>
const props = defineProps(["modelValue"]);
// validate inputs with rules
</script>
Eslint warns me with the popular problem:
ESLint: Unexpected mutation of "modelValue" prop.(vue/no-mutating-props)
It's weird because devtools doesn't show me this warning... Vue3 removed this warning or I'm missing something?
I've read a lot of this problem but I'm still don't understand...
Why is this an anti-pattern? I saw a lot of examples where I see the problem, but with my example, it's a form, this is not what we want? that the child can directly modify the props? I see no problem here, I want that the parent component can overwrite all inputs with props, but at the same time childs modify the parent data, so then parent component can submit the form easily.
So how can I fix this?
Should child components clone the props and using their local data to populate the inputs and watch all inputs and emit the data? But then the parent component will have to create another variable to receive the updated data right? Like:
viewForm
<template>
<sub-form-a :data="formA" #update="updateFormAHandler"></sub-form-a>
<sub-form-b :data="formB" #update="updateFormBHandler"></sub-form-b>
<button #click="submitHandler">Send</button>
</template>
<script setup>
import {ref} from 'vue'
let formA = ref({ //This data would come from an API call, but for showing the example.
a: "aaaa",
b: "bbbb",
})
let formB = ref({ c: "cccc", d: "dddd" })
let formAUpdated = ref({})
let formBUpdated = ref({})
function updateFormAHandler(data){
formAUpdated = data
}
function updateFormBHandler(data){
formBUpdated = data
}
function submitHandler(){
// Here I should have variables formA and formB updated with that user entered on the inputs.
// Submit formAUpdated and formBUpdated.
}
</script>
This is the way? I feel a bit confusing...

You don't need to emit anything from your component in your scenario, you're simply binding the data. Emit would be used for triggering custom events on the parent, like closing pop-ups (for example).
In your component, you don't want to mutate the data but you want to update a local version of it as per Vue rules. This can be done with a computed property in your component like so:
computed: {
localFormA() {
return this.formA
},
},
And then use localFormA in your component to bind to the inputs as normal, so you're not mutating the original prop being passed down.
<input type="text" v-model="localFormA.myProperty"/>
In your parent, you pass down formA as a prop:
<sub-form-a :formA="formA"></sub-form-a>

Related

Is is possible to call watch() on an input ref in Vue 3 to watch the input's value attribute?

I know that we can use the v-model directive on an <input> element and use watch() to trigger a function when the state of the <input>'s value attribute changes.
I'm trying, however, to watch an <input> ref. When I do, the watcher's function is executed when the element is mounted to the DOM, but does not trigger when the <input>'s value attribute changes.
Am I doing something wrong?
<script setup>
import { ref, watch, watchEffect } from 'vue'
const refA = ref(null)
watch(refA, () => {
console.log('refA changed')
console.log(refA)
console.log(refA.value)
console.log(refA.value.value)
}, { deep: true })
</script>
<template>
<input ref="refA" type="text" value="test" /> <br />
</template>
From what I know, template refs can be watched, but they only fire when the .value changes - that's the value of the ref, i.e. the HTML element, not the value of a HTML input element. That fits with what you describe, the watcher firing once at mount and never again. Also, it is not possible to deep-watch a HTML element.
It is fascinating to me how different template refs and data felt in Vue 2, when apparently the mechanism on script-side was always the same, which is now made obvious in Vue 3 composition API. But they still do different things, where one gives access to a HTML element or a Vue component instance, and the other stores a value. I guess the big difference comes from using a ref in the ref attribute, not from the way it is declared.
Anyway, if you want to bind to the value of an input, use v-model (or v-bind:modelValue and #update:modelValue), but if you need access to the node for whatever reason, use ref=. If you want to do both, you need to use both, it is not possible to use just the template ref.
I am not sure what you are trying to achieve. if you don't need to use v-model then you should use v-bind and emit the input event, like the below.
<template>
<input :value="refA" type="text" #input="refA = $event.target.value" /> <br />
</template>
This will trigger watch on every update to your input.
A template ref which you have used as part of your example cannot be used to watch the internal value of the component.See below comments thread regarding the same.

Vue 3 Mutate Composition API state from external script

Forewords
In Option API, I was able to directly mutate instance data properties without losing any of reactivity. As described in here.
If you ask why, well not everything is written in Vue and there're cases where external JS libraries have to change certain value inside Vue instance.
For example:
document.app = createApp({
components: {
MyComponent, //MyComponent is a Option API
}
})
//Somewhere else
<MyComponent ref="foo"/>
Then component state can be mutated as follow:
//Console:
document.app.$refs.foo.$data.message = "Hello world"
With the help of ref, regardless of component hiarchy, the state mutating process is kept simple as that.
Question
Now in Composition API, I want to achieve the same thing, with setup script if it's possible.
When I do console.log(document.app.$refs), I just only get undefined as returned result.
So let's say I have MyComponent:
<template>
{{message}}
<template>
<script setup>
const message = ref('Hello world');
</script>
How to mutate this child component state from external script? And via a ref preferably, if it's easier
Refs that are exposed from setup function are automatically unwrapped, so a ref can't be changed as a property on component instance.
In order for a ref to be exposed to the outside, this needs to be explicitly done:
setup(props, ctx) {
const message = ref('Hello world');
ctx.expose({ message });
return { message };
}
This is different in case of script setup because variables are exposed on a template but not component instance. As the documentation states:
An exception here is that components using are private by default: a parent component referencing a child component using won't be able to access anything unless the child component chooses to expose a public interface using the defineExpose macro
It should be:
<script setup>
...
const message = ref('Hello world');
defineExpose({ message });
</script>

is it correct global component communication in vue?

i make modal popup components myPopup.vue for global.
and import that in App.vue and main.js
i use this for global, define some object Vue.prototype
make about popup method in Vue.prototype
like, "show" or "hide", any other.
but i think this is maybe anti pattern..
i want to find more best practice.
in App.vue
<div id="app>
<my-popup-component></my-popup-conponent>
<content></content>
</div>
main.js
...
Vue.prototype.$bus = new Vue(); // global event bus
Vue.prototype.$popup = {
show(params) {
Vue.prototype.$bus.$emit('showPopup', params);
},
hide() {
Vue.prototype.$bus.$emit('hidePopup');
}
}
Vue.component('my-popup-component', { ... });
...
myPopup.vue
....
export default {
...
created() {
this.$bus.$on('showPopup', this.myShow);
this.$bus.$on('hidePopup', this.myHide);
}
...
need-popup-component.vue
methods: {
showPopup() {
this.$popup.show({
title: 'title',
content: 'content',
callback: this.okcallback
});
}
}
It seems to be works well, but i don't know is this correct.
Is there any other way?
I was very surprised while reading your solution, but if you feel it simple and working, why not?
I would do this:
Add a boolean property in the state (or any data needed for showing popup), reflecting the display of the popup
use mapState in App.vue to bring the reactive boolean in the component
use v-if or show in App.vue template, on the popup declaration
create a 'showPopup' mutation that take a boolean and update the state accordingly
call the mutation from anywhere, anytime I needed to show/hide the popup
That will follow the vue pattern. Anything in state, ui components reflect the state, mutations mutates the state.
Your solution works, ok, but it doesn't follow vue framework, for exemple vue debug tools will be useless in your case. I consider better to have the minimum of number of patterns in one app, for maintenance, giving it to other people and so on.
You somehow try to create global component, which you might want to consume in your different projects.
Here is how I think I would do this -
How do I reuse the modal dialog, instead of creating 3 separate dialogs
Make a separate modal component, let say - commonModal.vue.
Now in your commonModal.vue, accept single prop, let say data: {}.
Now in the html section of commonModal
<div class="modal">
<!-- Use your received data here which get received from parent -->
<your modal code />
</div>
Now import the commonModal to the consuming/parent component. Create data property in the parent component, let say - isVisible: false and a computed property for the data you want to show in modal let say modalContent.
Now use it like this
<main class="foo">
<commonModal v-show="isVisible" :data="data" />
<!-- Your further code -->
</main>
The above will help you re-use modal and you just need to send the data from parent component.
How do I know which modal dialog has been triggered?
Just verify isVisible property to check if modal is open or not. If isVisible = false then your modal is not visible and vice-versa
How my global dialog component will inform it's parent component about its current state
Now, You might think how will you close your modal and let the parent component know about it.
On click of button trigger closeModal for that
Create a method - closeModal and inside commonModal component and emit an event.
closeModal() {
this.$emit('close-modal')
}
Now this will emit a custom event which can be listen by the consuming component.
So in you parent component just use this custom event like following and close your modal
<main class="foo">
<commonModal v-show="isVisible" :data="data" #close- modal="isVisible = false"/>
<!-- Your further code -->
</main>

Component rendered before correct prop data is passed in from another component

I have a component called EditPost and that uses another component called PostForm. I am using vuex store to make an api call to retrieve the post object to be edited from the backend in the EditPost beforeCreate method and using a computed property to get the retrieved post from the store which I then pass as a prop to the PostForm component.
Since the object exists already, I want its data to be populated in the input fields of the PostForm. But the values of the object aren't there since the component is rendered before. How can I make sure the data is safely reached before the component gets rendered.
My EditPost component is basically like this:
<template>
<PostForm v-bind:key="fetchPost" />
</template>
<script>
beforeCreate() {
this.$store.dispatch('loadPost');
}
computed:
fetchPost() {
return this.$store.getters.getPost;
}
</script>
You can wait for the loadPost action to be completed in beforeCreated(), then the component won't be created before the API responds. But be aware that this is not the best practice since the user won't see anything before the API returns a response.
Example:
<template>
<PostForm v-bind:key="fetchPost" />
</template>
<script>
async beforeCreate() {
await this.$store.dispatch('loadPost');
}
computed:
fetchPost() {
return this.$store.getters.getPost;
}
</script>

Is it posible to delete `div` from template?

I have a component myHello:
<temlate>
<div>
<h2>Hello</h1>
<p>world</p>
</div>
</template>
And main component:
<h1>my hello:</h1>
<my-hello><my-hello>
After rendering shows this:
<h1>my hello:</h1>
<div>
<h2>Hello</h1>
<p>world</p>
</div>
How to delete <div> ?
With VueJS, every component must have only one root element. The upgrade guide talks about this. If it makes you feel better, you are not alone. For what it's worth the components section is a good read.
With the myriad of solutions to your problem, here is one.
component myHello:
<temlate>
<h2>Hello</h1>
</template>
component myWorld:
<temlate>
<p>world</p>
</template>
component main
<h1>my hello:</h1>
<my-hello><my-hello>
<my-world><my-world>
Vue gives you the tools to do so by creating templates or you can do it by having a parent div with two parent divs as children. Reset the data from the data function. Stick with convention (create templates). It's hard to get used to use Vue when you have a jQuery background. Vue is better
Ex.
data () {
message: 'My message'
}
When you click a button to display a new message. Clear the message or just set the message variable with a new value.
ex. this.message = 'New Message'
If you like to show another div. you should used the if - else statement and add a reactive variable. Ex. showDivOne
data () {
message: 'My message'
showDivOne: true,
showDivTwo: false
}
Add this reactive variables to the parent divs that corresponds to the div.
When clicking the button, you should have a function like...
methods: {
buttonClick () {
this.showDivOne = false
this.showDivTwo = true
}
}
I think you can use v-if directive to controll. Add a flag to controll status to show or hide