Vue inside Aurelia

Libception. Learn how to use Vue inside of your Aurelia applications.

Aurelia's design embraces flexibility and interoperability, making it well-suited for integration with other libraries and frameworks. One common scenario is incorporating Vue components into an Aurelia application. This integration showcases Aurelia's adaptability and how it can leverage the strengths of other ecosystems. Below, we provide a detailed guide and code examples to integrate a Vue component seamlessly into an Aurelia 2 application.

Install Dependencies

First, ensure that your Aurelia project has the necessary dependencies to use Vue. You'll need Vue 3's core library and TypeScript types for single-file components (.vue files).

npm install vue
npm install --save-dev @vitejs/plugin-vue @vitejs/plugin-vue-jsx vue-tsc typescript

Configure Your Build System

Configure Vite to handle .vue single-file components with the official Vue plugin:

vite.config.js:

import { defineConfig } from 'vite';
import vue from '@vitejs/plugin-vue';
import vueJsx from '@vitejs/plugin-vue-jsx';

export default defineConfig({
  plugins: [
    vue({
      // Optional: Configure Vue SFC options
      script: {
        defineModel: true,
        propsDestructure: true
      }
    }),
    vueJsx() // For JSX/TSX support
  ],
  resolve: {
    alias: {
      // Ensure compatibility with Vue runtime
      'vue': 'vue/dist/vue.esm-bundler.js'
    }
  },
  define: {
    // Enable Vue devtools in development
    __VUE_OPTIONS_API__: true,
    __VUE_PROD_DEVTOOLS__: false
  }
});

TypeScript Configuration:

Add Vue support to your tsconfig.json:

{
  "compilerOptions": {
    "types": ["vite/client"],
    "isolatedModules": true,
    "esModuleInterop": true,
    "allowSyntheticDefaultImports": true,
    "strict": true
  },
  "include": ["src/**/*.ts", "src/**/*.vue"]
}

Create a Vue Component

For this example, let's create a simple Vue component. You can replace this with any Vue component you need.

<!-- src/components/MyVueComponent.vue -->
<template>
  <div class="vue-component">
    <h3>Hello from Vue, {{ name }}!</h3>
    <p>Count: {{ count }}</p>
    <button @click="increment">Increment</button>
    <button @click="$emit('custom-event', { message: 'Hello from Vue!' })">
      Emit Event
    </button>
  </div>
</template>

<script setup lang="ts">
import { ref } from 'vue';

interface Props {
  name?: string;
  initialCount?: number;
}

const props = withDefaults(defineProps<Props>(), {
  name: 'World',
  initialCount: 0
});

const emit = defineEmits<{
  'custom-event': [payload: { message: string }];
}>();

const count = ref(props.initialCount);

const increment = () => {
  count.value++;
};
</script>

<style scoped>
.vue-component {
  padding: 1rem;
  border: 2px solid #42b883;
  border-radius: 8px;
  background-color: #f9f9f9;
}

button {
  margin: 0.25rem;
  padding: 0.5rem 1rem;
  background-color: #42b883;
  color: white;
  border: none;
  border-radius: 4px;
  cursor: pointer;
}

button:hover {
  background-color: #369870;
}
</style>

This single-file component uses Vue 3's Composition API with <script setup> syntax, TypeScript support, props, events, and scoped styling.

Create an Aurelia Wrapper Component

To integrate the Vue component into Aurelia, create a wrapper Aurelia component that will render the Vue component.

// src/resources/elements/vue-wrapper.ts
import { customElement, bindable } from 'aurelia';
import { createApp, App } from 'vue';
import type { Component } from 'vue';

@customElement({ name: 'vue-wrapper', template: '<template><div ref="container"></div></template>' })
export class VueWrapper {
  @bindable public vueComponent: Component;
  @bindable public props?: Record<string, any>;
  
  private container!: HTMLDivElement;
  private vueApp: App<Element> | null = null;

  public attached(): void {
    if (this.container && this.vueComponent) {
      this.mountVueComponent();
    }
  }

  public propertyChanged(): void {
    if (this.vueApp && this.props) {
      // Vue 3: Remount with new props (or use provide/inject for complex state)
      this.unmountVueComponent();
      this.mountVueComponent();
    }
  }

  public detaching(): void {
    this.unmountVueComponent();
  }

  private mountVueComponent(): void {
    try {
      this.vueApp = createApp(this.vueComponent, {
        ...this.props,
        // Listen to Vue component events
        onCustomEvent: (payload: any) => {
          console.log('Vue component event received:', payload);
          // Dispatch custom event for Aurelia to handle
          this.container.dispatchEvent(new CustomEvent('vue-event', { 
            detail: payload,
            bubbles: true 
          }));
        }
      });

      this.vueApp.mount(this.container);
    } catch (error) {
      console.error('Failed to mount Vue component:', error);
      this.container.innerHTML = '<div>Failed to load Vue component</div>';
    }
  }

  private unmountVueComponent(): void {
    if (this.vueApp) {
      this.vueApp.unmount();
      this.vueApp = null;
    }
  }
}

This wrapper properly handles Vue 3 application lifecycle using Aurelia 2's lifecycle hooks. It supports bindable props that are passed to the Vue component, handles component events, and includes proper error handling and cleanup.

Register the Wrapper Component and Use It

Now, you must register the wrapper component with Aurelia and then use it in your application.

// src/main.ts
import { Aurelia } from 'aurelia';
import { VueWrapper } from './resources/elements/vue-wrapper';
import { MyApp } from './my-app';

Aurelia
  .register(VueWrapper)
  .app(MyApp)
  .start();

Then, use it in a view:

<!-- src/my-view.html -->
<template>
  <vue-wrapper 
    vue-component.bind="myVueComponent"
    props.bind="vueProps"
    vue-event.trigger="handleVueEvent($event)">
  </vue-wrapper>
</template>

Ensure you import and make the Vue component available in your Aurelia component:

// src/my-view.ts
import MyVueComponent from './components/MyVueComponent.vue';

export class MyView {
  public myVueComponent = MyVueComponent;
  public vueProps = { 
    name: 'Aurelia User',
    initialCount: 10
  };

  public handleVueEvent(event: CustomEvent): void {
    console.log('Received event from Vue component:', event.detail);
  }
}

Advanced Integration Patterns

Vue 3 Composables Integration

You can create Vue composables that work within the Aurelia-Vue integration:

// src/composables/useCounter.ts
import { ref, computed } from 'vue';

export function useCounter(initialValue = 0) {
  const count = ref(initialValue);
  
  const doubleCount = computed(() => count.value * 2);
  
  const increment = () => count.value++;
  const decrement = () => count.value--;
  const reset = () => count.value = initialValue;
  
  return {
    count,
    doubleCount,
    increment,
    decrement,
    reset
  };
}

Then use it in your Vue component:

<script setup lang="ts">
import { useCounter } from '../composables/useCounter';

const { count, doubleCount, increment, decrement, reset } = useCounter(5);
</script>

Global Vue Configuration

Configure Vue plugins and global properties in your wrapper:

private mountVueComponent(): void {
  try {
    this.vueApp = createApp(this.vueComponent, this.props);
    
    // Global error handler
    this.vueApp.config.errorHandler = (err, instance, info) => {
      console.error('Vue error:', err, info);
    };
    
    // Global properties
    this.vueApp.config.globalProperties.$aureliaContext = this;
    
    // Install plugins
    // this.vueApp.use(someVuePlugin);
    
    this.vueApp.mount(this.container);
  } catch (error) {
    console.error('Failed to mount Vue component:', error);
    this.container.innerHTML = '<div>Failed to load Vue component</div>';
  }
}

State Management with Pinia

For complex applications, integrate Pinia for Vue state management:

// src/stores/counterStore.ts
import { defineStore } from 'pinia';

export const useCounterStore = defineStore('counter', {
  state: () => ({
    count: 0
  }),
  
  getters: {
    doubleCount: (state) => state.count * 2
  },
  
  actions: {
    increment() {
      this.count++;
    }
  }
});

Then configure Pinia in your wrapper:

import { createPinia } from 'pinia';

private mountVueComponent(): void {
  this.vueApp = createApp(this.vueComponent, this.props);
  this.vueApp.use(createPinia());
  this.vueApp.mount(this.container);
}

Performance Optimizations

For better performance with frequent prop updates, consider using Vue's provide/inject pattern:

private mountVueComponent(): void {
  this.vueApp = createApp(this.vueComponent);
  
  // Provide reactive data instead of remounting
  this.vueApp.provide('aureliaProps', reactive(this.props || {}));
  
  this.vueApp.mount(this.container);
}

public propertyChanged(): void {
  // Update provided data instead of remounting
  if (this.vueApp && this.props) {
    const provided = this.vueApp._instance?.provides?.aureliaProps;
    if (provided) {
      Object.assign(provided, this.props);
    }
  }
}

Vue DevTools Support

Enable Vue DevTools for debugging:

// In development
if (process.env.NODE_ENV === 'development') {
  this.vueApp.config.performance = true;
}

TypeScript Shims

Add Vue SFC type support in src/shims-vue.d.ts:

declare module '*.vue' {
  import type { DefineComponent } from 'vue';
  const component: DefineComponent<{}, {}, any>;
  export default component;
}

Vue 3 Features Compatibility

This integration pattern supports all Vue 3 features:

  • Composition API with <script setup> syntax

  • Multiple root nodes in templates

  • Teleport for portal-like functionality

  • Suspense for async component loading

  • Fragment support for multiple root elements

  • TypeScript with full type safety

Following these steps, you can integrate Vue components into your Aurelia 2 application. This process highlights the flexibility of Aurelia, allowing you to take advantage of Vue's component library while enjoying the benefits of Aurelia's powerful features.

Last updated

Was this helpful?