Globalny stan aplikacji w vue, inertia z przekazywaniem i aktualizacją stanu z komponentu.
// 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,
}
}<!-- 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><!-- 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>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')