zustand-querystring 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/index.d.ts +1 -0
- package/lib/index.js +1 -0
- package/lib/middleware.d.ts +11 -0
- package/lib/middleware.js +115 -0
- package/lib/parser.d.ts +2 -0
- package/lib/parser.js +134 -0
- package/package.json +43 -0
package/lib/index.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { queryString, QueryStringOptions } from './middleware.js';
|
package/lib/index.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { queryString } from './middleware.js';
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { StateCreator, StoreMutatorIdentifier } from 'zustand/vanilla';
|
|
2
|
+
type DeepSelect<T> = T extends object ? {
|
|
3
|
+
[P in keyof T]?: DeepSelect<T[P]> | boolean;
|
|
4
|
+
} : boolean;
|
|
5
|
+
export interface QueryStringOptions<T> {
|
|
6
|
+
url?: string;
|
|
7
|
+
select?: (pathname: string) => DeepSelect<T>;
|
|
8
|
+
}
|
|
9
|
+
type QueryString = <T, Mps extends [StoreMutatorIdentifier, unknown][] = [], Mcs extends [StoreMutatorIdentifier, unknown][] = []>(initializer: StateCreator<T, Mps, Mcs>, options?: QueryStringOptions<T>) => StateCreator<T, Mps, Mcs>;
|
|
10
|
+
export declare const queryString: QueryString;
|
|
11
|
+
export {};
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import { parse, stringify } from './parser.js';
|
|
2
|
+
import { mergeWith, isEqual } from 'lodash-es';
|
|
3
|
+
const compact = (newState, initialState) => {
|
|
4
|
+
const output = {};
|
|
5
|
+
Object.keys(newState).forEach(key => {
|
|
6
|
+
if (newState[key] !== null &&
|
|
7
|
+
newState[key] !== undefined &&
|
|
8
|
+
typeof newState[key] !== 'function' &&
|
|
9
|
+
!isEqual(newState[key], initialState[key])) {
|
|
10
|
+
if (typeof newState[key] === 'object' && !Array.isArray(newState[key])) {
|
|
11
|
+
const value = compact(newState[key], initialState[key]);
|
|
12
|
+
if (value && Object.keys(value).length > 0) {
|
|
13
|
+
output[key] = value;
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
else {
|
|
17
|
+
output[key] = newState[key];
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
});
|
|
21
|
+
if (Object.keys(output).length === 0) {
|
|
22
|
+
return null;
|
|
23
|
+
}
|
|
24
|
+
return output;
|
|
25
|
+
};
|
|
26
|
+
const translateSelectionToState = (selection, state) => Object.keys(selection).reduce((acc, key) => {
|
|
27
|
+
const value = selection[key];
|
|
28
|
+
if (typeof value === 'boolean') {
|
|
29
|
+
if (value) {
|
|
30
|
+
acc[key] = state[key];
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
else {
|
|
34
|
+
acc[key] = translateSelectionToState(value, state[key]);
|
|
35
|
+
}
|
|
36
|
+
return acc;
|
|
37
|
+
}, {});
|
|
38
|
+
const queryStringImpl = (fn, options) => (set, get, api) => {
|
|
39
|
+
const defaultedOptions = {
|
|
40
|
+
partialize: state => state,
|
|
41
|
+
...options,
|
|
42
|
+
};
|
|
43
|
+
const url = defaultedOptions.url;
|
|
44
|
+
const initialState = get() ?? fn(set, get, api);
|
|
45
|
+
const getSelectedState = () => {
|
|
46
|
+
if (defaultedOptions.select) {
|
|
47
|
+
const selection = defaultedOptions.select(window.location.pathname);
|
|
48
|
+
// translate the selection to state
|
|
49
|
+
const selectedState = translateSelectionToState(selection, get());
|
|
50
|
+
return selectedState;
|
|
51
|
+
}
|
|
52
|
+
return get();
|
|
53
|
+
};
|
|
54
|
+
const initialize = (url, _set = set) => {
|
|
55
|
+
try {
|
|
56
|
+
const queryString = url.split('?')[1]?.slice(2);
|
|
57
|
+
if (!queryString) {
|
|
58
|
+
return fn(_set, get, api);
|
|
59
|
+
}
|
|
60
|
+
const parsed = parse(queryString);
|
|
61
|
+
const currentValue = get() ?? fn(_set, get, api);
|
|
62
|
+
const merged = mergeWith(currentValue, parsed);
|
|
63
|
+
set(merged, true);
|
|
64
|
+
return merged;
|
|
65
|
+
}
|
|
66
|
+
catch (error) {
|
|
67
|
+
console.error(error);
|
|
68
|
+
if (typeof window !== 'undefined') {
|
|
69
|
+
window.history.replaceState(null, '', window.location.pathname);
|
|
70
|
+
}
|
|
71
|
+
return fn(_set, get, api);
|
|
72
|
+
}
|
|
73
|
+
};
|
|
74
|
+
if (typeof window !== 'undefined') {
|
|
75
|
+
const setQuery = () => {
|
|
76
|
+
const selectedState = getSelectedState();
|
|
77
|
+
if (!selectedState) {
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
const compactedSelectedState = compact(selectedState, initialState);
|
|
81
|
+
if (!compactedSelectedState) {
|
|
82
|
+
window.history.replaceState(null, '', window.location.pathname);
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
const stringified = stringify(compactedSelectedState);
|
|
86
|
+
if (stringified) {
|
|
87
|
+
// console.log('set query', stringified);
|
|
88
|
+
// console.log('parse query', parse(stringified));
|
|
89
|
+
window.history.replaceState(null, '', `?q=${stringified}`);
|
|
90
|
+
}
|
|
91
|
+
};
|
|
92
|
+
//TODO: find a better way to do this
|
|
93
|
+
let previousUrl = '';
|
|
94
|
+
setInterval(() => {
|
|
95
|
+
if (window.location.href !== previousUrl) {
|
|
96
|
+
previousUrl = window.location.href;
|
|
97
|
+
setQuery();
|
|
98
|
+
}
|
|
99
|
+
}, 50);
|
|
100
|
+
const originalSetState = api.setState;
|
|
101
|
+
api.setState = (...args) => {
|
|
102
|
+
originalSetState(...args);
|
|
103
|
+
setQuery();
|
|
104
|
+
};
|
|
105
|
+
return initialize(window.location.href, (...args) => {
|
|
106
|
+
set(...args);
|
|
107
|
+
setQuery();
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
if (url) {
|
|
111
|
+
return initialize(url);
|
|
112
|
+
}
|
|
113
|
+
return fn(set, get, api);
|
|
114
|
+
};
|
|
115
|
+
export const queryString = queryStringImpl;
|
package/lib/parser.d.ts
ADDED
package/lib/parser.js
ADDED
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
const keyStringifyRegexp = /([=:@$/])/g;
|
|
2
|
+
const valueStringifyRegexp = /([&;/])/g;
|
|
3
|
+
const keyParseRegexp = /[=:@$]/;
|
|
4
|
+
const valueParseRegexp = /[&;]/;
|
|
5
|
+
function encodeString(str, regexp) {
|
|
6
|
+
return encodeURI(str.replace(regexp, '/$1'));
|
|
7
|
+
}
|
|
8
|
+
function trim(res) {
|
|
9
|
+
return typeof res === 'string' ? res.replace(/;+$/g, '') : res;
|
|
10
|
+
}
|
|
11
|
+
export function stringify(input, recursive) {
|
|
12
|
+
if (!recursive) {
|
|
13
|
+
return trim(stringify(input, true));
|
|
14
|
+
}
|
|
15
|
+
// Function
|
|
16
|
+
if (typeof input === 'function') {
|
|
17
|
+
return;
|
|
18
|
+
}
|
|
19
|
+
// Number, Boolean or Null
|
|
20
|
+
if (typeof input === 'number' ||
|
|
21
|
+
input === true ||
|
|
22
|
+
input === false ||
|
|
23
|
+
input === null) {
|
|
24
|
+
return ':' + input;
|
|
25
|
+
}
|
|
26
|
+
const res = [];
|
|
27
|
+
// Array
|
|
28
|
+
if (Array.isArray(input)) {
|
|
29
|
+
for (const elem of input) {
|
|
30
|
+
typeof elem === 'undefined'
|
|
31
|
+
? res.push(':null')
|
|
32
|
+
: res.push(stringify(elem, true));
|
|
33
|
+
}
|
|
34
|
+
return '@' + res.join('&') + ';';
|
|
35
|
+
}
|
|
36
|
+
// Object
|
|
37
|
+
if (typeof input === 'object') {
|
|
38
|
+
for (const [key, value] of Object.entries(input)) {
|
|
39
|
+
const stringifiedValue = stringify(value, true);
|
|
40
|
+
if (stringifiedValue) {
|
|
41
|
+
res.push(encodeString(key, keyStringifyRegexp) + stringifiedValue);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
return '$' + res.join('&') + ';';
|
|
45
|
+
}
|
|
46
|
+
// undefined
|
|
47
|
+
if (typeof input === 'undefined') {
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
// String
|
|
51
|
+
return '=' + encodeString(input.toString(), valueStringifyRegexp);
|
|
52
|
+
}
|
|
53
|
+
export function parse(str) {
|
|
54
|
+
let pos = 0;
|
|
55
|
+
str = decodeURI(str);
|
|
56
|
+
function readToken(regexp) {
|
|
57
|
+
let token = '';
|
|
58
|
+
for (; pos !== str.length; ++pos) {
|
|
59
|
+
if (str.charAt(pos) === '/') {
|
|
60
|
+
pos += 1;
|
|
61
|
+
if (pos === str.length) {
|
|
62
|
+
token += ';';
|
|
63
|
+
break;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
else if (str.charAt(pos).match(regexp)) {
|
|
67
|
+
break;
|
|
68
|
+
}
|
|
69
|
+
token += str.charAt(pos);
|
|
70
|
+
}
|
|
71
|
+
return token;
|
|
72
|
+
}
|
|
73
|
+
function parseToken() {
|
|
74
|
+
const type = str.charAt(pos++);
|
|
75
|
+
// String
|
|
76
|
+
if (type === '=') {
|
|
77
|
+
return readToken(valueParseRegexp);
|
|
78
|
+
}
|
|
79
|
+
// Number, Boolean or Null
|
|
80
|
+
if (type === ':') {
|
|
81
|
+
const value = readToken(valueParseRegexp);
|
|
82
|
+
if (value === 'true') {
|
|
83
|
+
return true;
|
|
84
|
+
}
|
|
85
|
+
if (value === 'false') {
|
|
86
|
+
return false;
|
|
87
|
+
}
|
|
88
|
+
const parsedValue = parseFloat(value);
|
|
89
|
+
return isNaN(parsedValue) ? null : parsedValue;
|
|
90
|
+
}
|
|
91
|
+
// Array
|
|
92
|
+
if (type === '@') {
|
|
93
|
+
const res = [];
|
|
94
|
+
loop: {
|
|
95
|
+
// empty array
|
|
96
|
+
if (pos >= str.length || str.charAt(pos) === ';') {
|
|
97
|
+
break loop;
|
|
98
|
+
}
|
|
99
|
+
// parse array items
|
|
100
|
+
while (1) {
|
|
101
|
+
res.push(parseToken());
|
|
102
|
+
if (pos >= str.length || str.charAt(pos) === ';') {
|
|
103
|
+
break loop;
|
|
104
|
+
}
|
|
105
|
+
pos += 1;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
pos += 1;
|
|
109
|
+
return res;
|
|
110
|
+
}
|
|
111
|
+
// Object
|
|
112
|
+
if (type === '$') {
|
|
113
|
+
const res = {};
|
|
114
|
+
loop: {
|
|
115
|
+
if (pos >= str.length || str.charAt(pos) === ';') {
|
|
116
|
+
break loop;
|
|
117
|
+
}
|
|
118
|
+
while (1) {
|
|
119
|
+
var name = readToken(keyParseRegexp);
|
|
120
|
+
res[name] = parseToken();
|
|
121
|
+
if (pos >= str.length || str.charAt(pos) === ';') {
|
|
122
|
+
break loop;
|
|
123
|
+
}
|
|
124
|
+
pos += 1;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
pos += 1;
|
|
128
|
+
return res;
|
|
129
|
+
}
|
|
130
|
+
// Error
|
|
131
|
+
throw new Error('Unexpected char ' + type);
|
|
132
|
+
}
|
|
133
|
+
return parseToken();
|
|
134
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "zustand-querystring",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"license": "MIT",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"keywords": [
|
|
7
|
+
"zustand",
|
|
8
|
+
"querystring"
|
|
9
|
+
],
|
|
10
|
+
"repository": {
|
|
11
|
+
"type": "github",
|
|
12
|
+
"url": "https://github.com/nitedani/zustand-querystring"
|
|
13
|
+
},
|
|
14
|
+
"exports": {
|
|
15
|
+
".": "./lib/index.js"
|
|
16
|
+
},
|
|
17
|
+
"typesVersions": {
|
|
18
|
+
"*": {
|
|
19
|
+
"*": [
|
|
20
|
+
"lib/index.d.ts"
|
|
21
|
+
]
|
|
22
|
+
}
|
|
23
|
+
},
|
|
24
|
+
"files": [
|
|
25
|
+
"lib"
|
|
26
|
+
],
|
|
27
|
+
"dependencies": {
|
|
28
|
+
"lodash-es": "^4.17.21"
|
|
29
|
+
},
|
|
30
|
+
"prettier": {
|
|
31
|
+
"singleQuote": true,
|
|
32
|
+
"arrowParens": "avoid"
|
|
33
|
+
},
|
|
34
|
+
"devDependencies": {
|
|
35
|
+
"@types/lodash-es": "^4.17.6",
|
|
36
|
+
"rimraf": "^3.0.2",
|
|
37
|
+
"typescript": "^4.9.3",
|
|
38
|
+
"zustand": "^4.1.4"
|
|
39
|
+
},
|
|
40
|
+
"scripts": {
|
|
41
|
+
"build": "rimraf lib && tsc -b"
|
|
42
|
+
}
|
|
43
|
+
}
|