Composables în Vue pentru Ajax submit form

Încerc să înțeleg cum aș putea folosi composables și cred că ori am așteptări greșite ori înțeleg ceva greșit.

Un exemplu minimal este:

<script setup>
const isBusy = ref(false)
const errors = ref(false)

const handleSubmit = async () => {
  isBusy.value = true;

  const response = await fetch('....', { new formData('...etc...') } );
  const json = await response.json();

  isBusy.value = false;
  if (!response.ok) errors.value = json.errors
}
</script>

<template>
  <div v-if="isBusy">Saving</div>
  <div v-if="errors">{{ errors }}</div>
  <form action="" @submit="handleSubmit">
    <input type="text" name="foo">
    <button type="submit">go</button>
  </form>
</template>

Treaba asta merge, dar pentru că folosesc în mai multe locuri, aș vrea să decuplez cumva. Iar în manual este fix situația asta tratată aici.

OK, exemplul din manual funcționează dacă fac request când se folosește componenta.

Dar nu-mi dau seama cum aș putea adapta să se întâmple request-ul la submit, păstrând totuși posibilitatea de a avea erorrile & loading state.

Urmând mai departe exemplul din manual, ar însemna să fac o variabilă reactivă, să zicem endpoint = ref(null) iar la submit să setez endpoint.value la ce am eu nevoie?

Nu exista ceva librarie standard de forms pentru Vue ? Probabil iti rezolva problema.

Înainte să folosesc un 3rd party aș vrea foarte mult să înțeleg cum funcționează…

Uita-te la codul sursa la third party ca sa vezi ce pattern se foloseste in vue.
Eu intuiesc ca trebuie sa dai de mai sus handlerul pentru request si isLoading/errors in componenta de form.

Da fapt am inteles, daca vrei sa tii mai sus starea trebuie sa il setezi undeva mai sus si sa il sincronizezi din hook/composable.

Corecturi și reformulări:

Am trecut prin asta. Pentru noi, cea mai bună soluție a fost utilizarea https://pinia.vuejs.org/, unde, în principiu, am definit atât starea componentei, cât și metodele care efectuau apeluri către SDK-ul de API.

Prin adoptarea acestei abordări, componente nu mai conțineau:

  • stări de date din surse externe (remote)
  • metode de apelare directă a SDK-ului

Un avantaj suplimentar este că, prin utilizarea Pinia, poți folosi plugin-uri, de exemplu, pentru a implementa persistența în localstorage pentru stări.

De asemenea, pentru store-urile Pinia, poți crea factory-uri care să genereze store-uri precompletate cu pattern-uri de formulare, inclusiv starea formData, erori, etc.

Un exemplu de astfel de factory. Posibil ca acest cod sa nu compilze. You get the idea

import { defineStore } from 'pinia';

interface BaseStoreState {
  initialized: boolean;
  loading: boolean;
  errors: Record<string, string>;
  id: null | number;
}

type StoreState<S> = BaseStoreState & S;

type StoreGetters<G> = {
  hasLoading(state: StoreState<G>): boolean;
  loadedItem(state: StoreState<G>): G['item'];
};

type StoreActions<A, S> = {
  setGeneralError(error: string): void;
  reject(error: string): Promise<void>;
  reset(): void;
  action(promise: () => Promise<any>, needsId?: boolean): Promise<any>;
  load(item: S['item']): void;
} & A;

export const storeContextActionFactory = <S, A, G>(
  name: string,
  state: Partial<BaseStoreState> & S,
  actions = {} as Partial<StoreActions<A, S>> & A,
  getters = {} as Partial<StoreGetters<G>> & G,
  extra = {},
) => {
  return defineStore(name, {
    state: () => ({
      name,
      item: {} as S['item'],
      initialized: false,
      loading: false,
      errors: {},
      id: null,
      ...state,
    }) as StoreState<S>,

    getters: {
      hasLoading: (state): boolean => {
        return state.loading;
      },
      loadedItem: (state): S['item'] => {
        return state.item;
      },
      ...getters,
    } as StoreGetters<G>,

    actions: {
      setGeneralError(this: StoreActions<A, S>, error: string) {
        this.errors.general = error;
      },
      reject(this: StoreActions<A, S>, error: string): Promise<void> {
        this.setGeneralError(error);
        return Promise.reject(this.errors);
      },
      reset(this: StoreActions<A, S>) {
        console.log(`### resetting ${name} store`);
        this.$reset();
      },
      action(this: StoreActions<A, S>, promise: () => Promise<any>, needsId = true): Promise<any> {
        console.log(`### action called on ${name} store`);
        return new Promise((resolve, reject) => {
          if (this.hasLoading) {
            console.warn(`Other action in progress. Please wait...`);
            this.errors.general = `Other action in progress. Please wait...`;
            return reject(this.errors);
          }
          if (needsId && !this.id) {
            console.warn(`No id provided`);
            this.errors.general = `No id provided`;
            reject(this.errors);
          }
          this.loading = true;
          promise()
            .then((response) => {
              resolve(response);
            })
            .catch((response) => {
              this.errors = response.response.data.errors;
              if (
                Object.keys(this.errors).length === 0 &&
                this.errors.constructor === Object
              ) {
                this.errors.general = response.response.data.message;
              }
              reject(this.errors);
            })
            .finally(() => {
              this.loading = false;
            });
        });
      },
      load(this: StoreActions<A, S>, item: S['item']) {
        this.item = item;
        this.initialized = true;
        this.loading = false;
        this.errors = {};
        this.id = item?.id || null;
      },
      ...actions,
    } as StoreActions<A, S>,
    ...extra,
  });
};

P.S. As putea spune ca Pinia nu e chiar 3rd party, atata timp cat il recomanda si vue :slight_smile:

2 Likes

Pinia e basically Flux pattern, adica redux in Vue. In React cel putin e antipattern sa stochezi query data in store fiindca se poate mai elegant. Inclusiv in redux toolkit mai nou avem useQuery care e synced in store, dar devine util doar daca vrei un history imutabil.

query/examples/vue/basic/src/Post.vue at main · TanStack/query (github.com)

E metoda eleganta din React, practic asignezi un query key dupa care se creeaza un cache si daca folosesti acelasi query key (poti sa faci inclusiv un wrapper la useQuery/query) si indiferent de componenta o sa iti ia acelasi state, cache sau daca invalidezi cache-ul se vor reincarca toate.