Building Dual Vue Version Compatible Components in Legacy Laravel Apps

📖 3 minutes read

When maintaining legacy Laravel dashboards that use Vue 1.x but planning migration to Vue 2.x, you can build forward-compatible components using template literals instead of single-file components.

The Problem

Vue 1.x doesn’t support modern single-file component syntax with <template>, <script>, and <style> tags. Direct Vue 2.x components break in Vue 1.x runtimes.

The Solution

Export components as JavaScript objects with template literals:

// Compatible with both Vue 1.x and 2.x
const DataTableComponent = {
  data() {
    return {
      items: [],
      loading: false
    }
  },
  template: `
    <div class="data-table">
      <div v-if="loading">Loading...</div>
      <table v-else>
        <tr v-for="item in items">
          <td>{{ item.name }}</td>
        </tr>
      </table>
    </div>
  `,
  mounted() {
    this.fetchData()
  },
  methods: {
    fetchData() {
      // API call logic
    }
  }
}

// Export for global registration
if (typeof window !== 'undefined') {
  window.DataTableComponent = DataTableComponent
}

Integration Pattern

  1. Compile in webpack/mix: resources/assets/js/components.js
  2. Export to window in old dashboard
  3. Register in legacy Vue 1.x app: Vue.component('data-table', window.DataTableComponent)
  4. Use in Blade: <data-table :config="config"></data-table>
  5. Same component works when you upgrade to Vue 2.x later

Why This Works

  • Template literals are standard ES6 JavaScript: Both Vue versions understand them
  • Plain component object definitions: Vue 1.x and 2.x both support this format
  • Window exports bypass module incompatibilities: No need for different build setups

Benefits

You maintain one codebase instead of duplicating components across Vue versions during multi-month migrations. When you finally upgrade to Vue 2.x, these components continue working without any changes.

Real-World Example

const ReportBuilderComponent = {
  props: ['initialFilters'],
  data() {
    return {
      filters: this.initialFilters || {},
      results: [],
      loading: false
    }
  },
  template: `
    <div class="report-builder">
      <div class="filters">
        <input v-model="filters.startDate" type="date">
        <input v-model="filters.endDate" type="date">
        <button @click="runReport">Generate</button>
      </div>
      <div v-if="loading" class="loading">Loading...</div>
      <table v-else class="results">
        <tr v-for="row in results">
          <td>{{ row.metric }}</td>
          <td>{{ row.value }}</td>
        </tr>
      </table>
    </div>
  `,
  methods: {
    async runReport() {
      this.loading = true
      const response = await fetch('/api/reports', {
        method: 'POST',
        body: JSON.stringify(this.filters)
      })
      this.results = await response.json()
      this.loading = false
    }
  }
}

if (typeof window !== 'undefined') {
  window.ReportBuilderComponent = ReportBuilderComponent
}

This pattern has saved countless hours during gradual Vue upgrades in legacy Laravel applications.

Daryle De Silva

VP of Technology

11+ years building and scaling web applications. Writing about what I learn in the trenches.

Comments

Leave a Reply

Your email address will not be published. Required fields are marked *