UNPKG

@ozmos/viper-vue

Version:

Vue plugin for Viper

286 lines 10.6 kB
import { useMutation, useQuery } from "@tanstack/vue-query"; import { computed, inject, ref, toValue, } from "vue"; export class Page { params = ref({}); formatTitle = (title) => title; props = {}; actions = {}; hashes = {}; queryClient; headerFunctions = []; constructor(config) { this.queryClient = config.queryClient; if (config.formatTitle) { this.formatTitle = config.formatTitle; } } mergeHeaders(func) { this.headerFunctions.push(func); return () => { this.headerFunctions = this.headerFunctions.filter((f) => f !== func); }; } async getHeaders() { const headers = await Promise.all(this.headerFunctions.map((func) => func())); return headers.reduce((acc, header) => ({ ...acc, ...header }), {}); } updateFromPageJson(json) { this.props = json.props; this.actions = json.actions; this.hashes = json.hashes; this.setPageTitle(json.title || ""); this.params.value = json.params; for (const key in this.props) { this.queryClient.setQueryData([this.hashes[key]], () => this.props[key]); } } setPageTitle(title) { document.title = this.formatTitle(title || ""); } } export function usePage() { const page = inject("viperPage"); async function viperFetch({ bind, body, headers, method = "GET", qs = {}, }) { const boundHeaders = {}; const bindKeys = []; const bindValues = []; for (const [key, value] of Object.entries(bind ?? {})) { if (toValue(value)) { bindKeys.push(key); bindValues.push(toValue(value)); } } if (bindKeys.length > 0) { boundHeaders["X-Viper-Bind-Keys"] = bindKeys.join(","); boundHeaders["X-Viper-Bind-Values"] = bindValues.join(","); } const url = new URL(window.location.href); if (qs) { for (const [key, value] of Object.entries(qs)) { const raw = toValue(value); if (Array.isArray(raw)) { for (const item of raw) { url.searchParams.append(`${key}[]`, String(item)); } } else { url.searchParams.set(key, String(raw)); } } } return fetch(url.toString(), { method, credentials: "include", headers: { ...(await page.getHeaders()), ...headers, ...boundHeaders, ...(body instanceof FormData ? {} : { "Content-Type": "application/json", }), Accept: "application/json", "X-Viper-Request": "true", "X-XSRF-TOKEN": decodeURIComponent(getXsrfToken() || ""), }, body, }); } return { setPageTitle: page.setPageTitle, params: page.params, useQuery(key, ...args) { const options = args[0]; const { bind, qs, ...opts } = options ?? {}; const enabled = ref(false); const queryKey = computed(() => { return [ page.hashes[key], ...Object.entries(bind ?? {}) .map(([key, value]) => `bind:${key}:${toValue(value)}`) .filter(Boolean), ...(qs ? Object.entries(qs).map(([key, value]) => `qs:${key}:${toValue(value)}`) : []), ]; }); const query = useQuery({ ...opts, enabled, initialData: () => page.props[key], queryKey, queryFn: async () => { const res = await viperFetch({ bind, headers: { "X-Viper-Only": key, }, qs, }); enabled.value = true; if (!res.ok) { throw await res.json(); } const data = await res.json(); return data.props[key]; }, }); // Trust that initialData makes this non-null and preserve TanStack Query's return type return { ...query, data: query.data, }; }, useMutation(key, ...args) { const options = args[0] || { bind: {} }; const { bind, ...mutationOptions } = options; return useMutation({ ...mutationOptions, mutationFn: async (data = {}) => { const res = await viperFetch({ method: "POST", bind, body: data instanceof FormData ? data : JSON.stringify(data), headers: { ...(data instanceof FormData ? {} : { "Content-Type": "application/json" }), Accept: "application/json", "X-Viper-Action": key, }, }); if (res.status === 422) { const data = (await res.json()); throw data.errors; } if (!res.ok) { throw await res.json(); } return (await res.json()); }, }); }, useForm(key, ...args) { const options = args[0] || {}; const { bind, state: initialState, ...mutationOptions } = options; const _initialState = { ...toValue(initialState ?? {}) }; const state = ref(initialState); const errors = ref({}); const mutation = useMutation({ ...mutationOptions, mutationFn: async (data = {}) => { errors.value = {}; const res = await viperFetch({ method: "POST", bind, body: JSON.stringify({ ...toValue(state), ...toValue(data ?? {}), }), headers: { "X-Viper-Action": key, }, }); if (res.status === 422) { const data = (await res.json()); errors.value = Object.entries(data.errors).reduce((acc, [key, value]) => ({ ...acc, [key]: value[0] }), {}); throw data.errors; } if (!res.ok) { throw await res.json(); } return (await res.json()); }, }); return { ...mutation, mutate(override = {}) { return mutation.mutate(override); }, mutateAsync(override = {}) { return mutation.mutateAsync(override); }, reset: () => { state.value = { ..._initialState }; }, errors, state, }; }, useFormData(key, ...args) { const options = args[0] || {}; const { bind, state: initialState, files, ...mutationOptions } = options; const _initialState = { ...toValue(initialState ?? {}) }; const state = ref(initialState); const errors = ref({}); const mutation = useMutation({ ...mutationOptions, mutationFn: async (data = {}) => { errors.value = {}; const json = { ...toValue(state), ...toValue(data ?? {}), }; const formData = new FormData(); for (const key of files) { if (Array.isArray(json[key])) { for (const file of json[key]) { formData.append(`${key}[]`, file); } } else if (json[key]) { formData.set(key, json[key]); } delete json[key]; } formData.set("state", JSON.stringify(json)); const res = await viperFetch({ method: "POST", body: formData, bind, headers: { "X-Viper-Action": key, }, }); if (res.status === 422) { const data = (await res.json()); errors.value = Object.entries(data.errors).reduce((acc, [key, value]) => ({ ...acc, [key]: value[0] }), {}); throw data.errors; } if (!res.ok) { throw await res.json(); } return (await res.json()); }, }); return { ...mutation, mutate(override = {}) { return mutation.mutate(override); }, mutateAsync(override = {}) { return mutation.mutateAsync(override); }, reset: () => { state.value = { ..._initialState }; }, errors, state, }; }, }; } function getXsrfToken() { const cookies = document.cookie.split(";"); for (let i = 0; i < cookies.length; i++) { const cookie = cookies[i].trim(); if (cookie.startsWith("XSRF-TOKEN=")) { return cookie.substring("XSRF-TOKEN=".length, cookie.length); } } return null; } //# sourceMappingURL=page.js.map