FlexDeck

A list with the behaviour of Table (loading, empty and error states, search, pagination) and none of its markup: you draw each row.

Source in the kit: composables/useFlexDeck.ts · components/FlexDeck/

Cards with pagination

examples/flexdeck/Cards.vue

Slots #loading-state , #empty-state and #error-state replace the defaults.

0 รายการ จากทั้งหมด 0 รายการ

<template>
  <FlexDeck
    :options="deck"
    container-class="grid gap-3 sm:grid-cols-2 lg:grid-cols-3"
    @pageChange="users.fetchPageChange"
    @search="users.fetchSearch"
  >
    <!-- The default slot draws one row; layout is entirely yours through container-class -->
    <template #default="{ row }">
      <Card>
        <div class="flex items-center gap-3">
          <Avatar :src="(row as IUser).avatar" />
          <div class="min-w-0">
            <p class="truncate font-medium">
              {{ (row as IUser).name }}
            </p>
            <p class="truncate text-sm text-gray-500">
              {{ (row as IUser).email }}
            </p>
          </div>
        </div>
      </Card>
    </template>
  </FlexDeck>
</template>

<script lang="ts" setup>
import { type IUser, createUsersLoader } from '~/loaders/users'

const users = createUsersLoader()

// useFlexDeck is useTable without columns: same loader, same states, same pagination
const deck = useFlexDeck<IUser>({
  repo: users,
  options: {
    isEnabledSearch: true,
  },
})

onMounted(() =>
  users.fetchPage(1, '', {
    params: {
      limit: 6,
    },
  })
)
</script>

loaders/users.ts

import { defineStore } from 'pinia'

// One file per API resource: its types, then one store per loader.
// `defineStore` makes the loader a singleton, so a list page and its edit form share the same state.

export interface IUser {
  id: string
  name: string
  email: string
  role: 'admin' | 'editor' | 'viewer'
  salary: number
  isActive: boolean
  avatar: string
  createdAt: string
}

export interface IRole {
  id: string
  name: string
}

// Paginated CRUD: GET /users, GET /users/:id, POST /users, PUT /users/:id, DELETE /users/:id
export const createUsersLoader = () => {
  const options = useRequestOptions()

  return usePageLoader<IUser>({
    baseURL: '/users',
    getBaseRequestOptions: options.getMock,
  })
}

// The store is the shared instance: a list page and the form that edits it see the same state.
// Call createUsersLoader() instead when a component wants a private one (each docs example does).
export const useUsersLoader = defineStore('users', createUsersLoader)

// A plain array: GET /roles
export const useRolesLoader = defineStore('roles', () => {
  const options = useRequestOptions()

  return useListLoader<IRole>({
    url: '/roles',
    getRequestOptions: options.getMock,
  })
})

// A single object, or any one-off request: GET /profile
export const useProfileLoader = defineStore('profile', () => {
  const options = useRequestOptions()

  return useObjectLoader<IUser & { loadedAt: string }>({
    method: 'get',
    url: '/profile',
    getRequestOptions: options.getMock,
  })
})

Load more

examples/flexdeck/LoadMore.vue

The pattern the apps use for feeds and timelines: fetchNextPage() with its own status.

<template>
  <div class="space-y-3">
    <FlexDeck
      :options="deck"
      container-class="space-y-2"
    >
      <template #default="{ row }">
        <div class="rounded-md bg-gray-50 px-3 py-2 text-sm">
          {{ (row as IUser).name }}
        </div>
      </template>
    </FlexDeck>

    <Button
      v-if="hasMore"
      label="Load more"
      color="neutral"
      variant="outline"
      block
      :loading="users.fetch.statusNextPage.isLoading"
      @click="users.fetchNextPage()"
    />
  </div>
</template>

<script lang="ts" setup>
import { type IUser, createUsersLoader } from '~/loaders/users'

const users = createUsersLoader()

const deck = useFlexDeck<IUser>({
  repo: users,
  options: {
    // The button below replaces the page numbers
    isHidePagination: true,
  },
})

// fetchNextPage() appends the next page to fetch.items and reports progress in fetch.statusNextPage,
// so the list that is already on screen does not flash back to a loading state
const hasMore = computed(() => users.fetch.options.currentPage < users.fetch.options.totalPage)

onMounted(() =>
  users.fetchPage(1, '', {
    params: {
      limit: 5,
    },
  })
)

defineExpose({
  debug: computed(() => ({
    'items.length': users.fetch.items.length,
    'currentPage': users.fetch.options.currentPage,
    'totalPage': users.fetch.options.totalPage,
    'statusNextPage.isLoading': users.fetch.statusNextPage.isLoading,
  })),
})
</script>