UNPKG

vue-condition-watcher

Version:

Vue composition API for automatic data fetching. With conditions as the core. Easily control and sync to URL query string by conditions

262 lines (261 loc) 11.2 kB
import { computed, isRef, onUnmounted, reactive, readonly, ref, shallowRef, unref, watch, watchEffect, } from 'vue-demi'; import { containsProp, isNoData as isDataEmpty, isObject, isServer, rAF } from './utils/helper'; import { createParams, deepClone, filterNoneValueObject, isEquivalent, syncQuery2Conditions } from './utils/common'; import { createEvents } from './utils/createEvents'; import { useCache } from './composable/useCache'; import { useHistory } from './composable/useHistory'; import { usePromiseQueue } from './composable/usePromiseQueue'; export default function useConditionWatcher(config) { function isFetchConfig(obj) { return containsProp(obj, 'fetcher', 'conditions', 'defaultParams', 'initialData', 'manual', 'immediate', 'history', 'pollingInterval', 'pollingWhenHidden', 'pollingWhenOffline', 'revalidateOnFocus', 'cacheProvider', 'beforeFetch', 'afterFetch', 'onFetchError'); } function isHistoryOption() { if (!config.history || !config.history.sync) return false; return containsProp(config.history, 'navigation', 'ignore', 'sync'); } // default config let watcherConfig = { fetcher: config.fetcher, conditions: config.conditions, immediate: true, manual: false, initialData: undefined, pollingInterval: isRef(config.pollingInterval) ? config.pollingInterval : ref(config.pollingInterval || 0), pollingWhenHidden: false, pollingWhenOffline: false, revalidateOnFocus: false, cacheProvider: () => new Map(), }; // update config if (isFetchConfig(config)) { watcherConfig = { ...watcherConfig, ...config }; } const cache = useCache(watcherConfig.fetcher, watcherConfig.cacheProvider()); const backupIntiConditions = deepClone(watcherConfig.conditions); const _conditions = reactive(watcherConfig.conditions); const isFetching = ref(false); const isOnline = ref(true); const isActive = ref(true); const data = shallowRef(cache.cached(backupIntiConditions) ? cache.get(backupIntiConditions) : watcherConfig.initialData || undefined); const error = ref(undefined); const query = ref({}); const pollingTimer = ref(); const { enqueue } = usePromiseQueue(); // - create fetch event & condition event & web event const { conditionEvent, responseEvent, errorEvent, finallyEvent, reconnectEvent, focusEvent, visibilityEvent, stopFocusEvent, stopReconnectEvent, stopVisibilityEvent, } = createEvents(); const resetConditions = (cond) => { Object.assign(_conditions, isObject(cond) && !cond.type ? cond : backupIntiConditions); }; const loading = computed(() => !error.value && !data.value); const conditionsChangeHandler = async (conditions, throwOnFailed = false) => { const checkThrowOnFailed = typeof throwOnFailed === 'boolean' ? throwOnFailed : false; if (isFetching.value) return; isFetching.value = true; error.value = undefined; const conditions2Object = conditions; let customConditions = {}; const deepCopyCondition = deepClone(conditions2Object); if (typeof watcherConfig.beforeFetch === 'function') { let isCanceled = false; customConditions = await watcherConfig.beforeFetch(deepCopyCondition, () => { isCanceled = true; }); if (isCanceled) { // eslint-disable-next-line require-atomic-updates isFetching.value = false; return Promise.resolve(undefined); } if (!customConditions || typeof customConditions !== 'object' || customConditions.constructor !== Object) { // eslint-disable-next-line require-atomic-updates isFetching.value = false; throw new Error(`[vue-condition-watcher]: beforeFetch should return an object`); } } const validateCustomConditions = Object.keys(customConditions).length !== 0; /* * if custom conditions has value, just use custom conditions * filterNoneValueObject will filter no value like [] , '', null, undefined * example. {name: '', items: [], age: 0, tags: null} * return result will be {age: 0} */ query.value = filterNoneValueObject(validateCustomConditions ? customConditions : conditions2Object); const finalConditions = createParams(query.value, watcherConfig.defaultParams); let responseData = undefined; data.value = cache.cached(query.value) ? cache.get(query.value) : watcherConfig.initialData || undefined; return new Promise((resolve, reject) => { config .fetcher(finalConditions) .then(async (fetchResponse) => { responseData = fetchResponse; if (typeof watcherConfig.afterFetch === 'function') { responseData = await watcherConfig.afterFetch(fetchResponse); } if (responseData === undefined) { console.warn(`[vue-condition-watcher]: "afterFetch" return value is ${responseData}. Please check it.`); } if (!isEquivalent(data.value, responseData)) { data.value = responseData; } if (!isEquivalent(cache.get(query.value), responseData)) { cache.set(query.value, responseData); } responseEvent.trigger(responseData); return resolve(fetchResponse); }) .catch(async (fetchError) => { if (typeof watcherConfig.onFetchError === 'function') { // eslint-disable-next-line @typescript-eslint/no-extra-semi ; ({ data: responseData, error: fetchError } = await watcherConfig.onFetchError({ data: undefined, error: fetchError, })); data.value = responseData || watcherConfig.initialData; error.value = fetchError; } errorEvent.trigger(fetchError); if (checkThrowOnFailed) { return reject(fetchError); } return resolve(undefined); }) .finally(() => { isFetching.value = false; finallyEvent.trigger(); }); }); }; const revalidate = (throwOnFailed = false) => enqueue(() => conditionsChangeHandler({ ..._conditions }, throwOnFailed)); function execute(throwOnFailed = false) { if (isDataEmpty(data.value) || isServer) { revalidate(throwOnFailed); } else { rAF(() => revalidate(throwOnFailed)); } } // - Start polling with out setting to manual if (!watcherConfig.manual) { watchEffect((onCleanup) => { if (unref(watcherConfig.pollingInterval)) { pollingTimer.value = (() => { let timerId = null; function next() { const interval = unref(watcherConfig.pollingInterval); if (interval && timerId !== -1) { timerId = setTimeout(nun, interval); } } function nun() { // Only run when the page is visible, online and not errored. if (!error.value && (watcherConfig.pollingWhenHidden || isActive.value) && (watcherConfig.pollingWhenOffline || isOnline.value)) { revalidate().then(next); } else { next(); } } next(); return () => timerId && clearTimeout(timerId); })(); } onCleanup(() => { pollingTimer.value && pollingTimer.value(); }); }); } // - mutate: Modify `data` directly // - `data` is read only by default, recommend modify `data` at `afterFetch` // - When you need to modify `data`, you can use mutate() to directly modify data /* * Two way to use mutate * - 1. * mutate(newData) * - 2. * mutate((currentData) => { * currentData[0].name = 'runkids' * return currentData * }) */ const mutate = (...args) => { const arg = args[0]; if (arg === undefined) { return data.value; } if (typeof arg === 'function') { data.value = arg(deepClone(data.value)); } else { data.value = arg; } cache.set({ ..._conditions }, data.value); return data.value; }; // - History mode base on vue-router if (isHistoryOption()) { const historyOption = { sync: config.history.sync, ignore: config.history.ignore || [], navigation: config.history.navigation || 'push', listener(parsedQuery) { const queryObject = Object.keys(parsedQuery).length ? parsedQuery : backupIntiConditions; syncQuery2Conditions(_conditions, queryObject, backupIntiConditions); }, }; useHistory(query, historyOption); } // - Automatic data fetching by default if (!watcherConfig.manual && watcherConfig.immediate) { execute(); } watch(() => ({ ..._conditions }), (nc, oc) => { // - Deep check object if be true do nothing if (isEquivalent(nc, oc)) return; conditionEvent.trigger(deepClone(nc), deepClone(oc)); // - Automatic data fetching until manual to be false !watcherConfig.manual && enqueue(() => conditionsChangeHandler(nc)); }); reconnectEvent.on((status) => { isOnline.value = status; }); visibilityEvent.on((status) => { isActive.value = status; }); const stopSubscribeFocus = focusEvent.on(() => { if (!isActive.value) return; execute(); // if (isHistoryOption() && cache.cached({ ..._conditions })) { //todo sync to query // } }); if (!watcherConfig.revalidateOnFocus) { stopFocusEvent(); stopSubscribeFocus.off(); } onUnmounted(() => { pollingTimer.value && pollingTimer.value(); stopFocusEvent(); stopReconnectEvent(); stopVisibilityEvent(); }); return { conditions: _conditions, data: readonly(data), error: readonly(error), isFetching: readonly(isFetching), loading, execute, mutate, resetConditions, onConditionsChange: conditionEvent.on, onFetchSuccess: responseEvent.on, onFetchError: errorEvent.on, onFetchFinally: finallyEvent.on, }; }