Skip to content
Back to skills

031 Vue 91388a4b

ASecurity

```vue <script setup lang="ts"> // Simple props interface Props { title: string count?: number items: string[] }

  • 9 stars
  • 0 votes
  • 0 copies
  • 0 views
  • Added October 11, 2026
developmenttypescriptgovueperformance

Works with

  • cli

Security analysis

A100/100

Scanned October 11, 2026

npx -y skills add tools-only/X-Skills --skill 031-vue_91388a4b --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of 031 Vue 91388a4b?

Add the live security badge to your README. It updates with every re-scan.

Security grade badge for 031 Vue 91388a4b
[![Security: A — Skills Directory](https://www.skillsdirectory.com/api/skills/tools-only-031-vue-91388a4b/badge)](https://www.skillsdirectory.com/skills/tools-only-031-vue-91388a4b)

More formats (shields.io, HTML) on the badges page. Keep it an A: scan every change in CI with Pro.

SKILL.md
# Components

## Props with TypeScript

```vue
<script setup lang="ts">
// Simple props
interface Props {
  title: string
  count?: number
  items: string[]
}

const props = defineProps<Props>()

// Props with defaults
const propsWithDefaults = withDefaults(defineProps<Props>(), {
  count: 0,
  items: () => []
})

// Runtime props (without TypeScript)
const runtimeProps = defineProps({
  title: {
    type: String,
    required: true
  },
  count: {
    type: Number,
    default: 0,
    validator: (value: number) => value >= 0
  },
  items: {
    type: Array as PropType<string[]>,
    default: () => []
  }
})

// Access props
console.log(props.title)
console.log(props.count)
</script>

<template>
  <div>
    <h1>{{ title }}</h1>
    <p>Count: {{ count }}</p>
  </div>
</template>
```

## Emits (Events)

```vue
<script setup lang="ts">
// TypeScript emits
interface Emits {
  (e: 'update', value: string): void
  (e: 'delete', id: number): void
  (e: 'submit', payload: { name: string; email: string }): void
}

const emit = defineEmits<Emits>()

// Emit events
function handleUpdate() {
  emit('update', 'new value')
}

function handleDelete(id: number) {
  emit('delete', id)
}

function handleSubmit() {
  emit('submit', { name: 'John', email: 'john@example.com' })
}

// Runtime emits with validation
const runtimeEmit = defineEmits({
  update: (value: string) => {
    return value.length > 0
  },
  delete: (id: number) => {
    return id > 0
  }
})
</script>

<template>
  <button @click="handleUpdate">Update</button>
  <button @click="handleDelete(123)">Delete</button>
</template>
```

## v-model (Two-way Binding)

```vue
<!-- Parent Component -->
<script setup lang="ts">
import { ref } from 'vue'
import CustomInput from './CustomInput.vue'

const searchQuery = ref('')
const filters = ref({ category: '', price: 0 })
</script>

<template>
  <!-- Single v-model -->
  <CustomInput v-model="searchQuery" />

  <!-- Multiple v-models -->
  <FilterPanel
    v-model:category="filters.category"
    v-model:price="filters.price"
  />
</template>

<!-- CustomInput.vue -->
<script setup lang="ts">
interface Props {
  modelValue: string
}

interface Emits {
  (e: 'update:modelValue', value: string): void
}

const props = defineProps<Props>()
const emit = defineEmits<Emits>()

function handleInput(event: Event) {
  const target = event.target as HTMLInputElement
  emit('update:modelValue', target.value)
}
</script>

<template>
  <input :value="modelValue" @input="handleInput" />
</template>

<!-- FilterPanel.vue with multiple v-models -->
<script setup lang="ts">
interface Props {
  category: string
  price: number
}

interface Emits {
  (e: 'update:category', value: string): void
  (e: 'update:price', value: number): void
}

const props = defineProps<Props>()
const emit = defineEmits<Emits>()
</script>

<template>
  <select
    :value="category"
    @change="emit('update:category', ($event.target as HTMLSelectElement).value)"
  >
    <option value="books">Books</option>
    <option value="electronics">Electronics</option>
  </select>
  <input
    type="number"
    :value="price"
    @input="emit('update:price', Number(($event.target as HTMLInputElement).value))"
  />
</template>
```

## Slots

```vue
<!-- Parent Component -->
<template>
  <Card>
    <template #header>
      <h2>Card Title</h2>
    </template>

    <template #default>
      <p>Main content goes here</p>
    </template>

    <template #footer="{ close }">
      <button @click="close">Close</button>
    </template>
  </Card>
</template>

<!-- Card.vue -->
<script setup lang="ts">
import { useSlots } from 'vue'

const slots = useSlots()

// Check if slot exists
const hasHeader = !!slots.header
const hasFooter = !!slots.footer

function close() {
  console.log('Closing card')
}
</script>

<template>
  <div class="card">
    <div v-if="hasHeader" class="card-header">
      <slot name="header"></slot>
    </div>

    <div class="card-body">
      <slot></slot> <!-- Default slot -->
    </div>

    <div v-if="hasFooter" class="card-footer">
      <slot name="footer" :close="close"></slot> <!-- Scoped slot -->
    </div>
  </div>
</template>
```

## Scoped Slots (Advanced)

```vue
<!-- List Component with Scoped Slot -->
<script setup lang="ts" generic="T">
interface Props {
  items: T[]
}

const props = defineProps<Props>()
</script>

<template>
  <div class="list">
    <div v-for="(item, index) in items" :key="index">
      <slot :item="item" :index="index"></slot>
    </div>
  </div>
</template>

<!-- Usage -->
<template>
  <List :items="users">
    <template #default="{ item, index }">
      <div>{{ index }}: {{ item.name }}</div>
    </template>
  </List>
</template>
```

## Provide/Inject

```vue
<!-- Parent Component (Provider) -->
<script setup lang="ts">
import { provide, ref, readonly, InjectionKey } from 'vue'

// Type-safe injection key
interface UserData {
  name: string
  email: string
}

export const userKey = Symbol() as InjectionKey<UserData>

const user = ref<UserData>({
  name: 'John Doe',
  email: 'john@example.com'
})

function updateUser(newUser: UserData) {
  user.value = newUser
}

// Provide data
provide(userKey, readonly(user.value))
provide('updateUser', updateUser)
</script>

<!-- Child Component (Injector) -->
<script setup lang="ts">
import { inject } from 'vue'
import { userKey } from './Parent.vue'

// Inject with type safety
const user = inject(userKey)
const updateUser = inject<(user: UserData) => void>('updateUser')

// Inject with default value
const theme = inject('theme', 'light')

function handleUpdate() {
  if (updateUser) {
    updateUser({ name: 'Jane', email: 'jane@example.com' })
  }
}
</script>

<template>
  <div>
    <p>User: {{ user?.name }}</p>
    <p>Theme: {{ theme }}</p>
    <button @click="handleUpdate">Update User</button>
  </div>
</template>
```

## Teleport

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

const showModal = ref(false)
</script>

<template>
  <button @click="showModal = true">Show Modal</button>

  <!-- Teleport to body -->
  <Teleport to="body">
    <div v-if="showModal" class="modal">
      <div class="modal-content">
        <h2>Modal Title</h2>
        <p>Modal content</p>
        <button @click="showModal = false">Close</button>
      </div>
    </div>
  </Teleport>

  <!-- Teleport to specific element -->
  <Teleport to="#modal-container">
    <div class="notification">Notification message</div>
  </Teleport>

  <!-- Conditional teleport -->
  <Teleport to="body" :disabled="!isMobile">
    <div>Only teleported on mobile</div>
  </Teleport>
</template>

<style scoped>
.modal {
  position: fixed;
  top: 0;
  left: 0;
  width: 100%;
  height: 100%;
  background: rgba(0, 0, 0, 0.5);
  display: flex;
  align-items: center;
  justify-content: center;
}

.modal-content {
  background: white;
  padding: 2rem;
  border-radius: 8px;
}
</style>
```

## Dynamic Components

```vue
<script setup lang="ts">
import { ref, shallowRef, Component } from 'vue'
import HomeView from './HomeView.vue'
import AboutView from './AboutView.vue'
import ContactView from './ContactView.vue'

// Use shallowRef for component references (performance)
const currentView = shallowRef<Component>(HomeView)

const components = {
  home: HomeView,
  about: AboutView,
  contact: ContactView
}

function switchView(view: keyof typeof components) {
  currentView.value = components[view]
}
</script>

<template>
  <button @click="switchView('home')">Home</button>
  <button @click="switchView('about')">About</button>
  <button @click="switchView('contact')">Contact</button>

  <!-- Dynamic component with KeepAlive -->
  <KeepAlive>
    <component :is="currentView" />
  </KeepAlive>
</template>
```

## Async Components

```vue
<script setup lang="ts">
import { defineAsyncComponent } from 'vue'

// Lazy load component
const HeavyComponent = defineAsyncComponent(() =>
  import('./HeavyComponent.vue')
)

// With loading and error states
const AdminPanel = defineAsyncComponent({
  loader: () => import('./AdminPanel.vue'),
  loadingComponent: () => import('./LoadingSpinner.vue'),
  errorComponent: () => import('./ErrorDisplay.vue'),
  delay: 200, // Delay before showing loading component
  timeout: 3000 // Timeout before showing error
})
</script>

<template>
  <Suspense>
    <template #default>
      <HeavyComponent />
    </template>
    <template #fallback>
      <div>Loading...</div>
    </template>
  </Suspense>
</template>
```

## Quick Reference

| Pattern | Use Case |
|---------|----------|
| `defineProps<T>()` | Type-safe props with TypeScript |
| `withDefaults()` | Props with default values |
| `defineEmits<T>()` | Type-safe event emitters |
| `v-model` | Two-way data binding |
| `<slot>` | Content distribution |
| Scoped slots | Pass data from child to parent |
| `provide/inject` | Dependency injection (avoid prop drilling) |
| `<Teleport>` | Render DOM outside component hierarchy |
| `<component :is>` | Dynamic component switching |
| `defineAsyncComponent()` | Lazy load components |

Attribution

Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.

Comments

Loading comments…

Newsletter

Get the weekly list of safe new skills, and the ones we pulled.