Vue Component System

Module 14: JavaScript Frontend Frameworks - Vue & Angular

Introduction to Components

Components are the building blocks of Vue applications. A component is a reusable, self-contained piece of code that encapsulates HTML, CSS, and JavaScript functionality. Components allow us to build complex user interfaces by composing them from smaller, focused pieces.

Real-world analogy: Components are like LEGO blocks. Each block has a specific purpose and can connect to other blocks. You can build complex structures by combining simple blocks in various ways. Similarly, Vue components are modular pieces that you can combine to create sophisticated applications.

flowchart TD A[Application] --> B[Header Component] A --> C[Main Content Component] A --> D[Footer Component] C --> E[Sidebar Component] C --> F[Product List Component] F --> G[Product Card Component] G --> H[Rating Component] style A fill:#42b883,color:white style B fill:#35495e,color:white style C fill:#42b883,color:white style D fill:#35495e,color:white style E fill:#35495e,color:white style F fill:#42b883,color:white style G fill:#35495e,color:white style H fill:#35495e,color:white

Component hierarchy in a typical Vue application

Why Use Components?

Components provide several key benefits to modern web development:

Reusability

Once you create a component, you can reuse it throughout your application or even across projects. This prevents code duplication and promotes consistency.

Example: A button component with standard styling and behavior can be reused across the entire application, ensuring consistent look and feel.

Maintainability

Components break down complex UIs into manageable, focused pieces. Each component is responsible for a specific part of the UI, making the codebase easier to understand and maintain.

Example: Instead of a 500-line file handling an entire page, you might have 10 components of 50 lines each, with clear responsibilities.

Encapsulation

Components encapsulate their functionality, keeping their internal logic and state isolated from the rest of the application. This reduces side effects and makes debugging easier.

Example: A dropdown component manages its own open/closed state without affecting other parts of the application.

Composition

Complex UIs can be built by composing simpler components, allowing for a logical organization of your application's structure.

Example: A shopping cart page might be composed of cart item components, which themselves contain product image components, quantity selectors, and price displays.

Component Registration

Before using a component in Vue, you need to register it. There are two ways to register components:

Global Registration

Global components are available throughout your application without needing to explicitly import them into each component where they're used.

// main.js
import { createApp } from 'vue'
import App from './App.vue'
import ButtonComponent from './components/ButtonComponent.vue'

const app = createApp(App)

// Register globally
app.component('button-component', ButtonComponent)

app.mount('#app')

Pros: Easy to use anywhere; no need to import in individual components.

Cons: All globally registered components are included in the final bundle even if they aren't used, potentially increasing bundle size. They also make dependency relationships less explicit.

Local Registration

Local components are registered within the specific components that use them. This approach is generally preferred for most components.

// ParentComponent.vue
import ButtonComponent from './ButtonComponent.vue'

export default {
  components: {
    ButtonComponent
    // or with a different name: 'my-button': ButtonComponent
  },
  // rest of the component definition...
}

Pros: Makes dependencies explicit; supports tree-shaking for better bundle size optimization.

Cons: Requires importing in each component where it's used.

Best Practice:

Use local registration for most components to maintain clear dependency relationships and optimize bundle size. Use global registration sparingly for truly ubiquitous components like basic UI elements or for plugins that register their own components.

Component Types and Definition Formats

Vue offers different ways to define components, each with its own use cases and advantages.

Single-File Components (SFCs)

The most common and recommended approach for Vue applications is using Single-File Components, which have a .vue file extension. SFCs contain the template, script, and style for a component in a single file.

<!-- ButtonComponent.vue -->
<template>
  <button class="custom-button" @click="handleClick">
    {{ text }}
  </button>
</template>

<script>
export default {
  props: {
    text: {
      type: String,
      default: 'Click me'
    }
  },
  methods: {
    handleClick() {
      this.$emit('button-click')
    }
  }
}
</script>

<style scoped>
.custom-button {
  background-color: #42b883;
  color: white;
  border: none;
  padding: 10px 15px;
  border-radius: 4px;
  cursor: pointer;
}
.custom-button:hover {
  background-color: #35495e;
}
</style>

Benefits of SFCs:

JavaScript Object Components

For simpler use cases or when not using a build system, you can define components as JavaScript objects directly:

// Define a component using JavaScript object
const ButtonComponent = {
  template: `
    <button class="custom-button" @click="handleClick">
      {{ text }}
    </button>
  `,
  props: {
    text: {
      type: String,
      default: 'Click me'
    }
  },
  methods: {
    handleClick() {
      this.$emit('button-click')
    }
  }
}

Component Definition Styles in Vue 3

Vue 3 introduced a new Composition API alongside the traditional Options API. Both can be used in SFCs and JavaScript object components.

Options API

<script>
export default {
  // Component state
  data() {
    return {
      count: 0
    }
  },
  // Computed properties
  computed: {
    doubleCount() {
      return this.count * 2
    }
  },
  // Methods
  methods: {
    increment() {
      this.count++
    }
  },
  // Lifecycle hooks
  mounted() {
    console.log('Component mounted')
  }
}
</script>

Organizes code by option types (data, methods, computed, etc.). Familiar to Vue 2 developers and often easier for beginners.

Composition API

<script setup>
import { ref, computed, onMounted } from 'vue'

// Component state
const count = ref(0)

// Computed properties
const doubleCount = computed(() => count.value * 2)

// Methods
function increment() {
  count.value++
}

// Lifecycle hooks
onMounted(() => {
  console.log('Component mounted')
})
</script>

Organizes code by logical concern rather than option type. Better TypeScript support and more flexible for complex components.

Component Templates

The template defines a component's HTML structure and how it renders to the DOM. Vue offers different ways to define templates.

Template in Single-File Components

In an SFC, the template is defined in the <template> section:

<template>
  <div class="user-profile">
    <img :src="user.avatar" :alt="user.name">
    <h2>{{ user.name }}</h2>
    <p>{{ user.bio }}</p>
  </div>
</template>

Template in JavaScript

For JavaScript object components, templates can be defined as strings:

const UserProfile = {
  template: `
    <div class="user-profile">
      <img :src="user.avatar" :alt="user.name">
      <h2>{{ user.name }}</h2>
      <p>{{ user.bio }}</p>
    </div>
  `,
  // other options...
}

Render Functions

For advanced use cases, Vue allows defining the component's output using render functions instead of templates:

import { h } from 'vue'

export default {
  props: ['user'],
  render() {
    return h('div', { class: 'user-profile' }, [
      h('img', { src: this.user.avatar, alt: this.user.name }),
      h('h2', this.user.name),
      h('p', this.user.bio)
    ])
  }
}

When to use render functions: For programmatically generated content, complex conditional rendering, or when building reusable component libraries that need maximum flexibility.

Template Features

Vue templates support all the template syntax we covered in the previous lecture:

Component Lifecycle

Each Vue component instance goes through a series of initialization steps when it's created. This is called the component lifecycle. Vue provides lifecycle hooks that let you run code at specific stages of this process.

flowchart TD A[beforeCreate] --> B[created] B --> C[beforeMount] C --> D[mounted] D --> E[beforeUpdate] E --> F[updated] E --> G[beforeUnmount] F --> G G --> H[unmounted] style A fill:#42b883,color:white style B fill:#42b883,color:white style C fill:#42b883,color:white style D fill:#42b883,color:white style E fill:#35495e,color:white style F fill:#35495e,color:white style G fill:#35495e,color:white style H fill:#35495e,color:white

Main Lifecycle Hooks

export default {
  // Called before the component instance is initialized
  beforeCreate() {
    console.log('Component is being created')
    // Data and events are not yet set up
  },
  
  // Called after the instance is created
  created() {
    console.log('Component is created')
    // Data is reactive, events are active
    // Good place to make API calls
    this.fetchData()
  },
  
  // Called before the component is mounted to the DOM
  beforeMount() {
    console.log('Component is about to be mounted')
  },
  
  // Called after the component is mounted to the DOM
  mounted() {
    console.log('Component is mounted')
    // DOM is accessible
    // Good place to initialize libraries that need DOM access
    this.initializeChart()
  },
  
  // Called before the component updates due to data changes
  beforeUpdate() {
    console.log('Component is about to update')
  },
  
  // Called after the component has updated
  updated() {
    console.log('Component has updated')
    // DOM is re-rendered with new data
  },
  
  // Called before the component is unmounted/removed
  beforeUnmount() {
    console.log('Component is about to be unmounted')
    // Good place to clean up resources
    this.destroyChart()
  },
  
  // Called after the component is unmounted/removed
  unmounted() {
    console.log('Component is unmounted')
    // Component is completely destroyed
  }
}

Practical Usage of Lifecycle Hooks

export default {
  data() {
    return {
      chartInstance: null,
      dataLoaded: false,
      users: []
    }
  },
  
  // Best for data fetching
  created() {
    // API calls, initialize non-DOM data
    fetch('https://api.example.com/users')
      .then(response => response.json())
      .then(data => {
        this.users = data
        this.dataLoaded = true
      })
  },
  
  // Best for DOM interactions
  mounted() {
    // Initialize libraries that need DOM access
    import('chart.js').then(Chart => {
      const ctx = this.$refs.chart.getContext('2d')
      this.chartInstance = new Chart(ctx, {
        // chart configuration
      })
    })
    
    // Add event listeners
    window.addEventListener('resize', this.handleResize)
  },
  
  // Clean up resources
  beforeUnmount() {
    // Destroy any objects to prevent memory leaks
    if (this.chartInstance) {
      this.chartInstance.destroy()
    }
    
    // Remove event listeners
    window.removeEventListener('resize', this.handleResize)
  }
}

Lifecycle Hooks with Composition API

In the Composition API, lifecycle hooks are imported functions:

<script setup>
import { onMounted, onBeforeUnmount, ref } from 'vue'

const chartInstance = ref(null)

onMounted(() => {
  // Similar to mounted() in Options API
  import('chart.js').then(Chart => {
    const ctx = document.getElementById('chart').getContext('2d')
    chartInstance.value = new Chart(ctx, {
      // chart configuration
    })
  })
  
  window.addEventListener('resize', handleResize)
})

onBeforeUnmount(() => {
  // Similar to beforeUnmount() in Options API
  if (chartInstance.value) {
    chartInstance.value.destroy()
  }
  
  window.removeEventListener('resize', handleResize)
})

function handleResize() {
  // Resize logic here
}
</script>

Component Communication

Vue components need to communicate with each other. The most basic form of communication is between parent and child components, which we'll explore in greater detail in our next lecture.

flowchart TD A[Parent Component] --"Props"--> B[Child Component] B --"Events"--> A style A fill:#42b883,color:white style B fill:#35495e,color:white

Parent-Child Communication

Communication Between Siblings or Distant Components

Dynamic Components

Vue provides a special <component> element that allows switching between different components dynamically.

<template>
  <div class="tab-container">
    <div class="tab-buttons">
      <button 
        v-for="tab in tabs" 
        :key="tab.name"
        @click="currentTab = tab.name"
        :class="{ active: currentTab === tab.name }">
        {{ tab.label }}
      </button>
    </div>
    
    <!-- Dynamic component based on currentTab -->
    <component :is="currentTabComponent" class="tab-content"></component>
  </div>
</template>

<script>
import HomeTab from './tabs/HomeTab.vue'
import ProfileTab from './tabs/ProfileTab.vue'
import SettingsTab from './tabs/SettingsTab.vue'

export default {
  components: {
    HomeTab,
    ProfileTab,
    SettingsTab
  },
  data() {
    return {
      currentTab: 'home',
      tabs: [
        { name: 'home', label: 'Home' },
        { name: 'profile', label: 'Profile' },
        { name: 'settings', label: 'Settings' }
      ]
    }
  },
  computed: {
    currentTabComponent() {
      // Convert 'home' to 'HomeTab', etc.
      return this.currentTab.charAt(0).toUpperCase() + 
             this.currentTab.slice(1) + 'Tab'
    }
  }
}
</script>

KeepAlive for Component State Preservation

When switching between components with <component :is>, the component is destroyed and recreated by default. To preserve their state, you can wrap them in a <keep-alive> component:

<keep-alive>
  <component :is="currentTabComponent"></component>
</keep-alive>

Real-world use case: This is particularly useful for preserving form input data when users switch between tabs, or for maintaining scroll position in lists.

KeepAlive Hooks

Components inside <keep-alive> get access to two additional lifecycle hooks:

export default {
  // Called when a kept-alive component is reactivated
  activated() {
    console.log('Component activated')
    // Good place to fetch fresh data
    this.refreshData()
  },
  
  // Called when a kept-alive component is deactivated
  deactivated() {
    console.log('Component deactivated')
    // Good place to pause expensive operations
    this.pauseVideoPlayback()
  }
}

Practical Component Example: Reusable Modal

Let's create a reusable modal component to illustrate the component concept:

// ModalComponent.vue
<template>
  <transition name="modal-fade">
    <div v-if="isVisible" class="modal-overlay" @click="closeOnBackdrop ? close() : null">
      <div class="modal-container" @click.stop>
        <div class="modal-header">
          <h3>{{ title }}</h3>
          <button class="modal-close" @click="close">×</button>
        </div>
        
        <div class="modal-body">
          <slot>Default content if no slot content provided</slot>
        </div>
        
        <div class="modal-footer">
          <slot name="footer">
            <button class="modal-button cancel" @click="close">Cancel</button>
            <button class="modal-button confirm" @click="confirm">Confirm</button>
          </slot>
        </div>
      </div>
    </div>
  </transition>
</template>

<script>
export default {
  name: 'ModalComponent',
  props: {
    title: {
      type: String,
      default: 'Modal Title'
    },
    isVisible: {
      type: Boolean,
      default: false
    },
    closeOnBackdrop: {
      type: Boolean,
      default: true
    }
  },
  methods: {
    close() {
      this.$emit('close')
    },
    confirm() {
      this.$emit('confirm')
    }
  },
  created() {
    // Add ESC key listener to close modal
    const handleEsc = (event) => {
      if (event.key === 'Escape' && this.isVisible) {
        this.close()
      }
    }
    
    document.addEventListener('keydown', handleEsc)
    
    this.$once('hook:beforeDestroy', () => {
      document.removeEventListener('keydown', handleEsc)
    })
  }
}
</script>

<style scoped>
.modal-overlay {
  position: fixed;
  top: 0;
  left: 0;
  right: 0;
  bottom: 0;
  background-color: rgba(0, 0, 0, 0.5);
  display: flex;
  justify-content: center;
  align-items: center;
  z-index: 100;
}

.modal-container {
  background-color: white;
  border-radius: 8px;
  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.33);
  width: 500px;
  max-width: 90%;
  max-height: 90%;
  display: flex;
  flex-direction: column;
}

.modal-header {
  display: flex;
  justify-content: space-between;
  align-items: center;
  padding: 15px 20px;
  border-bottom: 1px solid #eee;
}

.modal-close {
  background: none;
  border: none;
  font-size: 20px;
  cursor: pointer;
  color: #999;
}

.modal-body {
  padding: 20px;
  overflow-y: auto;
  flex-grow: 1;
}

.modal-footer {
  padding: 15px 20px;
  border-top: 1px solid #eee;
  display: flex;
  justify-content: flex-end;
  gap: 10px;
}

.modal-button {
  padding: 8px 15px;
  border-radius: 4px;
  border: none;
  cursor: pointer;
}

.modal-button.cancel {
  background-color: #f5f5f5;
}

.modal-button.confirm {
  background-color: #42b883;
  color: white;
}

/* Transition animations */
.modal-fade-enter-active, .modal-fade-leave-active {
  transition: opacity 0.3s;
}

.modal-fade-enter-from, .modal-fade-leave-to {
  opacity: 0;
}
</style>

Using the Modal Component

<template>
  <div>
    <button @click="showModal = true">Open Modal</button>
    
    <modal-component 
      title="Confirm Action" 
      :is-visible="showModal" 
      @close="showModal = false"
      @confirm="handleConfirm">
      
      <p>Are you sure you want to proceed with this action?</p>
      <p>This cannot be undone.</p>
      
      <!-- Custom footer using named slot -->
      <template #footer>
        <button class="custom-button" @click="showModal = false">No, Cancel</button>
        <button class="custom-confirm-button" @click="handleConfirm">Yes, Proceed</button>
      </template>
    </modal-component>
  </div>
</template>

<script>
import ModalComponent from './ModalComponent.vue'

export default {
  components: {
    ModalComponent
  },
  data() {
    return {
      showModal: false
    }
  },
  methods: {
    handleConfirm() {
      // Handle the confirmation action
      console.log('Action confirmed!')
      this.showModal = false
    }
  }
}
</script>

This example illustrates several key component concepts:

Activities for Practice

Exercise 1: Create a Button Component

Create a reusable button component with the following features:

  • Props for customization: text, type (primary, secondary, danger), size (small, medium, large), disabled state
  • Custom events for click and hover actions
  • Slots for allowing custom content (icons, text)
  • Styled appropriately based on the type and size props

Then, create a simple app that uses multiple instances of your button component with different configurations.

Exercise 2: Component Lifecycle Exploration

Create a component that demonstrates all the major lifecycle hooks. For each hook:

  • Add a console.log statement with the hook name
  • Display the hook name in the UI when it fires
  • Add a simple action that's appropriate for that lifecycle stage

Create a parent component with a toggle button that mounts/unmounts this component so you can observe the complete lifecycle.

Exercise 3: Dynamic Tabs Component

Expand on the dynamic components example to create a full tab system:

  • Create a Tabs container component
  • Create a Tab component for individual tab content
  • Implement tab switching with dynamic components
  • Use keep-alive to preserve tab state
  • Add the ability to add and remove tabs dynamically

For an extra challenge, implement a "drag to reorder" feature for the tabs.

Additional Resources