This project was first created back in 2019 to serve as an example how to build a product configurator using Vue 2 and Vuex. It used Vue CLI scaffolding.
We are in 2022 now and times have changed. Project scaffolding is still possible using the Vue CLI, but the Vite CLI is the new kid on the block which is much faster. Also, Vue 3 was released (already somewhere in 2021) and Pinia is the new defacto for state management (formerly it was Vuex).
Enough reasons to update this demo project. Please continue reading if you are here for the ‘old’ version. I will write about the updated version in a blog coming soon.
Part 3 Component implementation
Since usability becomes more and more important these days, this blog series covers a product configurator for a fictive webshop from concept to implementation.
Our product is a configurable postcard. You can change the text, shape of the card and the paper size. During configuration, the price is updated in real time based on your choices.
In this part, we’ll implement the frontend and connect it to the store. I’ll go through all the details step by step. The project is created using @vue/cli and the code is available on Github, release v0.2.
File and folder structure
In part 2 we created a ‘configurator’ module and an ‘account’ module in the vuex store. The first is responsible for the product configurator, the latter for authentication (login and logout). The two modules are fully separated and hence it makes sense to create two container components: AccountNav and PostCardConfigurator.
These container components handle everything with respect to the store. This means that they are the only components that dispatch actions. They pass props down to their child components. The child components are purely presentational, they just render their templates based on props.
User interaction with these components (i.e., a click on a button) results in an event emitted upwards to the parent. The events go upwards and upwards through the component tree until it arrives at the container component that communicates with the store to update the state.
In the App.vue component it looks like this:
The <AccountNav /> component is a simple Bootstrap navbar with a login form in it. The <PostCardConfigurator /> is the configurator.
Considering that these two components act as a root for their respective responsibilities, I created two folders ‘configurator’ and ‘account’ in the /components folder. All configurator related components go in the first folder and account related components are stored in the account folder. This way we not only separate the logic but also the views (i.e., components).
Implementing the Account Navbar
The navbar component looks like this:
It’s basically a Bootstrap navbar with some custom styling and a <LoginForm /> component. The login form component either shows a login form (not logged in) or a ‘You are logged in’ message with a logout button. I know it is a contrived example, but adding this simple login/ logout functionality shows how using modules improves the maintainability of the code.
The <LoginForm />, a presentational component, needs to know about the logged in status to show the correct thing to the user. So we have to pass the auth status as a prop. This auth status is coming from the store and is available in <AccountNav /> by using the Vuex helper mapState. In addition, the Vuex actions with respect to accounts are also available in <AccountNav /> using mapActions:
The <LoginForm /> is added to the template part of <AccountNav /> like this:
Apparently, the <LoginForm /> component is supposed to emit two events: ‘login’ and ‘logout’. Here is the code of this component:
It basically renders the form based on the auth status. When logged in, the message ‘You are logged in’ is displayed with a logout button.
The login and logout functions are straightforward, they just emit the events to the parent which in turn communicates with the store and passes the updated auth status back as a prop.
Implementing the product configurator
Just like the <AccountNav /> component, the <PostCardConfigurator /> component is the container and the only component that communicates with the store. For a customer who orders two postcards, it looks like this:
It basically consists of a <ProductList />, a <PriceContainer /> and a <ProceedToCheckoutButton />. Again, information about the configurator comes from the store (using mapState again) and is passed to these components as props. In addition, user interaction with these components (or their children) yields events that are emitted upwards.
This flow is illustrated in the diagram below:
With a deeply nested component structure, this is a little annoying as a lot of the components are just passing props down and events up. However, you are free to add more smart components that communicate with the store if that better fits the structure of your application. In extremis, you could decide to let every component talk to the store, but in my opinion, this leads to an unmaintainable codebase in the long run. For this rather small application, I decided to have just 2 smart components.
With that out of the way, let’s do a quick rundown of the PriceContainer and ProceedToCheckoutButton components. The first just renders the price (on 2 decimals) that is passed as a prop, the latter only renders a button. I have not set up a real add to cart implementation as it falls outside of the scope of this blog series. So, in the remainder of this article, I’ll discuss the <ProductList /> component.
Implementing <ProductList />
The <ProductList /> component receives an array of products as a prop and is responsible for rendering this list of products. In addition, it should render a ‘add product button’ which will result in a new empty postcard added to the list.
So, this sounds like we create two new components: <SingleProduct />, which we pass an entry from the products array and <AddProduct />.
The latter is very simple: it renders a button and on click emits an ‘add-product’ event to its parent. The <ProductList /> listens to this event and emits it upwards to its ‘smart’ parent. Then, the correct Vuex action and mutation are triggered, and a new product with a default configuration is added to the products array in the state.
The former is not much harder though: we need to render a list of products and hence, use the v-for directive:
We add id as a prop as we need it later on when resetting or removing a product. In addition, we add v-model=”product.config”. In my initial implementation, I tried to set the whole product object as v-model but that didn’t work. My linter warned me with this message: ‘v-model directives cannot update the iteration variable “product” itself’. Therefore, I added an extra property (config) to my product object layer and use that as v-model. This issue is better explained in this Stack Overflow thread.
Now let’s have a look at the implementation of the <SingleProduct /> component as things become interesting from here!
This component is responsible for rendering a single product and listening to interactions from the user with it:
Again, we split the component into child components, one for each input. In addition, we need to handle clicks on the ‘remove button’ in the top right and the ‘clear configuration’ link at the bottom right. The first should remove the product from the list, whereas the latter resets the configuration back to the default. Both just emit an event upwards with the product id as an argument. This way the ‘smart’ root component is able to remove or reset the right product based on this id.
Here is the full code of the <SingleProduct /> component:
The child components I just mentioned are <CardChooseShape />, <CardChoosePapersize />, <CardChooseAmount />, <CardChoosePaperquality />, <CardChooseHeadline /> and <CardChooseMaintext />. Actually, only <CardChooseShape /> is a bit more complex, so we treat it in detail in the next section. For the other five components, we just set a v-model as the corresponding property in the product.config object.
The CardChooseShape component is more complex because it has child components involved. We need this to be able to draw the different shapes of paper we like our users to choose from. We could have made our lives easier by using ordinary radio buttons, but this is more fun!
In its parent component’s template, the <CardChooseShape /> component is rendered as follows:
As for its sibling components, this component uses a single property from the config object as v-model. In addition, it emits a ‘changed’ event which is emitted upwards by the updateParent method we have discussed before. So far so good.
The <CardChooseShape /> component code looks like this:
Incoming props for this component are the ‘shape’ and the ‘id’. A v-model implementation assumes an incoming prop ‘value’ and an emitted event ‘input’. With the model option, we can overwrite these. Here, we use ‘shape’ as the prop (instead of ‘value’) and ‘changed’ as the event.
The template part of the component shows the list of <CardCustomRadioShape /> components we use to show the user the different shapes of paper. This group of components should be seen as a radio group. Each has a different value (‘theshape’ prop) and the checked one is the instance for which the ‘current’ prop equals the ‘theshape’ prop.
The code for the <CardCustomRadioShape /> component is show below:
As promised, under the hood we just use an <input type=”radio”> tag. The label is styled to get the right shapes. The ‘checked’ attribute of the radio element is defined by the equality of the ‘current’ and ‘theshape’ props.
Note that the computed property ‘inputId’ is necessary to get unique identifiers for each instance of <CardCustomRadioShape />. Just using ‘theshape’ prop would not suffice here in case we have multiple <SingleProduct /> instances on the page. Therefore, the ‘id’ is used to make a unique identifier.
Choosing a shape means that the chosen method is executed which emits the chosen shape to the parent. The parent listens to this event and emits it one layer up to the <SingleProduct /> component. This way the config.shape is updated via the v-model binding.
Summary and conclusion
Using v-model on the <SingleProduct /> component and its children introduces some complexities, but applying v-model in custom components as we have done for components such as <CardChooseShape /> yields a perfectly working product configurator.
Looking back at the implementation of the product configurator I must admit that thinking first and act thereafter really worked out well. Starting from the sketch in part 1, defining the components and child components felt really natural.
Also, thinking about the state, actions, and mutations and implementing it before implementing the components felt a bit like TDD, a practice I have been reading about before but never gave a try.
An obvious next step for this little project would be to implement it in a real webshop. This could be a Vue only SPA with some API/ backend, but it is also possible to add the product configurator to a typical Laravel project.