Keep track of child component values in parent without using emit? - javascript

I have a custom component called HobbyForm which is a simple form with two controls, a checkbox and an input, this component is being called from a parent component called Content, along with other similar 'form' components.
<template>
<form>
<div class="row align-items-center">
<div class="col-1">
<Checkbox id="isHobbyActive" :binary="true" v-model="isActive"/>
</div>
<div class="col-5">
<InputText id="hobby" placeholder="Hobby" type="text" autocomplete="off" v-model="hobby"/>
</div>
</div>
</form>
</template>
<script>
export default {
name: 'HobbyForm',
data() {
return {
hobby: {
isActive: false,
hobby: null
}
}
},
}
</script>
My Content component is something like:
<template>
<language-form></language-form>
<hobby-form v-for="(hobbie, index) in hobbies" :key="index" v-bind="hobbies[index]"></hobby-form>
<Button label="Add Hobby" #click="addHobby"></Button>
</template>
<script>
export default {
name: "Content",
components: {
LanguageForm,
HobbyForm
},
data() {
return {
language: '',
hobbies: [
{
isActive: false,
hobby: null
}
]
};
},
methods: {
addHobby() {
this.hobbies.push({
isActive: false,
hobby: null
});
}
},
};
</script>
The idea is to be able to add more instances of the HobbyForm component to add another hobby record to my hobby data property; but I don't know how to keep track of these values from my parent without using an emit in my child components, since I don't want to manually trigger the emit, I just want to have the data updated in my parent component.
How should I access my child component's data from my parent and add it to my array?

In the current form passing parent data into a child component via v-bind="hobbies[index]" makes no sense as the child component (HobbyForm) has no props so it does not receive any data from the parent...
To make it work:
Remove data() from the child HobbyForm
Instead declare a prop of type Object
Bind form items to the properties of that Object
Pass the object into each HobbyForm
<template>
<form>
<div class="row align-items-center">
<div class="col-1">
<Checkbox id="isHobbyActive" :binary="true" v-model="hobby.isActive"/>
</div>
<div class="col-5">
<InputText id="hobby" placeholder="Hobby" type="text" autocomplete="off" v-model="hobby.hobby"/>
</div>
</div>
</form>
</template>
<script>
export default {
name: 'HobbyForm',
props: {
hobby: {
type: Object,
required: true
}
}
}
</script>
Even tho props are designed to be one way only so child should not mutate prop value, this is something else as you do not mutate prop value, you are changing (via a v-model) the properties of the object passed via a prop (see the note at the bottom of One-Way Data Flow paragraph)
Also change the parent to:
<hobby-form v-for="(hobby, index) in hobbies" :key="index" v-bind:hobby="hobby"></hobby-form>
Demo:
const app = Vue.createApp({
data() {
return {
hobbies: [{
isActive: false,
hobby: null
}]
};
},
methods: {
addHobby() {
this.hobbies.push({
isActive: false,
hobby: null
});
}
},
})
app.component('hobby-form', {
props: {
hobby: {
type: Object,
required: true
}
},
template: `
<form>
<div class="row align-items-center">
<div class="col-1">
<input type="checkbox" id="isHobbyActive" v-model="hobby.isActive"/>
</div>
<div class="col-5">
<input type="text" id="hobby" placeholder="Hobby" autocomplete="off" v-model="hobby.hobby"/>
</div>
</div>
</form>
`
})
app.mount('#app')
<script src="https://unpkg.com/vue#3.1.5/dist/vue.global.js"></script>
<div id='app'>
<hobby-form v-for="(hobby, index) in hobbies" :key="index" v-bind:hobby="hobby"></hobby-form>
<button #click="addHobby">Add Hobby</button>
<hr/>
<pre> {{ hobbies }} </pre>
</div>

Related

v-model inside a component is not listening to events vuejs

I need to make a dynamic v-model using v-for. I have a child component that will be used inside the father component.
I'll put the full code and the warning received.
It is funny because I use the same method in other project (NUXT) and it is working just fine. And this project (vuejs) has the same code and it is not.
It is seems to me it is not listening to 'event' and because of that the v-model is not working well. The v-model value does not update according to the changes on input space.
This is the Child Component
<template>
<div class="row p-1 g-3 d-md-flex justify-content-md-end">
<div class="col-4 d-flex flex-row-reverse">
<label for="inputPassword6" class="col-form-label">{{ profileDataItem }}</label>
</div>
<div class="col-8">
<input
type="text"
class="form-control"
:value="value"
#input="$emit('input', $event.target.value)"
>
</div>
</div>
</template>
<script>
export default {
name: 'RegisterField',
props: {
profileDataItem: {
type: String,
required: true
},
value: {
required: true
},
}
}
</script>
And this is the father component
<template>
<div class="form-register p-3 m-auto">
<form class="login-form p-3 border m-auto">
<h4 class="text-center p-2">Register yourself</h4>
<RegisterField
v-for="(item, index) in profileDataItems"
:key="index"
:profileDataItem="profileItems[index]"
v-model="item.model"
>
</RegisterField>
</form>
</div>
</template>
<script>
import RegisterField from './RegisterField.vue'
import ButtonSubmit from '../ButtonSubmit.vue'
export default {
name: 'RegisterForm',
components: {
RegisterField,
ButtonSubmit
},
data () {
return {
profileItems: ['First Name', 'Last Name', 'Email', 'Password', 'Confirm Password'],
profileDataItems: [
{ model: "firstName" },
{ model: "lastName" },
{ model: "email" },
{ model: "password" },
{ model: "confirmPassword" },
],
payloadProfileData: [],
}
},
}
</script>
Warning on console
[Vue warn]: Missing required prop: "value"
at <RegisterField key=4 profileDataItem="Confirm Password" modelValue="confirmPassword" ... >
at <RegisterForm>
at <SigninPage onVnodeUnmounted=fn<onVnodeUnmounted> ref=Ref< Proxy {…} > >
at <RouterView>
at <App>
change value prop to modelValue your component's code should be this:
<template>
<div class="row p-1 g-3 d-md-flex justify-content-md-end">
<div class="col-4 d-flex flex-row-reverse">
<label for="inputPassword6" class="col-form-label">{{
profileDataItem
}}</label>
</div>
<div class="col-8">
<input
type="text"
class="form-control"
:value="modelValue"
#input="$emit('input', $event.target.value)"
/>
</div>
</div>
</template>
<script>
export default {
name: 'RegisterField',
props: {
profileDataItem: {
type: String,
required: true,
},
modelValue: {
required: true,
},
},
};
</script>

vue.js updated hook keeps on rerendering page without update in the state

I am trying to get data from an api, while using updated() to rerender when there is a change in the state that holds the fetch url but the updated hooks keeps rendering without any change in the state.
I use click events to monitor the state change but the update happens without state change
the template
<template>
<div class="overlay" ref="overlay">
</div>
<div class="home">
Hello People
<form #submit.prevent="handleSearch">
<input type="text" name="search" v-model="search" />
<button type="submit">Search</button>
</form>
<select name="" id="select" v-model="select" v-on:change="selectclass">
<option value="">Default</option>
<option value="Breaking+Bad">Breaking Bad</option>
<option value="Better+Call+Saul">Better call saul</option>
</select>
<div class="charlist">
<div class="gridlist" v-for="char in chars" :key="char.id">
<div>
<div class="subgrid">
<img class="img" :src="char.img" alt="alt">
<p> {{ char.name }} </p>
<router-link :to="{ name: `Details`, params: {id: char.char_id} }">
<button> more</button>
</router-link>
</div>
</div>
</div>
</div>
</div>
</template>
the script
<script>
export default {
name: 'Home',
components: {
},
data() {
return {
url: 'https://www.breakingbadapi.com/api/characters?limit=10&offset=10',
search: "",
select: "",
chars: []
}
},
methods: {
handleSearch() {
this.url = `https://www.breakingbadapi.com/api/characters?name=${this.search}`
},
selectclass() {
this.url = `https://www.breakingbadapi.com/api/characters?category=${this.select}`
},
getData() {
fetch(this.url)
.then(res => res.json())
.then(data => {
this.chars = data
console.log(this.chars)
})
}
},
mounted() {
this.getData()
},
updated() {
console.log(this.url)
this.getData()
}
}
</script>
What I think is happening is a loop - Component Inits -> Gets Data -> Updated is called, which again triggers getData again, then the cycle repeats itself.
From what I remember Vue will rerender as long as a variable value is set and it does not care if the old value and the new are the same.
To get around that you should just call getData inside the selectclass and handleSearch. That should prevent the loop from starting.
Look at this warning in the documentation:
https://vuejs.org/api/options-lifecycle.html#updated

How to pass the chebox value from a parent to parent to child and after set it into the vuex store VueJS/Vuex

Hi everyone i have a big trouble i need to modify the value of checkbox from parent to another parent to child , is not this difficulty i know but i'm at the start and after spent 2 days for this i can't try anything else , now i'll add all the code the child is named "UICheckbox" and i pass it to "ToDoListItem" and this last is passed to "ToDoList" , and every Todos is an in an array into "store.js"
UICheckbox
<template>
<div class="checkbox-container">
<input
type="checkbox"
:id="id"
:v-model="localTodo"
>
<label :for="id">
<p class="font-bold">{{ text }}</p>
</label>
</div>
</template>
<script>
export default {
props: [
"id",
"value",
"text"
],
data() {
return {
localTodo: this.value
}
},
watch: {
localTodo:{
handler(newVal, oldVal) {
this.$emit("change", newVal)
},
deep: true,
}
}
}
</script>
<style>
</style>
ToDoListItem
<template>
<div class="flex flex-row w-full my-2">
<UICheckbox
:v-model="localTodo.checked"
:id="todo.id"
:text="todo.text"
#change="localTodo.cheked = $event"
/>
<delete-button :todo="todo" />
</div>
</template>
ToDoList
<to-do-list-item
:todo="todo"
#click="changeChecked(todo)"
#change="todo.checked = $event"
/>

Vue.js slots - how to retrieve slot content in computed properties

I have a problem with vue.js slots. On one hand I need to display the slot code. On the other hand I need to use it in a textarea to send it to external source.
main.vue
<template>
<div class="main">
<my-code>
<template v-slot:css-code>
#custom-css {
width: 300px
height: 200px;
}
</template>
<template v-slot:html-code>
<ul id="custom-css">
<li> aaa </li>
<li> bbb </li>
<li> ccc </li>
</ul>
</template>
</my-code>
</div>
</template>
my-code.vue
<template>
<div class="my-code">
<!-- display the code -->
<component :is="'style'" :name="codeId"><slot name="css-code"></slot></component>
<slot name="html-code"></slot>
<!-- send the code -->
<form method="post" action="https://my-external-service.com/">
<textarea name="html">{{theHTML}}</textarea>
<textarea name="css">{{theCSS}}</textarea>
<input type="submit">
</form>
</div>
</template>
<script>
export default {
name: 'myCode',
props: {
codeId: String,
},
computed: {
theHTML() {
return this.$slots['html-code']; /* The problem is here, it returns vNodes. */
},
theCSS() {
return this.$slots['css-code'][0].text;
},
}
}
</script>
The issues is that vue doesn't turn the slot content. It's an array of <VNode> elements. Is there a way to use slots inside the textarea. Or a way to retrieve slot content in the theHTML() computed property.
NOTE: I use this component in vuePress.
You need to create a custom component or a custom function to render VNode to html directly. I think that will be the simplest solution.
vnode to html.vue
<script>
export default {
props: ["vnode"],
render(createElement) {
return createElement("template", [this.vnode]);
},
mounted() {
this.$emit(
"html",
[...this.$el.childNodes].map((n) => n.outerHTML).join("\n")
);
},
};
</script>
Then you can use it to your component
template>
<div class="my-code">
<!-- display the code -->
<component :is="'style'" :name="codeId"
><slot name="css-code"></slot
></component>
<slot name="html-code"></slot>
<!-- send the code -->
<Vnode :vnode="theHTML" #html="html = $event" />
<form method="post" action="https://my-external-service.com/">
<textarea name="html" v-model="html"></textarea>
<textarea name="css" v-model="theCSS"></textarea>
<input type="submit" />
</form>
</div>
</template>
<script>
import Vnode from "./vnode-to-html";
export default {
name: "myCode",
components: {
Vnode,
},
props: {
codeId: String,
},
data() {
return {
html: "", // add this property to get the plain HTML
};
},
computed: {
theHTML() {
return this.$slots[
"html-code"
]
},
theCSS() {
return this.$slots["css-code"][0].text;
},
},
};
</script>
this thread might help How to pass html template as props to Vue component

VueJS Components inside a loop act as one

UPDATE
Was able to make it work, but got one last problem. Updated code is here:
VueJs not working on first click or first event
-----------------------------------------------------------
I've been trying to find out a way for the components inside a loop to not act as one.
I have a loop (3 divs), and inside the loop, I have 2 textboxes. But whenever I enter a value in any of them, the value is populated to everyone.
Can anyone help me separate those components?
I'm trying to make the parent div (1st loop) dynamic. So the children components (2nd loop) should be acting separately with their own grandparent components (textbox).
Here's my code:
<div id="app">
<div v-for="(ctr, c) in 3" :key="c">
<button #click="input_add">1st</button>
<div>
<div v-for="(input, act) in inputs" :key="act.id">
<input type="text" v-model="input.name">
<input type="text" v-model="input.time">
<button #click="input_remove(act)">Delete</button>
<button #click="input_add">Add row</button>
</div>
</div>
{{ inputs }}
</div>
</div>
const app = new Vue({
el: "#app",
data: {
inputs: [],
counter: 0,
},
methods: {
input_add() {
this.inputs.push({
id: this.counter + 1,
day: null,
name: null,
time: null,
})
this.counter += 1
},
input_remove(index) {
this.inputs.splice(index,1)
this.counter -= 1
}
}
});
Result:
as I mentioned in the comment, you should create a component for the iterated item.
parent component:
<div v-for="(item, index) in array" :key="index">
<child :item="item" />
</div>
Now you sent the item as prop. Let's catch it in child.
child components:
<div>
<input type="text" v-model="input.name">
<input type="text" v-model="input.time">
<button #click="input_remove(act)">Delete</button>
<button #click="input_add">Add row</button>
</div>
{{ inputs }}
props: [item], // I am not sure you need it or not, BUT just showing how to do it.
data() {return { // your datas };},
methods: {
// your methods...
},
//and else...
Now each iterated item can control self only. I am hope it make sense now.
then build the buttons an input in child component. After that you can apply the events for just clicked item.
You should use Array of Objects. Here's a codesandbox. This way everytime you add a new object to the array, a new index is created with a new name and time ready to be filled in.
<template>
<div id="app">
<img width="25%" src="./assets/logo.png">
<div v-for="item in basic" :key="item.id">
<button #click="addRow">Add row</button>
<input type="text" v-model="item.name">
<input type="text" v-model="item.time">
{{ item.name }} - {{ item.time }}
</div>
</div>
</template>
<script>
export default {
name: "App",
data() {
return {
id: 1,
basic: [{ name: "", time: "" }]
};
},
methods: {
addRow() {
console.log("added");
this.id += 1;
this.basic.push({
name: "",
time: ""
});
}
}
};
</script>

Categories