@oarepo/vue-query-synchronizer
Version:
Browser vue-router query synchronization library
774 lines (627 loc) • 18 kB
JavaScript
/*!
* @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 };