Data-driven components in Vue.js
TLDR: In this article, we discuss building components based on the data definitions. Code available at vinicius0026/data-driven-components
This is the fourth article in our Structuring Large Vue.js Applications series. Here is the full list of released and planned articles:
- Properly typed Vuex Stores — published May 13, 2020
- Adopting TypeScript in your Vue.js Application in a sane way — published May 14, 2020
- Modularizing the logic of your Vue.js Application — published May 15, 2020
- Data-driven components — published May 25, 2020 — You are here
- Handling data at the edge of your Vue.js application — published June 1st, 2020
In the previous articles, we discussed how to adopt TypeScript in a lean way and how to modularize the application logic in Vue.js applications. But in both articles, we barely touched Vue components. It’s time to change that. In this article, we will pick up where we left and will leverage our Type definitions and our modularized logic to build a lean, maintainable, and reusable invoice component.
Let’s get started!
Sketching the functionality
In our previous articles, we have defined a simplified data model for an invoice application, and we have built the core logic for handling operations on an invoice. If you haven’t checked these articles yet, now it is the time to do it.
Today we are going to build a few components to render and manipulate an invoice.
Below we have a rough mockup of what we want the component to look like:
Please keep in mind that our goal here is to discuss code structure. We will overlook concerns such as UI and UX.
Planing the components
So, how do we go about breaking up the requirements into manageable components? And, perhaps more importantly, what will the interface (props, events emitted, slots) of these components look like?
Here is one possible high-level breakdown of the components:
The two main components here are the Invoice
and LineItem
components. The Invoice component takes an invoice object of type Types.Invoice
and, whenever this object changes, emits the updated invoice. The same thing happens for the LineItem
component.
The ProductSelector
component will encapsulate the logic for selecting a product and will emit the chosen product.
If we use appropriate names for the props and events emitted, we can use Vue’s v-model
directive to bind data to our components. Let's see how that works in code.
Invoice component
We can start by implementing the Invoice component fully, assuming the other parts are available. This approach will generate a wish-list of components that we will implement one by one.
Our invoice model doesn’t currently have the concept of Invoice Number or Invoice Due Date. It would be straightforward to add it to the invoice type and modify the invoice module, but to make this article simpler, we are just hard coding some values there for now.
Notice how we are taking a prop of type Types.Invoice
and are emitting input
events whenever the invoice is changed. Now our modularized Invoice logic is paying off its price. Look how simple the code in our Invoice component is: it just ties the events from the underlying components to the Invoice module.
We are using the Emit
decorator from vue-property-decorator
. It will emit the return value of the decorated function, which makes this code really concise. If you are not used to it, it is possible to achieve the same thing by doing:
Notice also how we are invoking the LineItem
and AddLineItem
components, that we have not yet implemented. Let's take care of that.
AddLineItem component
Let’s start with the AddLineItem
component. In the Invoice
component, we have defined that the AddLineItem
component should emit an add
event whenever a line item is added. This is the component definition:
This component is also rather simple. We have a button that will trigger our EditLineItemModal
component, passing a new LineItem object to it. This new line item object is built in the newLineItem
method. Notice how here we are using a Types.Partial<Types.LineItem>
type.
Types.Partial
is a helper that we will add to the types
folder to allow having incomplete objects of a certain type. In this case, we don't have a product to assign to the LineItem object, that is why we are using a partial. This how the Types.Partial
helper is defined:
A Partial
object will have the same properties of the passed-in type, but all fields can be undefined
or null
. This helper should be used with caution because we cannot know if the properties are present or not.
Let’s move on to the EditLineItemModal
component now.
EditLineItemModal component
We are using a SimpleModal
component here to encapsulate the modal behavior. It is the same code as available in https://vuejs.org/v2/examples/modal.html. We are not going to reproduce the modal code here, but it is available at the repo.
This component has three fields to define a LineItem: the product field, encapsulated in the ProductSelector
component, and two input fields for the rate
and quantity
.
One thing to notice here is how we are making a local copy of the passed-in prop. As we have Ok
and Cancel
buttons, we cannot update the prop itself when a field is changed, because the user might hit cancel. So we do a deep copy of the item
prop into the localLineItem
object anytime the item
changes and emit the local line item when the user clicks Ok
.
Also, as the rate
is a Decimal
object, we had to wrap its value in a getter
and setter
, so that we can transform it to and from a number, that is what the input
html element can handle. If you have several places in your application where you need to handle Decimals, you might want to create a DecimalInput
component that takes Decimals in and emits Decimals out, so that you can use v-model
directly with your Decimal object.
ProductSelector
The ProductSelector
component is a thin wrapper around the select
element.
We are hardcoding the products here to simplify our example. But in an actual application, this component would have the ability to search the products, loading them as needed from an API. The main takeaway here is that we are encapsulating the product selection in a component, so we can easily change its internals, without affecting the components that use it. If you need to implement a selector similar to this one, take a look at Vue Multiselect.
We have now completed the components needed to build the AddLineItem
functionality. Let's move on to the LineItem
component.
LineItem component
The LineItem
component shows the line item details, along with the line item total amount. There are also two buttons, one to edit the current line item and one to remove it from the invoice.
We are reusing the EditLineItemModal
component we wrote for the AddLineItem
. We emit a LineItem
object whenever the item is edited. We also emit a remove
event when the user clicks the Delete
link. Once again, we are using our module's logic when needed, in this case, to calculate the total amount of the line item.
Using the Invoice component
Now our Invoice
component is fully developed, and we can use it in our application. Let's add it to the existing HelloWorld
component.
Here, we are creating a local invoice, using the Invoice module, and passing it to the Invoice component we just wrote.
The Invoice component is fully usable — we can add, remove, and edit line items, and the total amount is calculated correctly. In a real application, instead of just tying the Invoice component with a local data invoice object, we would probably link it to a Vuex store that would eventually trigger network requests to send the data to some API. Anyway, we have neatly encapsulated the invoice manipulation logic in the component, which delegates the business logic handling to the modules.
Validation
If you are reading closely, you might have noticed that we haven’t added validation to our EditLineItemModal
form. This could lead to a bad state in our application because this component is taking a Partial
line item object as a prop, and it might as well emit a partial LineItem. Let's fix that now.
This is a bit naive validation, but it is enough for our purposes. Now it is not possible to save a line item if the product or rate are not set or if the quantity is not a number.
In a real application, we should use more robust validation libraries such as Vuelidate or Vee Validate.
Wrapping up
We have developed a few components to create our invoice functionality. We started by defining a rough wireframe for the invoice component and have broken it down into smaller pieces.
We created small and maintainable components that are derived from our type definitions. As promised, the components are a thin layer that wires the user interactions with our core logic. As long as we keep the interface (props/events) untouched, we can change our components freely, and the overall functionality should still work.
I hope you have liked this approach. Let me know your thoughts in the comments.
Originally published at https://viniciusteixeira.tk on May 25, 2020.