DEV Community

Cover image for Part 4 (b): How to Build a To-Do App with Vue Js: Creating Reusable UI
Makanju Oluwafemi
Makanju Oluwafemi

Posted on

Part 4 (b): How to Build a To-Do App with Vue Js: Creating Reusable UI

In the previous phase of our project, we successfully established the foundational aspects by setting up the project boilerplate, integrating necessary libraries, and crafting essential pages like Login.vue, Register.vue, and Dashboard.vue. We also configured routing to ensure smooth navigation throughout the application.

If you've reached this point, congratulations on this remarkable achievement! Now, we're about to delve into the realm of reusable components, a crucial step towards building a modular and maintainable application.

To provide a snapshot of our current folder structure, you can refer to the image below:

folder-structure

Within the /src/components directory, let's organize our components into different folders based on their features. You'll create four main folders: /auth, /common, /dashboard, and /utility.

While the names can be adapted according to your preferences, this structure promotes a more feature-centric approach. For instance, components associated with authentication will reside in the /src/auth folder, and similarly for other features.

The /src/common folder, however, will be our starting point for crafting reusable components.

Create a "Navbar" Component:

/src/components/common/Navbar.vue

<template>
   <header class="flex justify-between items-center bg-primary p-5">
      <h1 class="font-popins text-4xl text-[#fff]"> {{ returnResponsiveTitle }}</h1>
      <div class="flex items-center">    
         <nav v-if="!isMobile">
            <ul class="flex ">
               <li
                  class="p-4 w-[120px] text-center font-popins rounded-2xl" 
                  v-for="(url , index) in urlDatas " :key="index">
                  <router-link class="text-[#FFFFFF] " :to="{name: url.url}">{{ url.name }}</router-link>
               </li>
            </ul>
         </nav>
         <div class="flex items-center  justify-center bg-[#fff] w-[50px] h-[50px] rounded-full">
            <h1 class="text-2xl font-popins font-bold">MA</h1>
         </div>
         <div v-if="isMobile" class="flex items-center justify-center p-2 rounded-sm">
            <font-awesome-icon
            @click="toggleNav" 
            class="text-4xl text-[#ffff] cursor-pointer"
            :icon="['fas', `${showNav ? 'times': 'hamburger'}`]"/>
         </div>
      </div>
   </header>
   <!-- show on mobile -->
      <div class="bg-primary" v-if="isMobile">
         <div class="flex flex-col" v-if="showNav">    
            <nav>
               <ul class="flex flex-col">
                  <li
                     class="p-4 w-[120px]  font-popins rounded-2xl" 
                     v-for="(url , index) in urlDatas " :key="index">
                     <router-link class="text-[#FFFFFF] " :to="{name: url.url}">{{ url.name }}</router-link>
                  </li>
               </ul>
            </nav>
         </div>
      </div>
</template>

<script>
export default {
   name:'NavBar',
   data(){
      return {
        urlDatas: [
            {
              url: 'login',
              name: 'Login',
            },
            {
              url: 'register',
              name: 'Register',
            },
            {
              url: 'dashboard',
              name: 'Dashboard',
            }
        ],
        isMobile: false,
        showNav: false,
      }
   },
   mounted(){
      this.checkScreenSize();
      addEventListener('resize' , this.checkScreenSize )
   },
   methods: {
      checkScreenSize() {
         this.isMobile = window.innerWidth <= '768'
      },
      toggleNav(){
         this.showNav = !this.showNav
      }
   },
   computed: {
      returnResponsiveTitle() {
         if(this.isMobile){
            return 'Vma'
         }
         return 'VueMadeEasy'
      }
   }
}
</script>


Enter fullscreen mode Exit fullscreen mode

This "Navbar" component is a responsive navigation bar that adapts its layout based on screen size. It features a dynamic title that changes between "Vma" for mobile and "VueMadeEasy" for larger screens. The navigation items are displayed as links, and a toggle button appears on mobile devices to expand or collapse the navigation menu.

Next, you will register and use the Navbar component in App.vue file.

/src/App.vue

<template>
  <NavBar />
  <RouterView />
</template>
<script>
import NavBar from './components/common/NavBar.vue'

export default {
  components: {
    NavBar
  }
}
</script>

Enter fullscreen mode Exit fullscreen mode

By integrating the "Navbar" component within App.vue, you've established a consistent navigation element across your application. This modular approach streamlines development and ensures the component's reusability.

Now, let's proceed to the /utility folder. Here, you'll create components that serve as building blocks used in multiple parts of your application. These components significantly enhance control and flexibility while maintaining consistency.

Consider changing the focus color of an input field in an application. You never want to start doing that everywhere. In this scenario, you will simply apply all of the necessary changes to your component in the /utility folder. voila! The changes will be reflected in all areas of the app that use the component.

Create utility components:

/src/components/utility/BaseButton.vue

<template>
   <div class="div">
      <label 
      class="size-2xl"
      :for="id">{{ label }}</label>
      <input 
         :id="id"
         :class="[isFocused && 'focused']"
         class="p-2 border-2 border-[#eee] w-full rounded-xl"
         :type="text"
         :placeholder="placeHolder"
         @focus="isFocused = true"
         @blur="isFocused = false"
         @input="handleText"
         :value="value"
         :aria-label="label"
         :aria-describedby="`${id}-description`"
      >
      <div 
         v-if="error" 
         class="h-[10px] mt-1">
         <span class="text-[red]">{{ msg }}</span>
      </div>
   </div>
</template>

<script>
   export default {
      name:'BaseInput',
      props: {
         id: String,
         label:String,
         text: String,
         placeHolder: String,
         value: [String , Number],
         error: {
            type: Boolean,
            default: false,
         },
         msg: {
            type: String,
            msg: '',
         }
      },
      data(){
         return {
           isFocused: false,
         }
      },
      methods: {
         handleText(e){
           this.$emit('update:modelValue', e.target.value )
         }
      }
   }
</script>

<style scoped>
.focused{
   outline: 2px solid #414066;
   box-shadow: 0px 2px 5px rgba(0, 0, 0, .3);
}
</style>

Enter fullscreen mode Exit fullscreen mode

In the script section, the component is named "BaseInput" and takes props including id, label, placeHolder, value, error, and msg. It utilizes the isFocused data property to monitor input focus, and the handleText method triggers an update:modelValue event, emitting the input value when it changes.

/src/components/utility/BaseModal.vue

<template>
   <div class="flex items-center justify-center h-[100%] bg-[#242222b7] w-full">   
      <div class="bg-[#fff] w-1/5 h-2/5 flex flex-col drop-shadow-sm rounded-lg">
         <span class="flex items-center justify-end  p-4 right-0">
            <font-awesome-icon 
            @click="handleClose"
            class="mb-5 text-2xl c-tst cursor-pointer" :icon="['fa', 'times']" /> 
         </span>
         <div class="flex flex-col items-center justify-center w-full">    
            <font-awesome-icon class="text-7xl mb-5 text-[green]" :icon="['fa', 'circle-check']" />
            <h1 class="text-4xl font-popins font-bold">Success</h1>
         </div>
      </div>
   </div>
</template>

<script>
   export default {
      name:'BaseModal',
      methods: {
         handleClose(){
            this.$emit('close')
         }
      }
   }
</script>

Enter fullscreen mode Exit fullscreen mode

Next, a modal component is defined with a dark semi-transparent background. Inside the modal, there's a container with a white background, displaying a checkmark icon and a success message. The close button, represented by a times icon, is positioned at the upper right corner.

In the script section, the component named "BaseModal" includes a method named "handleClose," which emits the 'close' event when triggered.

This component appears to be a success modal that displays a checkmark icon along with a success message and a close button. The close button emits a custom 'close' event when clicked, allowing parent components to handle modal closure behavior.

modal

/src/components/utility/BaseButton

<template>
   <button
      :class="['p-3 text-center w-full rounded-4xl', variantClass ]"
      :disabled="disabled"
      :aria-disabled="disabled"
      @click="handleClick"
      type="submit"
   >
    {{ label }}
  </button>
</template>

<script>
   export default {
      name:'BaseButton',
      props: {
         label: String,
         variant: {
            type: String,
            default: 'primary',
         },
         disabled: Boolean,
      },
      computed: {
         variantClass(){
            if(this.disabled){
               return  `variant-${this.variant}-disabled`;
            }
            return `variant-${this.variant}`;
         }
      },
      methods: {
         handleClick() {
            this.$emit('clk');
         },
      }
   }
</script>

<style>
   /* variant style */
   .variant-primary {
      background-color: #414066;
      color: #fff;
   }

   .variant-primary-disabled {
      background-color: #414066c3;
      cursor: not-allowed;
   }

   .variant-secondary {
      background-color: #4C6640;
      color: #fff;
   }

   .variant-secondary-disabled {
      background-color: #4c6640b4;
      cursor: not-allowed;
   }

</style>

Enter fullscreen mode Exit fullscreen mode

Next, create a .vue file for "BaseButton". Defined a button with dynamic classes based on the variant and disabled status in the file. The button's click event triggers the handleClick method to emit the 'clk' event.

In the script section, the component named "BaseButton" accepts props including label, variant, and disabled. The computed property "variantClass" dynamically generates class names based on the variant and disabled state. The handleClick method emits the 'clk' event.

The style section includes CSS rules for different variants of the button, adjusting background color and cursor based on the variant and disabled status.

/src/components/utility/BaseToast.vue

<template>
   <div v-if="show" class="p-3 flex border-t-4 border-t-[#525252] justify-between items-center bg-[#fee2e2] h-[70px]">
      <h2 class="text-[#e74e3c] text-sm">Oops! {{ msg }}</h2>
      <div class="p-3">
         <font-awesome-icon 
         class="text-4xl text-[#e74e3c]"
         :icon="['fas', 'exclamation']" />
      </div>
   </div>
</template>

<script>
   export default {
      name:'AppToast',
      props: {
         variant: {
           type: String,
           default: 'default',
         },
         show: Boolean,
         msg: String,
      },
   }
</script>
Enter fullscreen mode Exit fullscreen mode

A toast component is created with conditional rendering based on the "show" prop. The toast displays a message and an icon within a colored bar. The message is dynamically generated with an "Oops!" prefix. The color of the bar and icon is determined by the "variant" prop.

In the script section, the component named "AppToast" accepts props including "variant," "show," and "msg." The "variant" prop defines the color scheme, "show" controls whether the toast is displayed, and "msg" holds the message content.

This component is designed to show a notification toast with a customizable message and color, providing a visual indicator of various messages or alerts to users.

Now that you have created all the utility component needed, let's use them to create your "Login" and "Register" screens.

/src/components/auth/AuthLogin.vue


<template>
   <auth-wrapper class="m-auto mt-5">
      <h1 class="text-primary pt-5 pb-5 font-bold text-2xl font-popins">Login</h1>
      <app-input 
         :id = "'username'" 
         :label = "'Username :'"
         :value = "userName"
         :error = "user.error"
         :text = "'text'"
         :placeHolder="'Enter your username'"
         :msg = "user.msg"
         v-model="userName"
      />
      <app-input 
         class="mt-7"
         :id = "password" 
         :label = "'Password :'"
         :text = "'password'"
         :placeHolder="'Enter your password'"
         :value = "password"
         :error = "user.error"
         :msg = "user.msg"
         v-model="userName"
      />
      <app-button 
         class="mt-7 mb-3"
         :disabled="false" 
         :label="'Sign In'" 
         :variant="'primary'"
         @clk="handleSubmit"
      />
      <p class="mb-10 text-right">Already have an account? 
      <router-link 
      class="text-primary" 
      :to="{name:'register'}">sign up
      </router-link></p>
   </auth-wrapper>   
</template>

<script>
   import AppButton from '../utility/BaseButton.vue';
   import AppInput from '../utility/BaseInput.vue';
   import AuthWrapper from './part/AuthWrapper.vue';

   export default {
      name:'AuthLogin',
      components: {
         AuthWrapper,
         AppInput,
         AppButton,
      },
      data(){
         return {
            userName: '',
            password: '',
            user: {
               error: false,
               msg: 'invalid input field!',
            }
         }
      },
      methods: {
         handleSubmit(){
            console.log('hello')
         }
      }
   }
</script>

Enter fullscreen mode Exit fullscreen mode

Next, you will have to use this component in your login View

src/views/Login.vue

<template>
   <auth-login/>
</template>

<script>
import AuthLogin from '../components/auth/AuthLogin.vue'

export default {
   name:'LoginView',
   components: {
      AuthLogin
   },
}
</script>
Enter fullscreen mode Exit fullscreen mode

This code creates a view component called "LoginView" that renders a user login form using the "AuthLogin" component. The "AuthLogin" component is imported and registered as a child component for reusability and modularity.

login-snapshot

/src/components/auth/AuthSignup.vue

<template>
   <auth-wrapper class="m-auto mt-5">
      <h1 class="text-primary pt-5 pb-5 font-bold text-2xl font-popins">Register</h1>
      <app-input 
         :id = "'username'" 
         :label = "'Username :'"
         :value = "userName"
         :error = "user.error"
         :text = "'text'"
         :placeHolder="'Enter your username'"
         :msg = "user.msg"
         v-model="userName"
      />

      <app-input 
         class="mt-7"
         :id = "'email'" 
         :label = "'Email :'"
         :value = "email"
         :error = "user.error"
         :text = "'email'"
         :placeHolder="'Enter your username'"
         :msg = "user.msg"
         v-model="email"
      />

      <app-input 
         class="mt-7"
         :id = "password" 
         :label = "'Password :'"
         :text = "'password'"
         :placeHolder="'Enter your password'"
         :value = "password"
         :error = "user.error"
         :msg = "user.msg"
         v-model="userName"
      />
      <app-input 
         class="mt-7"
         :id = "'confpass'" 
         :label = "'Confirm Password :'"
         :text = "'password'"
         :placeHolder="'Confirm your password'"
         :value = "confirmPass"
         :error = "user.error"
         :msg = "user.msg"
         v-model="confirmPass"
      />
      <app-button 
         class="mt-7 mb-3"
         :disabled="false" 
         :label="'Sign In'" 
         :variant="'primary'"
         @clk="handleSubmit"
      />
      <p class="mb-10 text-right">Already have an account? 
      <router-link 
      class="text-primary" 
      :to="{name:'login'}">sign in
      </router-link></p>
   </auth-wrapper>   
</template>

<script>
   import AppButton from '../utility/BaseButton.vue';
   import AppInput from '../utility/BaseInput.vue';
   import AuthWrapper from './part/AuthWrapper.vue';

   export default {
      name:'AuthSignup',
      components: {
         AuthWrapper,
         AppInput,
         AppButton,
      },
      data(){
         return {
            userName: '',
            email:'',
            password: '',
            confirmPass: '',
            user: {
               error: false,
               msg: 'invalid input field!',
            }
         }
      },
      methods: {
         handleSubmit(){
            console.log('hello')
         }
      }
   }
</script>

Enter fullscreen mode Exit fullscreen mode

/src/views/Register.vue

<template>
   <auth-signup/>
</template>
<script>

import AuthSignup from '../components/auth/AuthSignup.vue'

export default {
   name:'RegisterView',
   components: {
      AuthSignup
   },
}
</script>
Enter fullscreen mode Exit fullscreen mode

This code defines a Vue view named "RegisterView" that displays a user registration form using the imported "AuthSignup" component. The "AuthSignup" component is encapsulated in a separate file and directory for modularity and ease of management.

register-snapshot

By using a modular and component-driven approach, you not only improve code organization but also lay the groundwork for a scalable and maintainable Vue.js application. Happy coding!

Top comments (0)