I am trying to create a simple CRUD app with Vuejs 3.
I have a homepage with a form (as a child component) and a table with created items (as another child component). I submit data via the form to API/database and the table updates. So far so good.
Then, for the update phase, I would like to have a detail page for each item where I also would have the form (the same component reused). But the idea is that form fields would be pre-populated with data from API/Database.
The table on the homepage has a route-link to a detail page and I am passing the id of the item as params. The detail page makes request to API based on id, receives item data and passes them as props into the form component.
If I try to render data directly into template like this, it works fine:
<p v-if="submitType === 'update' && item.id">{{ item.id }}</p>
Now, form fields are bound by v-model to data (form.id for example). But when I try to repopulate it as below, I always get undefined values.
data() {
return {
form: {
id: this.submitType === 'update' ? this.item.id : 0,
}
}
},
I suspect that problem is that the parent call to API is asynchronous and the passing of props is delayed. Because when I pass as props some hardcoded value, it appears as a value in the form field with no problem. Also if the form is shown only when props are received (with the v-if directive), the data.form.id is still undefined.
So is there any way how to pre-populate bound form fields with received props and still have the form component reused for insert and update actions? The rest of the relevant code is below. Thank you very much in advance
// Detail Page
<template>
<Form :item="item" submit-type="update"></Form>
</template>
<script>
export default {
data() {
return {
item: {}
}
},
created() {
callAPI(id).then( response => this.item = response.data )
}
}
</script>
// Form Component
<template>
<p v-if="submitType === 'update' && item.id">{{ item.id }}</p>
<div v-if="submitType === 'insert' || (submitType === 'update' && item.id )">
<section>
<form #submit.prevent="onSubmit">
<div>
<label for="id">ID</label>
<input id="id" name="id" v-model="form.id" type="number" placeholder="ID">
</div>
<input type="submit" value="Save">
</form>
</section>
</div>
</template>
<script>
export default {
name: 'Form',
props: {
item: {
type: Object
},
submitType: {
type: String
}
},
data() {
return {
form: {
id: this.submitType === 'update' ? this.item.id : 0,
}
}
},
}
</script>
You can try with watchers, take a look at following snippet:
const app = Vue.createApp({
data() {
return {
item: {},
type: 'update'
}
},
methods: {
change() {
this.type === 'update' ? this.type = 'insert' : this.type = 'update'
}
},
created() {
fetch('https://jsonplaceholder.typicode.com/todos/1')
.then(response => response.json())
.then(json => this.item = json)
//callAPI(id).then( response => this.item = response.data )
}
})
app.component('myForm', {
template: `
<p v-if="submitType === 'update' && item.id">{{ item.id }}</p>
<div v-if="submitType === 'insert' || (submitType === 'update' && item.id )">
<section>
<form #submit.prevent="onSubmit">
<div>
<label for="id">ID</label>
<input id="id" name="id" v-model="form.id" type="number" placeholder="ID">
</div>
<input type="submit" value="Save">
</form>
</section>
</div>
`,
props: {
item: {
type: Object
},
submitType: {
type: String
}
},
data() {
return {
form: {}
}
},
methods: {
fillData() {
this.submitType === 'update' ? this.form = {...this.item} : this.form = {id: 0}
}
},
watch: {
item() {
this.fillData()
},
submitType() {
this.fillData()
}
},
})
app.mount('#demo')
<script src="https://unpkg.com/vue#3/dist/vue.global.prod.js"></script>
<div id="demo">
<button #click="change">switch type</button>
{{type}}
<my-form :item="item" :submit-type="type"></my-form>
</div>
Related
I have the following code snippet from my app component:
<template>
<div>
<h3>Basic</h3>
<div v-for="(field, index) in basics" :key="index">
<input v-model="basics.name" placeholder="Name" type="text">
<br>
<br>
<input v-model="basics.email" placeholder="Email" type="email">
<br>
<hr/>
<button #click.prevent="addField">Add</button>
<button #click.prevent="removeField(index)">Remove</button>
<br>
<button #click.prevent="back">Back</button>
<button #click.prevent="toNext">Next</button>
</div>
</div>
</template>
<script>
import { mapActions } from "vuex";
export default {
name: "Basics",
data() {
return {
basics: [{
name: "",
email: ""
}]
};
},
methods: {
...mapActions(["addBasicData"]),
addFied(){
this.basics.push({
name: "",
email: ""
});
},
removeField(index){
this.basics.splice(index, 1);
},
toNext() {
this.addBasicData(this.basics);
this.$router.push({ name: "Location" });
},
back() {
this.$router.back();
}
}
};
</script>
In the code above when I finish filling up the form and click next button the data is sent to the state and we are guided to another route named "Location".
When I click back button in the "Location" route I'm back to route named "Basic".
The issue here is when I'm brought back to the route named "Basic" the form fields are empty although they are binded with the data object.
How do I populate these input fields when I return back to same route ?
Here is the working replica of the app: codesandbox
<div v-for="(field, index) in basics" :key="index">
<input v-model="basic.name" placeholder="Name" type="text">
<input v-model="basic.email" placeholder="Email" type="email">
<button #click.prevent="removeField(index)">Remove</button>
</div>
<hr/>
<button #click.prevent="addField">Add</button>
<br>
<button #click.prevent="back">Back</button>
<button #click.prevent="toNext">Next</button>
methods: {
addField() {
this.$store.commit('addBasic',{name:"",email:""} )
},
removeField(index) {
this.$store.commit('removeField',index )
},
toNext() {
this.$router.push({ name: "Location" });
}
},
computed: {
basic:{
get() {
return this.$store.getters.getBasic;
}
}
}
store.js
// ...
state: {
basic:[{name:"Jonny",email:"jonny#mail.com"},
{name:"Bonny",email:"Bonny#mail.com"}]
}
mutations: {
addBasic(state,value) {
state.basic.push(value)
},
removeField(state,index ){
state.basic.splice(index,1);
}
}
Thats just one of two versions how you can do it.
Or you can map the mutatations and call them directly in the click event.
https://vuex.vuejs.org/guide/mutations.html
https://vuex.vuejs.org/guide/forms.html
The add field button makes only sense outside of the loop.
addBasicData you dont need it
This method somehow works:
mounted() {
// eslint-disable-next-line no-unused-vars
let fromState = this.$store.state.Basics.basics;
if (fromState) {
this.basics.name = fromState.name;
this.basics.email = fromState.email;
}
}
I will really appreciate if there are any other convenient method to achieve this.
Tried mapState but didn't work
I'm a newbie of Vue, and I'm trying to simply clear the data of input component once I've submitted, but it seems I'm missing something, because since it's parent data is cleared, I still see the filled value of the input component.
Here is a living example.
I've set to the input child component v-model="title" from it's parent wrapper. Once I submit the data to the parent, I call addItem and in the end, I supposed to clear the data model just by clear it this.title = '', but I probably do something wrong on how to bind data from parent to child.
And above the code, starting from the parent component:
<template>
<form #submit="addItem" class="todo-insert">
<input-item
icon="create"
name="title"
placeholder="Add a ToVue item..."
v-model="title"
/>
<button-item tone="confirm" class="todo-insert__action">
Aggiungi
</button-item>
</form>
</template>
<script>
import ButtonItem from '#vue/Form/ButtonItem/ButtonItem.vue'
import InputItem from '#vue/Form/InputItem/InputItem.vue'
import uuid from 'uuid'
export default {
name: 'TodoInsert',
components: {
ButtonItem,
InputItem
},
data () {
return {
title: ''
}
},
methods: {
addItem (e) {
e.preventDefault()
const todo = {
id: uuid.v4(),
isComplete: false,
title: this.title
}
this.$emit('add-todo', todo)
this.title = ''
}
}
}
</script>
<style lang="scss" scoped src="./TodoList.scss"></style>
This is the child input component:
<template lang="html">
<label class="input">
<div v-if="label" class="input__label text-sans text-sans--label">
{{ label }}
</div>
<div class="input__row">
<input
:autocomplete="autocomplete"
:class="[hasPlaceholderLabel, isDirty]"
:name="name"
:placeholder="placeholder"
class="input__field"
type="text"
v-on:input="updateValue($event.target.value)"
v-on:blur="updateValue($event.target.value)"
>
<div v-if="placeholderLabel" class="input__placeholder text-sans text-sans--placeholder">
{{ placeholderLabel }}
</div>
<div v-if="icon" class="input__icon-area">
<icon-item
:name="icon"
/>
</div>
</div>
</label>
</template>
<script>
import IconItem from '../../IconItem/IconItem.vue'
export default {
name: 'InputItem',
props: {
autocomplete: {
type: String,
default: 'off'
},
icon: String,
label: String,
name: {
type: String,
default: 'input-text'
},
placeholder: String,
placeholderLabel: String
},
computed: {
hasPlaceholderLabel () {
return this.placeholderLabel ? 'input__field--placeholder-label' : ''
},
isDirty () {
// return this.$attrs.value ? 'input__field--dirty' : ''
return 'input__field--dirty'
}
},
methods: {
updateValue: function (value) {
this.$emit('input', value)
}
},
components: {
IconItem
}
}
</script>
<style lang="scss" src="./InputItem.scss"></style>
What am I missing?
Your child component is bound unidirectionally. It means that it can change the value, but does not receive any update from the parent component. To receive updates, you need to receive the property value in your child:
props: {
value: String
}
Then, you need to pass the value received to the input :
<input
:value="value"
:autocomplete="autocomplete"
:class="[hasPlaceholderLabel, isDirty]"
:name="name"
:placeholder="placeholder"
class="input__field"
type="text"
v-on:input="updateValue($event.target.value)"
v-on:blur="updateValue($event.target.value)"
>
Now the input should update when the parent component changes the value
I'm stuck on a problem and was hoping that a Javascript Jedi could help point me in the right direction.
Scope of the problem:
I'm passing a Laravel collection to my Vue component. Inside the component, I'm iterating through the collection and submitting a form via axios. The form submits, the data is updated in the database, but __I'm not clear on how to show the updated value without a page refresh.__
Expected Outcome:
The updated data is reflected in the {{ collection.value }} inside the Vue template after form submission
What's going wrong:
The data is being updated in the database, but the {{ collection.value }} remains the same until page is reloaded.
Web.php:
Route::post('/updateQty', 'MyController#update');
MyController:
public function update(Request $request)
{
$product = Product::where('id', $request->productId)
->update([ 'qty' => $request->qty ]);
return response()->json($product);
}
public function index()
{
$collection = DB::table('products')->get();
return view('my-blade', [
'collections' => $collection,
]);
}
Structure of $collection as stored in the database:
'qty' => decimal(8,2),
'class' => varchar(255),
'description' => varchar(255),
'value' => decimal(8,2),
'productId' => int(11)
my-blade:
<my-component :collections="{{ $collections }}"></my-component>
MyComponent.vue:
<template>
<div class="container">
<div class="row">
<div class="col-lg-12">
<table class="table table-sm">
<div v-if="collections.length">
<tr v-for="collection in collections" v-bind:key="collection.id">
<td>
<form #submit="updateQty">
<input type="hidden" id="productId" :value="collection.productId" name="productId">
<select class="form-control" name="qty" id="qty" #change="updateQty">
<option :value="collection.qty">{{ collection.qty }}</option>
<option v-for="(x, index) in 200" v-bind:key="index" :value="index">{{ index }}</option>
</select>
</form>
</td>
<td>{{ collection.value }}</td>
</tr>
</div>
</table>
</div>
</div>
</div>
</template>
<script>
export default {
props: ['collections'],
data() {
return {
qty: '',
}
}
mounted() {
console.log('MyComponent.vue mounted successfully');
},
methods: {
updateQty(e) {
e.preventDefault();
let currentObj = this;
let url = '/updateQty';
axios.post(url, {
qty: qty.value,
})
.then(function (response) {
currentObj.value = (response.data);
let collections = response.data;
})
},
}
}
</script>
App.js
Vue.component('my-component', require('./components/MyComponent.vue'));
I'm sure it's something simple, but for the life of me I can't wrap my head around it. Thank you very much in advance!
You just need to change up your script a bit.
First, save the collections property to a data property, or Vue will scream when you try to update it. To do this, I would rename the incoming prop as something like collections_prop. Then save it to the collections data property.
Then change let collections = to this.collections = in your update response.
EDIT: I changed the .then function to ES6 syntax as you may have trouble accessing the this variable otherwise. No need for the currentObj stuff.
export default {
props: ['collections_prop'],
mounted() {
console.log('MyComponent.vue mounted successfully');
},
data() {
return {
collections: this.collections_prop;
}
},
methods: {
updateQty(e) {
e.preventDefault();
let url = '/updateQty';
// not sure where qty is coming from
// but you said that's all worked out
// on your end
axios.post(url, {
qty: qty.value,
})
.then(response => {
this.collections = response.data;
})
},
}
}
And finally, don't forget to update the prop in your view.
<my-component :collections_prop="{{ $collections }}"></my-component>
Or if you want to later specify prop type as JSON:
<my-component :collections_prop='#json($collections)'></my-component>
I select the currency in the parent component of Vue using bootstrap select:
<template>
...
<div class = "dropdown">
<button class="btn btn-secondary dropdown-toggle" type="button"> {{currency}} </ button>
<div class = "dropdown-menu">
<button class = "dropdown-item" # click = "currencyChange ('USD')"> USD </ button>
<button class = "dropdown-item" # click = "currencyChange ('EUR')"> EUR </ button>
</div>
</div>
...
<div class = "box">
<box v-bind: currency-name = 'currency' />
</div>
<template>
<script>
...
data () {
return {
currency: 'USD'
}
},
components: {
box: component
},
methods: {
currencyChange (currency) {
this.currency = currency;
}
}
...
</script>
In the child component "box" I get data from server via the axios.get call and render it:
<script>
...
props: ['currencyName'],
data () {
return {
boxData: {},
}
},
created () {
axios.get (URL + this.currencyName)
.then (response => {
this.Data = response.data;
})
.catch (e => {
this.errors.push (e)
})
},
...
</script>
The problem is that if the EUR currency is selected, I never send new query to the server and data in the box component remains the same, for the 'USD' currency except currencyChange variable. If rename "created" hook to "updated" in component, everything starts to work as it should, with one exception - there are constant calls to the server.
How can I fix this wrong behaviour into a single call to the server only after dropdown click?
You can use the watch property to achieve this.
<script>
...
props: ['currencyName'],
data () {
return {
currentCurrency: this.currencyName,
boxData: {},
}
},
watch: {
currentCurrency() {
this.getData();
}
},
methods: {
getData() {
axios.get (URL + this.currentCurrency)
.then (response => {
this.Data = response.data;
})
.catch (e => {
this.errors.push (e)
})
},
}
...
</script>
Take a look at https://v2.vuejs.org/v2/guide/computed.html#Watchers
EDIT
As D F suggest you can add immediate: true, to your watcher to trigger it at the component initialization
Just completed a todolist tutorial.
When submitting the form the input field doesn't clear.
After trying both:
document.getElementById("todo-field").reset();
document.getElementById("#todo-field").value = "";
The input field properly clears but it also deletes the todo.
It seems to delete the input field before it has time to push the new todo in the todos.text array.
Would love some input guys! Thanks!!
<template>
<form id="todo-field" v-on:submit="submitForm">
<input type="text" v-model="text">
</form>
<ul>
<li v-for="todo in todos">
<input class="toggle" type="checkbox" v-model="todo.completed">
<span :class="{completed: todo.completed}" class="col-md-6">
<label #dblclick="deleteTodo(todo)">
{{todo.text}}
</label>
</span>
</li>
</ul>
<script>
export default {
name: 'todos',
data () {
return {
text: '',
todos: [
{
text:'My Todo One',
completed: false
},
{
text:'My Todo Two',
completed: false
},
{
text:'My Todo Three',
completed: false
}
]// End of array
}
},
methods: {
deleteTodo(todo){
this.todos.splice(this.todos.indexOf(todo),1);
},
submitForm(e){
this.todos.push(
{
text: this.text,
completed: false
}
);
//document.getElementById("todo-field").reset();
document.getElementById("#todo-field").value = "";
// To prevent the form from submitting
e.preventDefault();
}
}
}
</script>
These solutions are good but if you want to go for less work then you can use $refs
<form ref="anyName" #submit="submitForm">
</form>
<script>
methods: {
submitForm(){
// Your form submission
this.$refs.anyName.reset(); // This will clear that form
}
}
</script>
What you need is to set this.text to an empty string in your submitForm function:
submitForm(e){
this.todos.push(
{
text: this.text,
completed: false
}
);
this.text = "";
// To prevent the form from submitting
e.preventDefault();
}
Remember that binding works both ways: The (input) view can update the (string) model, or the model can update the view.
Assuming that you have a form that is huge or simply you do not want to reset each form field one by one, you can reset all the fields of the form by iterating through the fields one by one
var self = this;
Object.keys(this.data.form).forEach(function(key,index) {
self.data.form[key] = '';
});
The above will reset all fields of the given this.data.form object to empty string. Let's say there are one or two fields that you selectively want to set to a specific value in that case inside the above block you can easily put a condition based on field name
if(key === "country")
self.data.form[key] = 'Canada';
else
self.data.form[key] = '';
Or if you want to reset the field based on type and you have boolean and other field types in that case
if(typeof self.data.form[key] === "string")
self.data.form[key] = '';
else if (typeof self.data.form[key] === "boolean")
self.data.form[key] = false;
For more type info see here
A basic vuejs template and script sample would look as follow
<template>
<div>
<form #submit.prevent="onSubmit">
<input type="text" class="input" placeholder="User first name" v-model="data.form.firstName">
<input type="text" class="input" placeholder="User last name" v-model="data.form.lastName">
<input type="text" class="input" placeholder="User phone" v-model="data.form.phone">
<input type="submit" class="button is-info" value="Add">
<input type="button" class="button is-warning" #click="resetForm()" value="Reset Form">
</form>
</div>
</template>
See ow the #submit.prevent="onSubmit" is used in the form element. That would by default, prevent the form submission and call the onSubmit function.
Let's assume we have the following for the above
<script>
export default {
data() {
return {
data: {
form: {
firstName: '',
lastName: '',
phone: ''
}
}
}
},
methods: {
onSubmit: function() {
console.log('Make API request.')
this.resetForm(); //clear form automatically after successful request
},
resetForm() {
console.log('Reseting the form')
var self = this; //you need this because *this* will refer to Object.keys below`
//Iterate through each object field, key is name of the object field`
Object.keys(this.data.form).forEach(function(key,index) {
self.data.form[key] = '';
});
}
}
}
</script>
You can call the resetForm from anywhere and it will reset your form fields.
For reset all field in one form you can use event.target.reset()
const app = new Vue({
el: '#app',
data(){
return{
name : null,
lastname : null,
address : null
}
},
methods: {
submitForm : function(event){
event.preventDefault(),
//process...
event.target.reset()
}
}
});
form input[type=text]{border-radius:5px; padding:6px; border:1px solid #ddd}
form input[type=submit]{border-radius:5px; padding:8px; background:#060; color:#fff; cursor:pointer; border:none}
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.6/vue.js"></script>
<div id="app">
<form id="todo-field" v-on:submit="submitForm">
<input type="text" v-model="name"><br><br>
<input type="text" v-model="lastname"><br><br>
<input type="text" v-model="address"><br><br>
<input type="submit" value="Send"><br>
</form>
</div>
Markup
<template lang="pug">
form
input.input(type='text' v-model='formData.firstName')
input.input(type='text' v-model='formData.lastName')
button(#click='resetForm') Reset Form
</template>
Script
<script>
const initFromData = { firstName: '', lastName: '' };
export default {
data() {
return {
formData: Object.assign({}, initFromData),
};
},
methods: {
resetForm() {
// if shallow copy
this.formData = Object.assign({}, initFromData);
// if deep copy
// this.formData = JSON.parse(JSON.stringify(this.initFromData));
},
},
};
</script>
Read the difference between a deep copy and a shallow copy HERE.
I use this
this.$refs['refFormName'].resetFields();
this work fine for me.
This solution is only for components
If we toggle(show/hide) components using booleans then data is also removed.
No need to clean the form fields.
I usually make components and initialize them using booleans.
e.g.
<template>
<button #click="show_create_form = true">Add New Record</button
<create-form v-if="show_create_form" />
</template>
<script>
...
data(){
return{
show_create_form:false //making it false by default
}
},
methods:{
submitForm(){
//...
this.axios.post('/submit-form-url',data,config)
.then((response) => {
this.show_create_form= false; //hide it again after success.
//if you now click on add new record button then it will show you empty form
}).catch((error) => {
//
})
}
}
...
</script>
When use clicks on edit button then this boolean becomes true and after successful submit I change it to false again.
I had a situation where i was working with a custom component and i needed to clear the form data.
But only if the page was in 'create' form state, and if the page was not being used to edit an existing item. So I made a method.
I called this method inside a watcher on custom component file, and not the vue page that uses the custom component. If that makes sense.
The entire form $ref was only available to me on the Base Custom Component.
<!-- Custom component HTML -->
<template>
<v-form ref="form" v-model="valid" #submit.prevent>
<slot v-bind="{ formItem, formState, valid }"></slot>
</v-form>
</template>
watch: {
value() {
// Some other code here
this.clearFormDataIfNotEdit(this)
// Some other code here too
}
}
... some other stuff ....
methods: {
clearFormDataIfNotEdit(objct) {
if (objct.formstate === 'create' && objct.formItem.id === undefined) {
objct.$refs.form.reset()
}
},
}
Basically i checked to see if the form data had an ID, if it did not, and the state was on create, then call the obj.$ref.form.reset() if i did this directly in the watcher, then it would be this.$ref.form.reset() obvs.
But you can only call the $ref from the page which it's referenced.
Which is what i wanted to call out with this answer.
This is how I do it in Vue 3.
html:
<input type="text" v-model="state.name">
js:
import {reactive} from "vue";
const state = reactive({
name: ""
})
axios.post('/contact', state)
.then(res => {
if (res.status == 200) {
state.name = ""
}
})
Response status 200 being a successful submission of the form input. state.name is reactive and will be set to "" if the submission is successful.
if your using vue.js v-form you can simply do like
this.form.reset()
Documentation
Vform - Documentation