Skip to content

Instantly share code, notes, and snippets.

@atomjoy
Created September 12, 2026 10:01
Show Gist options
  • Select an option

  • Save atomjoy/63ff844323fa5f4c04e24e18fdbb9bb6 to your computer and use it in GitHub Desktop.

Select an option

Save atomjoy/63ff844323fa5f4c04e24e18fdbb9bb6 to your computer and use it in GitHub Desktop.
Vue table state watcher

Vue Table Filters Watcher

Globalny stan aplikacji w vue, inertia z przekazywaniem i aktualizacją stanu z komponentu.

Composable watcher

// composable/useTableWatcher.ts
import { watch, type Ref } from 'vue'
import { router } from '@inertiajs/vue3'
// import pickBy from 'lodash/pickBy'

interface TableState extends Record<string, any> {
    page?: number
    limit?: number
    search?: string
    roles?: Array<string>
    // Wymusi sprawdzenie
    // [key: string]: Record<string, unknown>
    // Wszystkie typy
    // [key: string]: any
}

interface UseTableWatcherOptions {
    debounceMs?: number
}

export function useTableWatcher<T extends TableState>(
    tableState: Ref<T>,
    options: UseTableWatcherOptions = { debounceMs: 400 },
) {
    let timeoutId: ReturnType<typeof setTimeout> | null = null

    const updateUrlFromState = (state: T) => {
        // const cleanQuery = pickBy(state,value => value !== '' && value !== null && value !== undefined)

        const cleanQuery = Object.fromEntries(
            Object.entries(state).filter(([_, value]) => {
                if (Array.isArray(value)) {
                    return value.length > 0
                }
                return value !== '' && value !== null && value !== undefined
            }),
        )

        router.get(
            window.location.pathname, // Bieżąca ścieżka
            cleanQuery,
            {
                preserveState: true,
                preserveScroll: true,
                replace: true,
                // only: ['component'], // Opcjonalnie partial refresh
            },
        )
    }

    watch(
        () => tableState.value, // Lub sklonuj tableState jeżeli debounce nie działa
        newState => {
            if (timeoutId) clearTimeout(timeoutId)
            timeoutId = setTimeout(() => {
                updateUrlFromState(newState)
            }, options.debounceMs)
        },
        { deep: true },
    )

    return {
        updateUrlFromState,
    }
}

Homepage

<!-- MainPage.vue -->
<script setup lang="ts">
    import { ref } from 'vue'
    import { useTableWatcher } from '@/composables/useTableWatcher'
    import TableFilters from '@/components/TableFilters.vue'

    const props = defineProps<{
        filters: {
            search?: string
            page?: number
            roles?: Array<string>
        }
    }>()

    const tableState = ref({
        search: props.filters.search || '',
        page: props.filters.page || 1,
        roles: props.filters.roles || [],
    })

    useTableWatcher(tableState, { debounceMs: 400 })
</script>

<template>
    <div>
        <TableFilters v-model="tableState" />
    </div>
</template>

Filters komponent

<!-- TableFilters.vue -->
<script setup lang="ts">
    const model = defineModel<{
        search: string
        page: number
    }>({ required: true })

    const nextPage = () => {
        model.value.page++
    }

    const toggleRole = (name: string) => {
        const index = model.value.roles.indexOf(name)
        if (index === -1) {
            model.value.roles.push(name)
        } else {
            model.value.roles.splice(index, 1)
        }
    }
</script>

<template>
    <div class="p-6">
        <input
            v-model="model.search"
            placeholder="Szukaj użytkownika..."
            class="border p-2 rounded w-64"
        />

        <div class="mt-4">
            <button @click="nextPage" class="px-4 py-2 bg-gray-600 text-white rounded">
                Strona: {{ model.page }}
            </button>
        </div>

        <div class="mt-4">
            <button
                @click="toggleRole('moderator')"
                class="px-4 py-2 bg-gray-600 text-white rounded"
            >
                Moderator
            </button>
        </div>

        <div class="mt-4 flex gap-4">
            <label> <input type="checkbox" v-model="model.roles" value="admin" /> Admin </label>
            <label> <input type="checkbox" v-model="model.roles" value="editor" /> Editor </label>
        </div>
    </div>
</template>

Api

Wymuszanie sprawdzania typów z Record<string, unknown>.

// Definiujesz bezpieczny typ dla nieznanych danych z API
type ApiPayload = Record<string, unknown>

// Funkcja, która sprawdza, czy dany obiekt ma tablicę stringów
function hasStringRoles(obj: ApiPayload): obj is { roles: string[] } {
    return (
        'roles' in obj &&
        Array.isArray(obj.roles) &&
        obj.roles.every(role => typeof role === 'string')
    )
}

// 1. Użycie w kodzie
const handleApiResponse = (data: ApiPayload) => {
    // data.roles.push('admin'); // ❌ Błąd TS: Obiekt jest typu 'unknown'
    if (hasStringRoles(data)) {
        // 🎉 Wewnątrz tego bloku TS wie, że data.roles to string[]
        data.roles.push('admin')
        console.log('Dodano rolę:', data.roles)
    } else {
        console.error('Dane nie zawierają poprawnej tablicy ról!')
    }
}

// 2.Można prościej
const data: Record<string, unknown> = { roles: ['user', 'editor'] }
// Rzutujesz 'unknown' na konkretny typ
const userRoles = data.roles as string[]
// Działa bez błędów
userRoles.push('admin')
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment