Skip to content

Vue 3 Integration Example

Complete Vue 3 Composition API integration examples with reusable composables.

Installation

bash
npm install bindra vue
# or
pnpm add bindra vue

Composables

Core Composable: useBindra

The main composable for integrating Bindra with Vue 3:

typescript
import { useBindra } from './composables';

const { data, loading, create, update, remove } = useBindra<User>({
  url: '/api/users',
  cache: { enabled: true }
});

Returns:

  • data - Reactive ref with data array
  • current - Current record (for navigation)
  • loading - Loading state
  • error - Error state
  • create(record) - Create operation
  • update(id, changes) - Update operation
  • remove(id) - Delete operation
  • refresh() - Refresh data
  • dataSource - Direct DataSource access

Pagination: useBindraPagination

Composable for paginated data:

typescript
import { useBindraPagination } from './composables';

const {
  data,
  hasMore,
  loadMore,
  currentPage,
  totalPages
} = useBindraPagination<Product>({
  url: '/api/products',
  pagination: { enabled: true, pageSize: 20 }
});

Additional Returns:

  • hasMore - More pages available
  • currentPage - Current page number
  • totalPages - Total pages
  • totalRecords - Total records count
  • loadMore() - Load next page
  • loadPage(page) - Jump to specific page

Real-time: useBindraRealtime

Composable for WebSocket/real-time features:

typescript
import { useBindraRealtime } from './composables';

const {
  data,
  connectionStatus,
  sendMessage
} = useBindraRealtime<Message>({
  url: '/api/messages',
  realtime: {
    enabled: true,
    url: 'wss://api.example.com/ws'
  }
});

Additional Returns:

  • connectionStatus - Connection state ('connecting' | 'connected' | 'disconnected' | 'error')
  • sendMessage(data) - Send WebSocket message

Search: useBindraSearch

Composable with debounced search:

typescript
import { useBindraSearch } from './composables';

const {
  data,
  search,
  searchQuery
} = useBindraSearch<Product>({
  url: '/api/products'
}, 300); // 300ms debounce

Validation: useBindraValidation

Composable for form validation:

typescript
import { useBindraValidation } from './composables';

const { errors, validate, clearErrors } = useBindraValidation(dataSource);

const submit = async () => {
  if (validate(formData)) {
    await create(formData);
  }
};

Examples

1. Todo List

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

interface Todo {
  id: number;
  title: string;
  completed: boolean;
}

const { data: todos, create, update, remove } = useBindra<Todo>({
  url: '/api/todos'
});

const newTodo = ref('');

const addTodo = async () => {
  await create({ title: newTodo.value, completed: false });
  newTodo.value = '';
};

const toggleTodo = async (todo: Todo) => {
  await update(todo.id, { completed: !todo.completed });
};
</script>

<template>
  <div>
    <input v-model="newTodo" @keyup.enter="addTodo" />
    <button @click="addTodo">Add</button>
    
    <ul>
      <li v-for="todo in todos" :key="todo.id">
        <input
          type="checkbox"
          :checked="todo.completed"
          @change="toggleTodo(todo)"
        />
        {{ todo.title }}
        <button @click="remove(todo.id)">Delete</button>
      </li>
    </ul>
  </div>
</template>

2. Searchable Product List

vue
<script setup lang="ts">
import { ref, watch } from 'vue';
import { useBindraPagination } from './composables';
import { debounce } from 'bindra';

const {
  data: products,
  hasMore,
  loadMore,
  dataSource
} = useBindraPagination({
  url: '/api/products',
  pagination: { enabled: true, pageSize: 20 }
});

const searchQuery = ref('');

const debouncedSearch = debounce((query: string) => {
  dataSource.fetch({ search: query });
}, 300);

watch(searchQuery, (query) => {
  debouncedSearch(query);
});
</script>

<template>
  <div>
    <input v-model="searchQuery" placeholder="Search..." />
    
    <div v-for="product in products" :key="product.id">
      {{ product.name }} - ${{ product.price }}
    </div>
    
    <button v-if="hasMore" @click="loadMore">
      Load More
    </button>
  </div>
</template>

3. Real-time Chat

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

const {
  data: messages,
  connectionStatus,
  create
} = useBindraRealtime({
  url: '/api/messages',
  realtime: {
    enabled: true,
    url: 'wss://api.example.com/ws'
  }
});

const messageText = ref('');

const sendMessage = async () => {
  await create({
    text: messageText.value,
    userId: currentUser.id,
    timestamp: new Date()
  });
  messageText.value = '';
};
</script>

<template>
  <div>
    <div :class="connectionStatus">
      Status: {{ connectionStatus }}
    </div>
    
    <div v-for="msg in messages" :key="msg.id">
      <strong>{{ msg.username }}:</strong> {{ msg.text }}
    </div>
    
    <input
      v-model="messageText"
      @keyup.enter="sendMessage"
      :disabled="connectionStatus !== 'connected'"
    />
  </div>
</template>

4. Form with Validation

vue
<script setup lang="ts">
import { ref } from 'vue';
import { useBindra, useBindraValidation } from './composables';

const { create, dataSource } = useBindra({
  url: '/api/users',
  validation: {
    name: [
      { type: 'required', message: 'Name required' },
      { type: 'minLength', value: 3 }
    ],
    email: [
      { type: 'required' },
      { type: 'email' }
    ]
  }
});

const { errors, validate, clearErrors } = useBindraValidation(dataSource);

const formData = ref({ name: '', email: '', age: 0 });

const submit = async () => {
  if (validate(formData.value)) {
    await create(formData.value);
    formData.value = { name: '', email: '', age: 0 };
    clearErrors();
  }
};
</script>

<template>
  <form @submit.prevent="submit">
    <div>
      <input v-model="formData.name" placeholder="Name" />
      <span v-if="errors.name" class="error">{{ errors.name }}</span>
    </div>
    
    <div>
      <input v-model="formData.email" type="email" placeholder="Email" />
      <span v-if="errors.email" class="error">{{ errors.email }}</span>
    </div>
    
    <button type="submit">Submit</button>
  </form>
</template>

Components

ProductList.vue

Searchable product list with infinite scroll pagination.

Features:

  • Debounced search
  • Category filtering
  • Infinite scroll
  • Load more button
  • Responsive grid layout

RealtimeChat.vue

Real-time chat component with WebSocket.

Features:

  • WebSocket connection
  • Real-time message updates
  • Connection status indicator
  • Auto-scroll to bottom
  • Message timestamps

Features Demonstrated

  • ✅ Reactive state management
  • ✅ Automatic cleanup (onUnmounted)
  • ✅ TypeScript support
  • ✅ Pagination (infinite scroll)
  • ✅ Search with debounce
  • ✅ Real-time updates (WebSocket)
  • ✅ Form validation
  • ✅ Error handling
  • ✅ Loading states
  • ✅ Optimistic updates

Best Practices

1. Cleanup

Composables automatically clean up subscriptions:

typescript
onUnmounted(() => {
  unsubscribers.forEach(unsub => unsub());
  dataSource.disconnectWebSocket?.();
});

2. Error Handling

All operations catch errors:

typescript
const create = async (record: Partial<T>) => {
  try {
    return await dataSource.create(record);
  } catch (err) {
    error.value = err as Error;
    throw err;
  }
};

3. Reactive State

Use Vue refs for reactive state:

typescript
const data = ref<T[]>([]);
const loading = ref(false);

Always debounce search inputs:

typescript
const debouncedSearch = debounce((query) => {
  dataSource.fetch({ search: query });
}, 300);

5. Type Safety

Always use TypeScript generics:

typescript
const { data } = useBindra<User>({ url: '/api/users' });
// data is Ref<User[]>

Project Setup

Install Dependencies

bash
npm install bindra vue
npm install -D @vue/compiler-sfc typescript

Vite Config

typescript
// vite.config.ts
import { defineConfig } from 'vite';
import vue from '@vitejs/plugin-vue';

export default defineConfig({
  plugins: [vue()]
});

TypeScript Config

json
{
  "compilerOptions": {
    "target": "ES2020",
    "module": "ESNext",
    "moduleResolution": "bundler",
    "strict": true,
    "jsx": "preserve"
  }
}

Learn More

Released under the MIT License.