DRY Vue: How to Reuse and Prepopulate Forms

Good code is DRY, good UX is simple. Forms appear all over but how can we reuse them in Vue?

Photo by Luca Zanon on Unsplash

A project I’m currently working on uses forms quite liberally. There are different forms for each element type that a user can create (4) and another one when editing these same elements.

As I was designing these views I immediately knew that both creation and editing should use the exact same form. From a UX stand point it makes sense, your user has just used a form to create an element, editing it shouldn’t require learning a new form.

That’s where the problem begins. In Adobe XD, reusing a form is a simple as copy-pasting it, but in Vue how could I reuse the same component? More importantly, how could I prepopulate a form with the data of the element to edit?

Intense web searches didn’t help me find an answer so here I am sharing what worked for me in the hopes that it can help others facing the same problem.

What we will be making, notice the first todo being edited

Setup

We will setup a really basic todo list app to give us a form that we can then reuse and prepopulate. Yes, I know, another todo list app but stay with me, the todo part only serves as container for the really juicy code.

I’ve set up a basic Todo List with Vue if you want to follow along, clone it with this command:

$ git clone -b basicTodoList --single-branch https://github.com/MaxMonteil/VueTodo.git

Otherwise get the whole project from the formReuse branch:

$ git clone -b formReuse --single-branch https://github.com/MaxMonteil/VueTodo.git

Once cloned, you can run it locally with

$ npm install
$ npm run serve

Displaying the Form

The first thing we’ll want to do is create an entry point for our creation form to appear.

Depending on your situation, there are quite a few options of where you’d want your form to reappear. For example, in a more involved application with a more complex form, a modal might make a lot more sense.

If that’s the case for you check out portal-vue, it isn’t absolutely necessary but it might help simplify your code.

In our case, we are going to have the form appear inside the ListItem component directly, thus replacing the current content.

To toggle whether we show the form or the content, we will add an editing field to our component’s data.

ListItem.vue
  <template>
<li class="todo-item" :class="{ completed: isCompleted }">
+ <div class="todo-content" v-if="!editing">
<p>{{ todo.priority }} | {{ todo.title }}</p>
<span>
...
+ <button @click="editTodo" v-if="!editing">
</span>
+ </div>
+     <todo-form v-else/>
</li>
</template>
  <script>
+ import TodoForm from './TodoForm.vue'

export default {
name: 'ListItem',
components: {
+ TodoForm
},
data () {
return {
+ editing: false
}
},
methods: {
+ editTodo () {
+ this.editing = !this.editing
+ }
}
}
</script>

Note that we will also need to move a few CSS styles around.

Toggling the Form

We have the editing field in our ListItem component but that is only one half of the equation because we still can’t close it. To close the form we will need something like a cancel button but that belongs in the form component itself, not the list item.

To do this, our form needs to know whether it is being displayed to edit or to create a todo.

We will use our very important populateWith prop which defaults to an object with an ‘empty’ property.

You could have it be a literal empty object but I feel it helps to explicitly state what we consider ‘empty’, it also massively simplifies the whole process of checking whether said object is empty or not.

Note that we can’t have an object as the default value, it must instead be returned by a function.

TodoForm.vue
  <template>
<form @submit.prevent="submit" id="todo-form">
...
+ <button @click="close" v-if="!populateWith.empty">X</button
</form>
</template>
  <script>
export default {
name: 'TodoForm',
+ props: {
+ populateWith: {
+ type: Object,
+ default: () => ({ empty: true })
+ }
+ },
+ methods: {
+ close () {
+ this.$emit('close')
+ }
}
}
</script>

ListItem.vue
      <todo-form
v-else
+ @close="editTodo"/>

Now, if the object is empty, that means the form is being used to create a todo, otherwise it is being used to edit one and we can show our cancel button.
This button will then emit to our ListItem thus toggling our editing property and hiding the form.

Populating the Form

Next we need to supply our form with the data it needs to populate itself.
From our ListItem component, all we need to do is pass the todo object through the populateWith prop to our form.
This works out well for us as the empty property will now be undefined which is falsy.

ListItem.vue
      <todo-form
v-else
+ :populateWith="todo"
@close="editTodo"/>

To then ensure our form populates itself at the right moment, we will hook into the component’s lifecycle with the created() method and overwrite the component’s data.

TodoForm.vue
  <script>
export default {
+ created () {
+ if (!this.populateWith.empty) {
+ this.todo = this.populateWith
+ }
+ }
}
</script>

Here our object is very simple and an assignment statement is all you need. With more complex objects, especially with the way v-model works as well as how Vue manages reactivity you might have to use more involved methods to copy the object.

One method that could work for you is to stringify then parse the object.

this.todo = JSON.parse(JSON.stringify(this.populateWith))

If your object is more complicated, is deeply nested, or has a circular structure, this might not work, you will have to make a deep copy.

The last thing to do is make sure that the form closes once the user submits their edits which is as simple as calling this.close().

Wrap Up

We now have a method of reusing forms in vue, thus avoiding repetition. I’ll admit I don’t know if this is the best method to reuse forms but it worked for me and I couldn’t find any other sources explaining this.

Hopefully it helps solve some of the problems you might be facing.

Thank you for reading!