@shifl-inc/ui

Reusable Vue 3 UI components for Shifl.
5,446
Install
npm install @shifl-inc/ui
Latest Version:0.9.29
Vue:3
License:MIT
Last Updated:Oct 22, 2026
Links: npm
Maintainer: pareshpadhariya

Shifl Grid (Vue 3)

JSON-driven data grid for Shifl UI, built with Vue 3 + TypeScript. Library-first output via Vite (library mode) with Tailwind + CSS variables for theming.

Status

  • ✅ Minimal grid (column visibility toggle, client-side sorting, global filter)
  • ✅ API integration with server-side data fetching
  • ✅ Pagination, search, filters, and sorting via API
  • 🚧 Everything else scaffolded/stubbed for incremental delivery (editing, virtual scroll, tours, views)

Requirements

  • Node >=20.11.0
  • Peer deps: vue@^3.4.0

Install

npm install @shifl-inc/ui

Usage

Setup

// main.ts
import { createApp } from 'vue';
import App from './App.vue';
import { ShiflGridPlugin } from '@shifl-inc/ui';
import '@shifl-inc/ui/style.css';

const app = createApp(App);
app.use(ShiflGridPlugin);
app.mount('#app');

Client-Side Mode (Static Data)

For static data that doesn't require API calls:

<script setup lang="ts">
import { ShiflGrid } from '@shifl-inc/ui';
import type { GridConfig } from '@shifl-inc/ui';

const config: GridConfig = {
  name: 'My Preferred Shipments',
  columns: [
    { key: 'shifl_ref', label: 'Shifl Ref #', sortable: true, freeze: true },
    { key: 'company_name', label: 'Customer', sortable: true }
  ],
  search: ['shifl_ref', 'company_name'],
  sort: { key: 'shifl_ref', order: 'asc' },
  rows: [
    { shifl_ref: 'SF-1001', company_name: 'GMA Inc' },
    { shifl_ref: 'SF-1002', company_name: 'Walads Autowash' }
  ],
  paginationMeta: {
    show: true,
    currentPage: 1,
    perPage: 20,
    total: 2
  }
};
</script>

<template>
  <ShiflGrid :config="config" />
</template>

Server-Side Mode (API Integration)

For dynamic data fetched from an API:

<script setup lang="ts">
import { ShiflGrid } from '@shifl-inc/ui';
import type { GridConfig } from '@shifl-inc/ui';

const config: GridConfig = {
  name: 'My Preferred Shipments',
  apiConfig: {
    apiUrl: 'https://api.example.com/shipments',
    method: 'GET', // Optional: 'GET' | 'POST' | 'PUT' (default: 'GET')
    getAuthToken: () => localStorage.getItem('token'), // Optional: function to get auth token
    headers: {
      // Optional: additional headers
      'X-Custom-Header': 'value'
    },
    additionalParams: {
      // Optional: extra params to send with requests
      module: 'shipments',
      customerId: 123
    },
    debounceMs: 300, // Optional: debounce search queries (default: 300ms)
    responseTransformer: <T,>(response: unknown) => {
      // Optional: transform API response to expected format
      const res = response as {
        data?: T[];
        meta?: {
          current_page?: number;
          per_page?: number;
          total?: number;
        };
      };
      return {
        data: res.data || [],
        meta: res.meta
          ? {
              currentPage: res.meta.current_page,
              perPage: res.meta.per_page,
              total: res.meta.total,
              show: true
            }
          : undefined
      };
    }
  },
  columns: [
    { key: 'shifl_ref', label: 'Shifl Ref #', sortable: true, freeze: true },
    { key: 'company_name', label: 'Customer', sortable: true }
  ],
  search: ['shifl_ref', 'company_name'],
  paginationMeta: {
    show: true,
    currentPage: 1,
    perPage: 20,
    total: 0 // Will be updated from API response
  }
};
</script>

<template>
  <ShiflGrid :config="config" />
</template>

Configuration Options

GridConfig

interface GridConfig<T = Record<string, unknown>> {
  id?: string;
  name?: string;
  customerId?: number;
  module?: string;
  resourceId?: string | number | null;
  creatorId?: number;
  pageLocation?: string;
  view?: 'regular' | 'compact' | 'comfortable';

  // API Configuration (for server-side mode)
  apiConfig?: {
    apiUrl: string; // Required: API endpoint URL
    method?: 'GET' | 'POST' | 'PUT'; // Optional: HTTP method (default: 'GET')
    getAuthToken?: () => string | undefined; // Optional: function to get auth token
    headers?: Record<string, string>; // Optional: additional headers
    additionalParams?: Record<string, unknown> | ((context) => Record<string, unknown>); // Optional: extra params
    paramsBuilder?: (context) => URLSearchParams | Record<string, unknown>; // Optional: custom query params
    bodyBuilder?: (context) => unknown; // Optional: custom request body
    responseTransformer?: <T>(response: unknown) => ApiResponse<T>; // Optional: transform API response
    debounceMs?: number; // Optional: search debounce delay (default: 300ms)
  };

  columns: GridColumn[];
  search?: string[]; // Searchable column keys (for client-side filtering)
  searchMode?: 'client' | 'server'; // Auto-set to 'server' if apiConfig is provided
  filters?: GridFilter[];
  sort?: GridSort;
  paginationMeta?: PaginationMeta;
  rows?: T[]; // Static data (for client-side mode)
}

Client-Side Mode Features

  • Sorting: Client-side only; click column headers to toggle asc/desc
  • Global filter: Simple case-insensitive substring search across listed search fields
  • Visibility: Toggle columns via pills above the grid
  • Pagination: Client-side pagination of static data

Server-Side Mode Features

  • Sorting: Server-side; click column headers to sort via API
  • Search: Debounced search queries sent to API (default: 300ms delay)
  • Filters: Filter values sent as query parameters or in request body
  • Pagination: Handled by server; pagination metadata from API response
  • Authentication: Optional bearer token via getAuthToken function
  • Custom params: Use additionalParams to send extra parameters
  • Pagination state: User's selected page is the source of truth; API's currentPage is informational only

API Response Formats

Default Response Format (Recommended)

The grid works out-of-the-box with this format:

{
  "data": [
    { "id": 1, "name": "Item 1", "status": "active" },
    { "id": 2, "name": "Item 2", "status": "inactive" }
  ],
  "meta": {
    "currentPage": 1,
    "perPage": 20,
    "total": 100,
    "lastPage": 5,
    "from": 1,
    "to": 20,
    "show": true
  }
}

Alternative Default Formats

The default transformer also supports these formats:

Format 1: Using rows instead of data
{
  "rows": [
    { "id": 1, "name": "Item 1" },
    { "id": 2, "name": "Item 2" }
  ],
  "meta": {
    "currentPage": 1,
    "perPage": 20,
    "total": 100
  }
}
Format 2: Data only (no pagination)
{
  "data": [
    { "id": 1, "name": "Item 1" },
    { "id": 2, "name": "Item 2" }
  ]
}

Required Fields

The grid requires these fields in meta for proper pagination:

  • Required for pagination to work:

    • perPage (or per_page in Laravel format) - number of items per page
    • total (total number of records) - used to calculate total pages
  • Optional but recommended:

    • currentPage (or current_page in Laravel format) - informational only, does not override user's selected page
    • lastPage (or last_page) - for better UX
    • from - starting record number
    • to - ending record number
    • show - whether to show pagination (default: true)

Important: Pagination State Behavior

The grid uses the user's pagination state as the source of truth. When a user clicks page 2, the grid will:

  • Display page 2 as active in the UI
  • Send page=2 in the API request
  • Show the correct active page even if the API response returns currentPage: 1

This behavior ensures the pagination UI remains correct even when using mock/dummy APIs that always return currentPage: 1. The API's currentPage value is only used for informational purposes and does not override the user's selection.

Custom Response Transformers

If your API uses a different response format, use the responseTransformer option:

Laravel-Style Pagination Format

const config: GridConfig = {
  apiConfig: {
    apiUrl: 'https://api.example.com/data',
    getAuthToken: () => localStorage.getItem('token'),
    responseTransformer: <T>(response: unknown) => {
      const res = response as {
        data?: T[];
        meta?: {
          current_page?: number;
          per_page?: number;
          total?: number;
          last_page?: number;
          from?: number;
          to?: number;
        };
      };

      return {
        data: res.data || [],
        meta: res.meta
          ? {
              currentPage: res.meta.current_page,
              perPage: res.meta.per_page,
              total: res.meta.total,
              lastPage: res.meta.last_page,
              from: res.meta.from,
              to: res.meta.to,
              show: true
            }
          : undefined
      };
    }
  }
  // ... rest of config
};

Laravel Response Example:

{
  "data": [{ "id": 1, "name": "Item 1" }],
  "meta": {
    "current_page": 1,
    "per_page": 20,
    "total": 100,
    "last_page": 5,
    "from": 1,
    "to": 20
  }
}

REST API Standard Format

const config: GridConfig = {
  apiConfig: {
    apiUrl: 'https://api.example.com/data',
    responseTransformer: <T>(response: unknown) => {
      const res = response as {
        results?: T[];
        items?: T[];
        count?: number;
        page?: number;
        pageSize?: number;
      };

      return {
        data: res.results || res.items || [],
        meta: {
          currentPage: res.page || 1,
          perPage: res.pageSize || 20,
          total: res.count || 0,
          show: true
        }
      };
    }
  }
  // ... rest of config
};

REST Response Example:

{
  "results": [{ "id": 1, "name": "Item 1" }],
  "count": 100,
  "page": 1,
  "pageSize": 20
}

GraphQL-Style Format

const config: GridConfig = {
  apiConfig: {
    apiUrl: 'https://api.example.com/graphql',
    responseTransformer: <T>(response: unknown) => {
      const res = response as {
        data?: {
          items?: T[];
          pagination?: {
            page: number;
            pageSize: number;
            totalCount: number;
          };
        };
      };

      return {
        data: res.data?.items || [],
        meta: res.data?.pagination
          ? {
              currentPage: res.data.pagination.page,
              perPage: res.data.pagination.pageSize,
              total: res.data.pagination.totalCount,
              show: true
            }
          : undefined
      };
    }
  }
  // ... rest of config
};

GraphQL Response Example:

{
  "data": {
    "items": [{ "id": 1, "name": "Item 1" }],
    "pagination": {
      "page": 1,
      "pageSize": 20,
      "totalCount": 100
    }
  }
}

Request Parameters Sent by Grid

The grid automatically sends these parameters:

GET Request (Query Parameters)

GET /api/data?search=query&page=1&per_page=20&sort=name&order=asc&filters[status]=active&filters[type][]=type1&filters[type][]=type2

POST/PUT Request (Body)

{
  "search": "query",
  "page": 1,
  "per_page": 20,
  "sort": "name",
  "order": "asc",
  "filters": {
    "status": "active",
    "type": ["type1", "type2"]
  },
  "module": "shipments",
  "customerId": 123
}

Error Handling

The grid handles errors gracefully. Your API can return standard HTTP error responses:

{
  "error": "Unauthorized",
  "message": "Invalid authentication token"
}

Or with status codes:

  • 400 - Bad Request
  • 401 - Unauthorized
  • 403 - Forbidden
  • 404 - Not Found
  • 500 - Internal Server Error

The grid will display the error and stop loading.

Scripts

  • npm run dev — Vite dev server
  • npm run build — Library build + d.ts via vite-plugin-dts
  • npm run lint — ESLint (Vue + TS) flat config
  • npm run test — Vitest (jsdom)
  • npm run storybook — Storybook dev
  • npm run storybook:build — Storybook static build

Project structure (high level)

  • src/components/grid — ShiflGrid.vue (minimal implementation)
  • src/composables — Core behaviors (sort/filter/columns implemented, others stubbed/TODO)
  • src/types — Grid + theme contracts
  • src/theme — CSS tokens
  • src/styles — Tailwind entry + component styles
  • src/utils — Helpers (config normalization)
  • src/plugins — Vue install plugin
  • stories/ — Storybook stories
  • .storybook/ — Storybook configuration

API Integration

The grid supports server-side data fetching with automatic API calls for search, filters, sorting, and pagination. When apiConfig is provided, the grid automatically:

  • Fetches data from your API endpoint
  • Sends search, filter, sort, and pagination parameters
  • Handles loading states and errors
  • Updates the grid when parameters change
  • Supports bearer token authentication
  • Debounces search queries to reduce API calls
  • Uses user's pagination state as source of truth - the active page displayed is based on user selection, not the API's currentPage response

See API Response Formats for detailed information about:

  • Expected API response formats
  • Custom response transformers
  • Request parameters sent by the grid
  • Error handling
  • Pagination behavior and requirements

Notes

  • Virtual scroll, editing, tours, saved/recommended views, telemetry are intentionally stubbed for later milestones.

Related Packages

@alaarab/ogrid-vue-vuetify

OGrid Vuetify – Vuetify-based data grid with sorting, filtering, pagination, col...

8,711 3
@alaarab/ogrid-vue-radix

OGrid Vue Radix – Lightweight data grid with sorting, filtering, pagination, col...

8,296 3
jl-datatable

A datatable server side rendering mode - Vue JS 3 ⚙️

7,403 8
@jsg-developer/vue-pack

A comprehensive, schema-driven Vue 3 component library built with TypeScript and...

5,772

Version History

Version Released Vue License
0.9.29 ^3.4.0 MIT
0.9.28 ^3.4.0 MIT
0.9.27 ^3.4.0 MIT
0.9.26 ^3.4.0 MIT
0.9.25 ^3.4.0 MIT