UNPKG

@oarepo/vue-query-synchronizer

Version:

Browser vue-router query synchronization library

774 lines (627 loc) 18 kB
/*! * @oarepo/vue-query-synchronizer v3.0.2 * (c) Miroslav Simek <simeki@vscht.cz> * Released under the MIT License. */ import { reactive, watch } from 'vue'; function _toConsumableArray(arr) { return _arrayWithoutHoles(arr) || _iterableToArray(arr) || _unsupportedIterableToArray(arr) || _nonIterableSpread(); } function _arrayWithoutHoles(arr) { if (Array.isArray(arr)) return _arrayLikeToArray(arr); } function _iterableToArray(iter) { if (typeof Symbol !== "undefined" && iter[Symbol.iterator] != null || iter["@@iterator"] != null) return Array.from(iter); } function _unsupportedIterableToArray(o, minLen) { if (!o) return; if (typeof o === "string") return _arrayLikeToArray(o, minLen); var n = Object.prototype.toString.call(o).slice(8, -1); if (n === "Object" && o.constructor) n = o.constructor.name; if (n === "Map" || n === "Set") return Array.from(o); if (n === "Arguments" || /^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(n)) return _arrayLikeToArray(o, minLen); } function _arrayLikeToArray(arr, len) { if (len == null || len > arr.length) len = arr.length; for (var i = 0, arr2 = new Array(len); i < len; i++) arr2[i] = arr[i]; return arr2; } function _nonIterableSpread() { throw new TypeError("Invalid attempt to spread non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method."); } function arraysMatch(arr1, arr2) { // Check if the arrays are the same length if (arr1.length !== arr2.length) return false; // Check if all items exist and are in the same order for (var i = 0; i < arr1.length; i++) { if (arr1[i] !== arr2[i]) return false; } // Otherwise, return true return true; } // adapted from vue router to be always // the same regardless of order of defined keys function queryFingerprint(query) { var fingerprint = ''; var detailedFingerprint = {}; var keys = Object.keys(query).sort(); for (var key_index in keys) { var key = keys[key_index]; var value = query[key]; key = encodeURIComponent(key); if (value == null) { fingerprint += '&' + key; detailedFingerprint[key] = null; continue; } // keep null values var values = Array.isArray(value) ? value.map(function (v) { return v && encodeURIComponent(v); }) : [value && encodeURIComponent(value)]; values.sort(); var valueFingerprint = ''; for (var i = 0; i < values.length; i++) { valueFingerprint += '&' + key; if (values[i] != null) { valueFingerprint += '=' + values[i]; } } fingerprint += valueFingerprint; detailedFingerprint[key] = valueFingerprint; } return { fingerprint: fingerprint, detailedFingerprint: detailedFingerprint }; } /** * Parses URL parameter as string */ var StringDatatype = { name: 'string', parseDefault: function parseDefault(value) { return value || ''; }, parse: function parse(value, defaultValue) { if (value === undefined) { return defaultValue; } if (!value) { return ''; } if (typeof value === 'string') { return value; } console.error('Incorrect variable for parameter, expecting string, got', value); return defaultValue; }, serialize: function serialize(value, defaultValue) { if (value === null || value === undefined || value === '' && !defaultValue) { return undefined; } return value === defaultValue ? undefined : value; } }; /** * Interprets URL parameter as integer */ var IntDatatype = { name: 'int', parseDefault: function parseDefault(value) { if (value.length) { return parseInt(value); } return 0; }, parse: function parse(value, defaultValue) { if (value === undefined || value === null) { return defaultValue; } var parsedValue = parseInt(value.toString()); if (isNaN(parsedValue)) { return defaultValue; } return parsedValue; }, serialize: function serialize(value, defaultValue) { if (value === null || value === undefined) { return undefined; } value = parseInt((value || 0).toString()); return value === defaultValue ? undefined : value.toString(); } }; /** * Interprets URL parameter as boolean. If the parameter is present the value is true, false otherwise */ var BoolDatatype = { name: 'bool', parseDefault: function parseDefault(value) { return value === '1' || value === 'true'; }, parse: function parse(value, defaultValue) { if (value === undefined) { return defaultValue || false; } return true; }, serialize: function serialize(value, defaultValue) { if (value === undefined) { return undefined; } if (value && value !== defaultValue) { return null; } return undefined; } }; /** * Interprets URL parameter as an array of strings, that is the parameter might be present multiple times */ var ArrayDatatype = { name: 'array', parseDefault: function parseDefault(value) { if (value === undefined) { return function () { return []; }; } if (typeof value === 'string') { if (value !== '') { return function () { return [value]; }; } else { return function () { return []; }; } } return function () { return _toConsumableArray(value || []); }; }, parse: function parse(value, defaultValue) { if (value === undefined) { return (defaultValue || []).slice(); } if (typeof value === 'string') { return [value]; } if (!value) { return []; } return value.filter(function (x) { return x !== null; }); }, serialize: function serialize(value, defaultValue) { if (value === null || value === undefined || value.length === 0) { return undefined; } if (arraysMatch(value, defaultValue)) { return undefined; } if (value.length === 1) { return value[0]; } return value; } }; /** * Helper function to generate a new datatype that is an array of strings but serialized within one * param=value url parameter with values separated by the given separator */ function separatedArrayDatatype(separator) { var dt = { name: "separated_array_".concat(separator), parseDefault: function parseDefault(value) { if (value === undefined) { return []; } if (typeof value === 'string') { if (value === '') { return []; } return value.split(separator); } return value || []; }, parse: function parse(value, defaultValue) { if (value === undefined) { return (defaultValue || []).slice(); } if (typeof value === 'string') { if (value === '') { return []; } return value.split(separator); } if (!value) { return []; } return value.filter(function (x) { return x !== null; }); }, serialize: function serialize(value, defaultValue) { if (value === null || value === undefined || value.length === 0) { return undefined; } if (arraysMatch(value, defaultValue)) { return undefined; } return value.join(separator); } }; return dt; } /** * parses param=v1,v2,v3,... into [v1,v2,v3] string array */ var CommaArrayDatatype = separatedArrayDatatype(','); /** * parses param=v1%20v2%20v3... into [v1,v2,v3] string array */ var SpaceArrayDatatype = separatedArrayDatatype(' '); /** * convert parameter in "meta/query" from string representation to object representation * * @internal */ function parseParamDefinition(defOrString, datatypes) { var def; if (typeof defOrString === 'string') { var splitted = defOrString.split(':'); if (splitted.length === 1) { splitted = ['string', splitted[0]]; } var datatype = datatypes[splitted[0]]; if (!datatype) { throw new Error("No query datatype \"".concat(splitted[0], "\"")); } def = { datatype: datatype, defaultValue: datatype.parseDefault(splitted[1]) }; } else { def = defOrString; } if (def.datatype === undefined) { def.datatype = datatypes['string']; } return def; } var _query = reactive({ query: {}, rawQuery: {}, enabled: false, serializedId: 0 }); var router; var datatypes; var debug = false; var navigationOperation; var queryDefinition = null; var querySettings = null; var fingerprint = null; var detailedFingerprint = {}; var watchers = {}; function dlog() { if (debug) { var _console; (_console = console).log.apply(_console, arguments); } } function prepareQuery(route) { var _queryDefinition = {}; var _querySettings = {}; // merge query definition from all matched segments on path route.matched.forEach(function (match) { if (!match.meta) { return; } if (match.meta.query) { Object.assign(_queryDefinition, match.meta.query); } if (match.meta.querySettings) { Object.assign(_querySettings, match.meta.querySettings); } }); if (!_queryDefinition) { return false; } // parse param definitions Object.keys(_queryDefinition).forEach(function (key) { _queryDefinition[key] = parseParamDefinition(_queryDefinition[key], datatypes); }); if (_querySettings.onInit) { _querySettings.onInit(_queryDefinition); } queryDefinition = _queryDefinition; querySettings = _querySettings; dlog('query definition, query settings', queryDefinition, querySettings); return true; } function serializeChangedValue(key, value) { dlog('serialize changed value', key, value); if (value !== undefined) { var def = queryDefinition[key]; if (def) { value = def.datatype.serialize(value, def.defaultValue); } } saveValueToRawQuery(key, value); } function saveValueToRawQuery(key, value) { dlog('save value to raw query', key, value); if (value === undefined) { if (key in _query.rawQuery) { delete _query.rawQuery[key]; _query.serializedId++; } return; } var isNull = value === null; var isArray = !isNull && Array.isArray(value); if (!isNull) { if (isArray) { value = value.map(function (x) { return x.toString(); }); } else { value = value.toString(); } } if (!(key in _query.rawQuery)) { dlog('value not in rawQuery, added'); _query.rawQuery[key] = value; _query.serializedId++; } else { var modified = false; var oldValue = _query.rawQuery[key]; if (oldValue === null) { modified = !isNull; } else if (Array.isArray(oldValue)) { if (!isArray) { modified = true; } else if (arraysMatch(oldValue, value)) { modified = true; } } else if (isArray) { modified = true; } else if (value !== oldValue) { modified = true; } if (modified) { dlog('value in rawQuery, modified'); _query.rawQuery[key] = value; _query.serializedId++; } else { dlog('value in rawQuery, not modified', _query.rawQuery[key]); } } dlog('Set to raw', key, value); } function setWatcher(key) { if (key in watchers) { return; } dlog('Adding watcher', key); watchers[key] = watch(function () { return _query.query[key]; }, function (value) { return serializeChangedValue(key, value); }); } function clearWatchers() { Object.values(watchers).forEach(function (watcher) { return watcher(); }); watchers = {}; } function handleRouteChange(to) { fingerprint = null; detailedFingerprint = {}; Object.keys(_query.query).forEach(function (key) { delete _query.query[key]; }); queryDefinition = {}; querySettings = {}; clearWatchers(); if (!prepareQuery(to)) { dlog('No query meta on this route'); _query.enabled = false; } else { dlog('Got query meta on this route'); _query.enabled = true; } } function parseAndStoreQuery(query, actualDetailedFingerprint) { dlog('Parse and store query called with', query, actualDetailedFingerprint); for (var _i = 0, _Object$keys = Object.keys(query); _i < _Object$keys.length; _i++) { var key = _Object$keys[_i]; if (detailedFingerprint[key] === actualDetailedFingerprint[key]) { continue; } var val = query[key]; var def = queryDefinition[key]; if (!def) { _query.query[key] = val; } else { _query.query[key] = def.datatype.parse(val, def.defaultValue); } } for (var _i2 = 0, _Object$keys2 = Object.keys(queryDefinition); _i2 < _Object$keys2.length; _i2++) { var _key = _Object$keys2[_i2]; if (_key in _query.query) { continue; } var _def = queryDefinition[_key]; var defVal = _def.defaultValue; if (typeof defVal === 'function') { defVal = defVal(); } _query.query[_key] = defVal; } _query.rawQuery = {}; Object.assign(_query.rawQuery, query); if (querySettings.onLoad) { querySettings.onLoad(_query.query); } } function setup(_router, _datatypes, _debug, _navigationOperation) { router = _router; datatypes = _datatypes; debug = _debug; if (_navigationOperation === 'push' || _navigationOperation === 'replace') { navigationOperation = function navigationOperation() { return _navigationOperation; }; } else { navigationOperation = _navigationOperation; } router.beforeEach(function (to, from) { dlog('beforeEach called', to.name, from.name); if (to.name !== from.name) { handleRouteChange(to); } if (!_query.enabled) { return; } // check fingerprint if query modified var actualFingerprint = queryFingerprint(to.query); if (actualFingerprint.fingerprint === fingerprint) { dlog('same fingerprint, not changing args'); return; } dlog('parsing args', actualFingerprint); // parse and store query params parseAndStoreQuery(to.query, actualFingerprint.detailedFingerprint); // set up watchers for (var _i3 = 0, _Object$keys3 = Object.keys(_query.query); _i3 < _Object$keys3.length; _i3++) { var key = _Object$keys3[_i3]; setWatcher(key); } // set fingerprint fingerprint = actualFingerprint.fingerprint; detailedFingerprint = actualFingerprint.detailedFingerprint; }); watch(function () { return _query.serializedId; }, function () { // no need to go through the parsing process again when router calls beforeEach, // so set the fingerprint var actualFingerprint = queryFingerprint(_query.rawQuery); fingerprint = actualFingerprint.fingerprint; detailedFingerprint = actualFingerprint.detailedFingerprint; if (querySettings.onChange) { querySettings.onChange(_query.rawQuery, _query.query); } var op = navigationOperation(_query.query, router); router[op]({ query: _query.rawQuery }); }); } function define(key, datatype, defaultValue) { queryDefinition[key] = { datatype: datatype, defaultValue: defaultValue }; _query.query[key] = datatype.parse(_query.rawQuery[key], defaultValue); setWatcher(key); dlog('Defined new key', key); } function as_array(key, datatype) { if (!(key in _query.query)) { define(key, datatype || ArrayDatatype, []); } var arr = _query.query[key] || []; if (!Array.isArray(arr)) { return [arr]; } else { return _toConsumableArray(arr); } } function addValue(key, value, datatype) { var arr = as_array(key, datatype); var idx = arr.indexOf(value); if (idx < 0) { arr.push(value); } _query.query[key] = arr; } function removeValue(key, value, datatype) { var arr = as_array(key, datatype); var idx = arr.indexOf(value); if (idx >= 0) { arr.splice(idx, 1); } _query.query[key] = arr; } var handler = { set: function set(target, prop, value) { if (!(prop in target)) { define(prop, StringDatatype, ''); } target[prop] = value; return true; }, get: function get(target, prop) { if (prop === 'define') { return define; } if (prop === 'addValue') { return addValue; } if (prop === 'removeValue') { return removeValue; } if (prop === '__definition') { return queryDefinition; } return target[prop]; } }; var proxiedQuery = new Proxy(_query.query, handler); /** * The main composition API entrypoint. Returns a reactive Query object with parsed values * * @typeParam T The type of the returned query object. An interface defining query parameters and their types * might be passed. If not a generic (untyped) interface is used. */ function useQuery() { return proxiedQuery; } /** * @internal */ function useRawQuery() { return _query; } function watchQuery(fn) { watch(function () { return _query.serializedId; }, function () { return fn; }); } function queryWatchSource() { return _query.serializedId; } var QuerySynchronizer = { install: function install(app, _ref) { var router = _ref.router, datatypes = _ref.datatypes, debug = _ref.debug, navigationOperation = _ref.navigationOperation; var _datatypes = { 'string': StringDatatype, 'bool': BoolDatatype, 'int': IntDatatype, 'array': ArrayDatatype, 'commaarray': CommaArrayDatatype, 'spacearray': SpaceArrayDatatype }; if (datatypes) { (datatypes || []).forEach(function (c) { _datatypes[c.name] = c; }); } setup(router, _datatypes, debug || false, navigationOperation || 'push'); app.config.globalProperties.$query = useQuery(); } }; export default QuerySynchronizer; export { ArrayDatatype, BoolDatatype, CommaArrayDatatype, IntDatatype, SpaceArrayDatatype, StringDatatype, proxiedQuery as query, queryWatchSource, separatedArrayDatatype, useQuery, useRawQuery, watchQuery };