vue-skills

repository·main·Indexed 25 days ago

https://github.com/vuejs-ai/skills

Specialized agent skills for AI-assisted Vue 3 development. It provides a set of best practices and guides for the Composition API, TypeScript, Vue Router 4, Pinia, JSX, and testing. Key features include the create-adaptable-composable skill for designing reusable composables using MaybeRef and MaybeRefOrGetter patterns, as well as guidance on class-based and state-driven animations.

Tokens
209.3K
Snippets
628
Records
737
Agent score
83%

What's inside vuejs-ai/skills

  1. Use vue-testing-best-practices skill

    main
    The vue-testing-best-practices skill provides guidance and patterns for Vue.js testing. It covers Vitest, Vue Test Utils, component testing, mocking, testing patterns, and Playwright for E2E testing. Use this skill to resolve common testing issues such as race conditions, Pinia injection errors, and async component rendering.
  2. Fix iOS Select Element Bug by adding a disabled placeholder

    main

    On iOS Safari, if a <select> element's v-model initial value does not match any available <option> value, the browser may visually highlight the first option but will not fire a change event when the user taps it. This leaves the user stuck, unable to actually select the first item.

    To resolve this, always include a disabled placeholder option with an empty string value (value="") and ensure your v-model initial state matches that empty string.

    <script setup>
    import { ref } from 'vue'
    
    // 1. Ensure initial value matches the placeholder value
    const selected = ref('')
    </script>
    
    <template>
      <!-- 2. Add a disabled placeholder option with value="" -->
      <select v-model="selected">
        <option disabled value="">Please select a fruit</option>
        <option value="apple">Apple</option>
        <option value="banana">Banana</option>
        <option value="orange">Orange</option>
      </select>
    </template>
  3. Implement Essential Vue Foundations

    main

    Apply these core principles to every Vue task:

    • Reactivity: Keep source state minimal using ref or reactive. Derive as much as possible using computed. Use watchers only for side effects.
    • SFC Structure: Organize Single File Components in the order: <script><template><style>. Keep templates declarative by moving branching/derivation logic into the script section.
    • Data Flow: Follow the "Props down, Events up" model. Use v-model only for true two-way component contracts. Use provide/inject only for deep-tree dependencies. Ensure all contracts are typed with defineProps and defineEmits.
    • Composables: Extract logic into composables when it is reused, stateful, or side-effect heavy. Separate feature logic from presentational components.
  4. Choose between Node-based and Browser-based test runners

    main

    When testing Vue components, choose your runner based on the testing requirements:

    Node-Based Runner (Vitest + happy-dom/jsdom)

    Best for: Fast CI/CD pipelines, pure logic testing, state management, event emission, and props/slots behavior. Limitation: Cannot test real CSS rendering, computed styles, native browser events (focus, drag, resize), or cookies.

    Vitest Browser Mode

    Required for: Verifying computed CSS styles, CSS transitions/animations, real focus/blur behavior, drag and drop, cookie operations, and viewport-dependent behavior. Tradeoff: Slower execution speed compared to Node-based runners.

  5. Use .prevent to stop default browser actions

    main

    Use the .prevent modifier when you need to call event.preventDefault() to stop the browser's default behavior, such as form submissions or link navigation. Do NOT use .passive when using .prevent.

    <!-- CORRECT: Use .prevent when you need to prevent default -->
    <template>
      <form @submit.prevent="handleSubmit">
        <!-- Correctly prevents form submission -->
      </form>
    </template>
  6. Structure Single-File Components (SFCs)

    main

    Colocate <template>, <script>, and <style> within a single .vue file instead of using separate .js/.ts and .css files. This improves maintainability and tooling support.

    <!-- components/UserCard.vue -->
    <script setup>
    import { computed } from 'vue'
    
    const props = defineProps({
      user: { type: Object, required: true }
    })
    
    const displayName = computed(() =>
      `${props.user.firstName} ${props.user.lastName}`
    )
    </script>
    
    <template>
      <div class="user-card">
        <h3 class="name">{{ displayName }}</h3>
      </div>
    </template>
    
    <style scoped>
    .user-card {
      padding: 1rem;
    }
    
    .name {
      margin: 0;
    }
    </style>
  7. Handle SSR lifecycle hook limitations

    main

    In Server-Side Rendering (SSR) applications, lifecycle hooks like mounted, onMounted, unmounted, and onUnmounted are never called on the server. Only beforeCreate, created, and the Composition API setup() function run during SSR.

    To avoid ReferenceError (e.g., window is not defined) or hydration mismatches, follow these rules:

    1. Browser APIs: Place all code using window, document, or localStorage inside onMounted or mounted.
    2. Critical Data Fetching: Perform essential data fetching in created or setup() so the data is available during the server render.
    3. Environment Guards: If you must access browser APIs in hooks that run on both server and client (like created), wrap them in a check for typeof window !== 'undefined'.
    4. Hydration Mismatches: Ensure the initial state rendered by the server matches the initial state on the client. Use onMounted to update state with client-only data (like current time or window dimensions) after the initial render.
    <script setup>
    import { ref, onMounted, onUnmounted } from 'vue'
    
    const user = ref(null)
    const windowWidth = ref(0)
    
    // This runs on both server and client (during setup)
    user.value = await useFetch('/api/user')
    
    // These only run on client
    onMounted(() => {
      windowWidth.value = window.innerWidth
      window.addEventListener('resize', handleResize)
    })
    
    onUnmounted(() => {
      window.removeEventListener('resize', handleResize)
    })
    
    function handleResize() {
      windowWidth.value = window.innerWidth
    }
    </script>
  8. Handle naming conflicts in self-referencing components

    main

    When using implicit self-reference via filename, an import statement in <script setup> with the same name will override the self-reference.

    To avoid this conflict and still access the imported component, you must alias the import. Once aliased, the original filename will correctly refer back to the current component (self-reference).

    <!-- FooBar.vue -->
    <script setup>
    // Alias the import to avoid overriding the implicit self-reference
    import OtherFooBar from './different/FooBar.vue'
    </script>
    
    <template>
      <!-- Renders the imported component -->
      <OtherFooBar />
      
      <!-- Renders this file (self-reference) -->
      <FooBar />
    </template>
  9. Test Components Using the Blackbox Approach

    main

    To avoid brittle tests that break during refactoring, follow a 'blackbox' testing philosophy: focus on how the component behaves from a user's perspective rather than how it is implemented internally.

    Task Checklist

    • Test what the component does, not how it does it.
    • Query elements by user-visible attributes (text, role, data-testid).
    • Simulate user interactions (e.g., click, type) instead of calling internal methods directly.
    • Assert on rendered output, emitted events, and visible state changes.
    • Avoid accessing internal state (wrapper.vm) or private methods.
    • Use data-testid attributes for elements that lack semantic meaning.
  10. Use naming conventions for private state in Pinia setup stores

    main

    If you need state that is intended to be internal/private, do not omit it from the return object of a setup store. Instead, use an underscore prefix convention (e.g., _authToken). This ensures the state is still available for SSR hydration, DevTools, and plugins, while communicating to other developers that it should not be accessed externally.

    import { defineStore } from 'pinia'
    import { ref, computed } from 'vue'
    
    export const useUserStore = defineStore('user', () => {
      // Convention: underscore prefix for "internal" state
      // Still returned, but signals it's not for external use
      const _authToken = ref('')
      const _lastFetchTime = ref(null)
    
      // Public state
      const name = ref('')
      const email = ref('')
    
      const isLoggedIn = computed(() => !!_authToken.value)
    
      // Return everything - convention communicates intent
      return {
        _authToken,
        _lastFetchTime,
        name,
        email,
        isLoggedIn
      }
    })
  11. Improve Provide/Inject debugging with descriptive Symbol names

    main

    Because provide/inject values are harder to trace in Vue DevTools than props, use Symbol with descriptive string descriptions. This ensures that when errors occur or when inspecting keys, the purpose of the injection is clear.

    // BETTER: Descriptive names appear in errors and debugging
    export const UserAuthKey = Symbol('UserAuthenticationState')
    export const ThemeConfigKey = Symbol('ThemeConfiguration')
    export const FormContextKey = Symbol('FormValidationContext')
    
    // WORSE: Generic names are harder to trace
    export const UserKey = Symbol()
    export const ThemeKey = Symbol('theme')