Good practice for re-usable form component in Vue - vue.js

I'm trying to write a re-usable form component for a "user". I'd like to be able to use it in both an "edit" and a "create" flow. I'd pass it a user object, it would validate as a user enters/modifies the user data and it would update the user object. The parent component (e.g EditUser, CreateUser) will do the actual saving.
How should I do this without mutating props? I've avoided using events so far because I'd need to fire one on every user input, passing a copy of the user object back up to the parent (I think).
EDIT: adding some (non-working) code to demonstrate. I can't share my exact code.
Parent component
<template>
<div >
<h1>header</h1>
<MyFormComponent
v-model="user"
>
</MyFormComponent>
<button>Save</button>
</div>
</template>
<script>
export default {
data(){
return {
user: {
name: 'User 1'
}
}
}
}
</script>
Form component
<template>
<form>
<MyFormInputComponent
v-model="user.name"
></MyFormInputComponent>
</form>
</template>
<script>
export default {
props: ['user'],
model: {
prop: 'user'
}
}
</script>
Thanks!

I don't know exactly your context, but this is how I use to do:
First, you don't need both components Parent and Child. You can do all you want inside Form Component.
To deal with the differences between create and edit modes, an option is computed property based on current route (if they are different according to create/edit operations).
Using this property, you decide if data will be fetched from API, if delete button will has shown, the title of the page and so on.
Here is an example:
async created() {
if (this.isEditMode) {
// fetch form data from API according to User ID and copy to a local form
},
},
computed: {
formTitle() {
return (this.isEditMode ? 'Update' : 'Create') + ' User';
},
}

Related

Parent component updates a child component v-for list, the new list is not rendered in the viewport (vue.js)

My app structure is as follows. The Parent app has an editable form, with a child component list placed at the side. The child component is a list of students in a table.
I'm trying to update a child component list. The child component uses a 'v-for', the list is generated through a web service call using Axios.
In my parent component, I am editing a students name, but the students new name is not reflected in the List that I have on screen.
Example:
Notice on the left the parent form has the updated name now stored in the DB. However, the list (child component) remains unchanged.
I have tried a few things such as using props, ref etc. I am starting to think that my app architecture may be incorrect.
Does anyone know how I might go about solving this issue.
Sections of the code below. You may understand that I am a novice at Vue.
Assistance much appreciated.
// Child component
<component>
..
<tr v-for="student in Students.slice().reverse()" :key="student._id">
..
</component>
export default {
env: '',
// list: this.Students,
props: {
inputData: Boolean,
},
data() {
return {
Students: [],
};
},
created() {
// AXIOS web call...
},
};
// Parent component
import List from "./components/students/listTerms";
export default {
name: "App",
components: {
Header,
Footer,
List,
},
};
// Implementation
<List />
I think that it is better to use vuex for this case and make changes with mutations. Because when you change an object in the data array, it is not overwritten. reactivity doesn't work that way read more about it here
If your list component doesn't make a fresh API call each time the form is submitted, the data won't reflect the changes. However, making a separate request each time doesn't make much sense when the component is a child of the form component.
To utilise Vue's reactivity and prevent overhead, it would be best to use props.
As a simplified example:
// Child component
<template>
...
<tr v-for="student in [...students].reverse()" :key="student._id">
...
</template>
<script>
export default {
props: {
students: Array,
},
};
</script>
// Parent component
<template>
<div>
<form #submit.prevent="submitForm">
<input v-model="studentData.name" />
<input type="submit" value="SUBMIT" />
</form>
<List :students="students" />
</div>
</template>
<script>
import List from "./components/students/listTerms";
export default {
name: "App",
components: {
List,
},
data() {
return {
students: [],
studentData: {
name: ''
}
}
},
methods: {
submitForm() {
this.$axios.post('/endpoint', this.studentData).then(() => {
this.students.push({ ...this.studentData });
}).catch(err => {
console.error(err)
})
}
}
};
</script>
Working example.
This ensures data that isn't stored successfully won't be displayed and data that is stored successfully reflects in the child component.

VueJS: Is it really bad to mutate a prop directly even if I want it to ovewrite its value everytime it re-renders?

The question says it all. As an example, think of a component that can send messages, but depending on where you call this component, you can send a predefined message or edit it. So you would end with something like this:
export default {
props: {
message: {
type: String,
default: ''
}
},
methods: {
send() { insert some nice sending logic here }
}
}
<template>
<div>
<input v-model="message"></input>
<button #click="send">Send</button>
</div>
</template>
If I do this and try to edit the predefined message then Vue warns me to "Avoid mutating a prop directly since the value will be overwritten whenever the parent component re-renders", but that's exactly the behaviour I'm searching for as the predefined message should return to being unedited if the user closes the component and opens it again.
I'm also not passing the prop to the father component, so the sending logic itself can be included in this same component.
It would still be considered bad practice? Why? How can I make it better? Thanks in advance!
A solution would be to assign the message you are passing as a prop to a variable in data and set this variable to the v-model instead.
<template>
<div>
<input v-model="message"></input>
<button #click="send">Send</button>
</div>
</template>
<script>
export default {
data(){
return{ message:this.msg
}
},
props: {
msg: {
type: String,
default: ''
}
},
methods: {
send() { use a bus to send yout message to other component }
}
}
</script>
If you are not passing the data to another component or from a component, you shouldn't be using props, you should use Vue's data object and data binding. This is for any component data that stays within itself, the component's local state. This can be mutated by you as well so for our example I would do something like:
export default {
data: function () {
return {
message: '',
}
},
methods: {
send() {
// insert some nice sending logic here
// when done reset the data field
this.data.message = '';
}
}
}
<template>
<div>
<input>{{ message }}</input>
<button #click="send">Send</button>
</div>
</template>
More info on props vs data with Vue

Detect browser close or page change VueJs

I try to detect when user change/insert into an input and he try to change/close page to give him a warning. I do some research but till now I didn't find anything.
<b-form-group label="Name" label-for="name-input">
<b-form-input
id="name-input"
v-model="name"
></b-form-input>
</b-form-group>
created() {
document.addEventListener('beforeunload', this.handlerClose)
},
handlerClose: function handler(event) {
console.log('CHANGE!!!!');
},
Detect navigating to a different page or close the page
You can try using the same eventhandler beforeunload on the window object, not the document object, as stated in the MDN Web Docs for example ( https://developer.mozilla.org/en-US/docs/Web/API/Window/beforeunload_event ). The event should handle both cases, switching page and closing page.
<script>
export default {
created() {
window.addEventListener('beforeunload', (event) => {
// Cancel the event as stated by the standard.
event.preventDefault();
// Chrome requires returnValue to be set.
event.returnValue = '';
});
}
}
</script>
This event enables a web page to trigger a confirmation dialog asking the user if they really want to leave the page. If the user confirms, the browser navigates to the new page, otherwise it cancels the navigation.
About your second question to detect whetever changes has been made : This eventhandler does not detect changes.
In order to mantain a state whetever the user made a change, e.g. to a form, I would outsource this state with a data prop isChanged and initialize it with false. Then use Vue directives v-on:change or v-on:input to change the prop from false to true.
<template>
<div>
<input type="text" #change="userMadeChange" v-model="inputText" />
</div>
</template>
<script>
export default {
data() {
return {
inputText : "",
isChanged : false
}
},
methods : {
userMadeChange() {
this.isChanged = true;
}
}
}
</script>
The easier way is to simply compare the stringified JSON of your selected data. If they are equivalent, then we know that the data has not been changed/updated/mutated by the user.
Here's a simple setup:
Create a method that generates the JSON for the user data that you want to observe for changes.
When the compoonent/app is created, you cache the data that it is created with and store/cache it
Create a computed property that simply returns the current state of the user data and cached user data
In the beforeunload handler, you can then check the returned value of this computed property to determine of the user has mutated data or not.
See proof-of-concept below:
new Vue({
el: '#app',
// COMPONENT DATA
data: {
// Dummy data
firstName: 'John',
lastName: 'Doe',
// Cache form data
cachedFormData: null,
},
// COMPONENT LIFECYCLE HOOK
created: function() {
// Create a cache when component/app is created
this.cachedFormData = this.formDataForComparison();
document.addEventListener('beforeunload', this.handlerClose);
},
// COMPUTED PROPERTIES
computed: {
// Compares cached user data to live data
hasChanged() {
return this.cachedFormData !== this.formDataForComparison();
}
},
// COMPONENT METHODS
methods: {
// Callback handler
handlerClose: function() {
if (this.hasChanged) {
// Logic when change is detected
// e.g. you can show a confirm() dialog to ask if user wants to proceed
} else {
// Logic when no change is detected
}
},
// Helper method that generates JSON for string comparison
formDataForComparison: function() {
return JSON.stringify({
firstName: this.firstName,
lastName: this.lastName
});
}
}
});
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<div id="app">
<input type="text" v-model="firstName" />
<input type="text" v-model="lastName" />
<br />
<br />
<span>Has user changed data? <strong>{{ hasChanged }}</strong></span>
</div>
An alternative method would be simply storing a flag that has a ground state of false, as proposed by the other answer. The flag state is switched to true wheneveran input/change event is detected on the element. However, there are several disadvantages associated with this method:
Even when the user undo his/her changes, it will still marked as changed. This constitutes a false positive.
You will need to either bind watchers to all the v-model members, or bind input/change event listeners to all input elements on the page. If your form is huge, there is a chance that you will forget to do this to an input element.

How do I create new instances of a Vue component, and subsuqently destroy them, with methods?

I'm trying to add components to the DOM dynamically on user input. I effectively have a situation with ±200 buttons/triggers which, when clicked, need to create/show an instance of childComponent (which is a sort of infowindow/modal).
I would also then need to be able to remove/hide them later when the user 'closes' the component.
I'm imagining something like this?
<template>
<div ref="container">
<button #click="createComponent(1)" />
...
<button #click="createComponent(n)" />
<childComponent ref="cc53" :num="53" v-on:kill="destroyComponent" />
...
<childComponent ref="ccn" :num="n" v-on:kill="destroyComponent"/>
</div>
</template>
<script>
import childComponent from '#/components/ChildComponent'
export default {
components: {childComponent},
methods: {
createComponent (num) {
// How do I create an instance of childComponent with prop 'num' and add it to this.$refs.container?
},
destroyComponent (vRef) {
// How do I destroy an instance of childComponent?
this.vRef.$destroy();
}
}
}
</script>
The number of possible childComponent instances required is finite, immutable and known before render, so I could loop and v-show them, but your typical user will probably only need to look at a few, and certainly only a few simultaneously.
My questions:
Firstly, given there are ±200 of them, is there any performance benefit to only creating instances dynamically as and when required, vs. v-for looping childComponents and let Vue manage the DOM?
Secondly, even if v-for is the way to go for this particular case, how would one handle this if the total number of possible childComponents is not known or dynamic? Is this a job for Render Functions and JSX?
If I understand, you want to display a list of the same component that take :num as a prop.
First, you have to keep in mind that Vue is a "Data driven application", wich means that you need to represent your list as Data in an array or an object, in your case you can use a myList array and v-for loop to display your child components list in the template.
The add and remove operations must be donne on the myList array it self, once done, it will be automatically applied on your template.
To add a new instance just use myList.push(n)
To remove an instance use myLsit.splice(myLsit.indexOf(n), 1);
The result should look like this :
<template>
<input v-model="inputId" />
<button #click="addItem(inputId)">Add Item</button>
<childComponent
v-for="itemId in myList"
:key="itemId"
:ref="'cc' + itemId"
:num="itemId"
#kill="removeItem(itemId)"
/>
</template>
<script>
data(){
return{
inputId : 0,
myList : []
}
},
methods:{
addItem(id){
this.myList.push(id)
},
removeItem(id){
this.myLsit.splice(this.myLsit.indexOf(id), 1)
}
}
</script>
Ps :
Didn't test the code, if there is any error just tell me
#kill method must be emitted by the childComponent, $emit('kill', this.num)
Here is an excellent tutorial to better understand v-for
Performance Penalties
As there is only a limited possibility of ±200 elements, I highly doubt that it can cause any performance issue, and for further fine-tuning, instead of using v-show, you can use v-if it'll reduce the total memory footprint, but increases the render time if you're going to change the items constantly.
Other Approaches
If there weren't limited possibilities of x elements, it'd be still and v-for having items which contain the v-if directive.
But if the user could only see one item (or multiple but limited items) at the same time, instead of v-for, It'd much better to directly bind the properties to the childComponent.
For example, if the child component is a modal that'll be shown by the application when a user clicked on the edit button for a row of a table. Instead of having x number of modals, each having editable contents of a row and showing the modal related to the edit button, we can have one modal and bind form properties to it. This approach usually implemented by having a state management library like vuex.
Finally, This is an implementation based on vuex, that can be used, if the user could only see one childComponent at the same time, it can be easily extended to support multiple childComponent viewed at the same time.
store.js
export Store {
state: {
childComponentVisible: false,
childComponentNumber: 0
},
mutations: {
setChildComponentNumber(state, value) {
if(typeof value !== 'number')
return false;
state.childComponentNumber = value;
},
setChildComponentVisibility(state, value) {
if(typeof value !== 'boolean')
return false;
state.childComponentVisible = value;
}
}
}
child-component.vue
<template>
<p>
{{ componentNumber }}
<span #click="close()">Close</span>
</p>
</template>
<script>
export default {
methods: {
close() {
this.$store.commit('setChildComponentVisibility', false);
}
}
computed: {
componentNumber() {
return this.$store.state.childComponentNumber;
}
}
}
</script>
list-component.vue
<template>
<div class="list-component">
<button v-for="n in [1,2,3,4,5]" #click="triggerChildComponent(n)">
{{ n }}
</button>
<childComponent v-if="childComponentVisible"/>
</div>
</template>
<script>
export default {
methods: {
triggerChildComponent(n) {
this.$store.commit('setChildComponentNumber', n);
this.$store.commit('setChildComponentVisibility', true);
}
},
computed: {
childComponentVisible() {
return this.$store.state.childComponentVisible;
}
}
}
</script>
Note: The code written above is abstract only and isn't tested, you might need to change it a little bit to make it work for your own situation.
For more information on vuex check out its documentation here.

How to defer passing of Ajax data to child components?

I'm building a simple to-do app with Vue 2.0 using built-in parent-child communication. The parent element that is directly attached to the Vue instance (new Vue({...}) is as follows:
<!-- Tasks.vue -->
<template>
<div>
<create-task-form></create-task-form>
<task-list :tasks="tasks"></task-list>
<task-list :tasks="incompleteTasks"></task-list>
<task-list :tasks="completeTasks"></task-list>
</template>
<script>
import CreateTaskForm from './CreateTaskForm.vue';
import TaskList from './TaskList.vue';
export default {
components: { CreateTaskForm, TaskList },
data() { return { tasks: [] }; },
created() { // Ajax call happens here...
axios.get('/api/v1/tasks')
.then(response => {
this.tasks = response.data;
console.log(this.tasks); // THIS IS LOGGED LAST
});
},
computed: {
completeTasks() {
return this.tasks.filter(task => task.complete);
},
incompleteTasks() {
return this.tasks.filter(task => task.complete);
}
}
}
</script>
The idea is that <tasks></tasks> will display a form to create a new task, as well 3 lists - all tasks, incomplete, and complete tasks. Each list is the same component:
<!-- TaskList.vue -->
<template>
<ul>
<li v-for="task in taskList">
<input type="checkbox" v-model="task.complete"> {{ task.name }}
</li>
</ul>
</template>
<script>
export default {
data() { return { taskList: [] }; },
props: ['tasks'],
mounted() {
this.taskList = this.tasks;
console.log(this.tasks); // THIS IS LOGGED FIRST
}
}
</script>
Problem
As you can see, I am trying to pass data from <tasks> to each of the 3 <task-lists>, using dynamic :tasks property:
<task-list :tasks="tasks"></task-list>
<task-list :tasks="incompleteTasks"></task-list>
<task-list :tasks="completeTasks"></task-list>
Note, I am not using shared (global) state, because each list needs a different portion of data, even though these portions of data belong to the same store. But the problem is that :tasks are assigned an empty array before the Ajax call happens; and as I am guessing, props are immutable, hence tasks in the child <task-list> are never updated when data is fetched in the parent <tasks>. In fact, <task-list> is created first (see the log), and only then the data is fetched using Ajax.
Questions
How do I defer the passing of data from <tasks> to my <task-list>s? How do I make sure that all components refer to the single source of truth that's updated dynamically?
Is there a way to solve this parent-child communication problem with "vanilla" Vue.js? Or do I need to use Vuex or something similar?
Am I right in using properties to pass data to children? Or should I use shared store in a global variable?
The problem is here: mounted() { this.taskList = this.tasks; ...} inside TaskList.vue as taskList property is updated only on mounted event.
There's a trivial solution in Vue: you should make taskList a computed property which depends on props, so that when parent data changes your computed property gets updated:
props: ['tasks'],
computed: {
taskList: function() {
return this.tasks;
}
}
Don't forget to remove taskList from data block.
Also, I would rewrite v-model="task.complete" into #change="$emit('something', task.id)" to inform a parent component that status has changed (and listen to ). Otherwise parent will never know the box is checked. You can then listen for this event on parent component to change tasks status accordingly. More reading: https://v2.vuejs.org/v2/guide/components.html#Custom-Events
You can also use watch property of vue instance
watch:{
tasks(newVal){
this.taskList = newVal;
//do something
}
}