tanbal-hooks 1.0.0
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/LICENSE +21 -0
- package/README.md +348 -0
- package/data-manager/useDataManager.js +157 -0
- package/data-manager/useLocalStorage.js +31 -0
- package/event/useClickOutside.js +24 -0
- package/event/useInputBlur.js +45 -0
- package/index.js +13 -0
- package/package.json +34 -0
- package/useQueryParams.js +188 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Tanbal
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,348 @@
|
|
|
1
|
+
# tanbal-hooks
|
|
2
|
+
|
|
3
|
+
A collection of useful and reusable React and Next.js hooks.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install tanbal-hooks
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Hooks
|
|
12
|
+
|
|
13
|
+
* [`useDataManager`](#usedatamanager)
|
|
14
|
+
* [`useLocalStorage`](#uselocalstorage)
|
|
15
|
+
* [`useClickOutside`](#useclickoutside)
|
|
16
|
+
* [`useInputBlur`](#useinputblur)
|
|
17
|
+
* [`useQueryParams`](#usequeryparams)
|
|
18
|
+
|
|
19
|
+
---
|
|
20
|
+
|
|
21
|
+
## useDataManager
|
|
22
|
+
|
|
23
|
+
A hook for managing data through REST API requests.
|
|
24
|
+
|
|
25
|
+
### Usage
|
|
26
|
+
|
|
27
|
+
```jsx
|
|
28
|
+
import useDataManager from 'tanbal-hooks';
|
|
29
|
+
|
|
30
|
+
const [data, status, actions] = useDataManager('/api/users');
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
### Return value
|
|
34
|
+
|
|
35
|
+
```js
|
|
36
|
+
[data, status, actions]
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
`status` can be:
|
|
40
|
+
|
|
41
|
+
```text
|
|
42
|
+
idle
|
|
43
|
+
loading
|
|
44
|
+
loaded
|
|
45
|
+
failed
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
`actions` contains:
|
|
49
|
+
|
|
50
|
+
```js
|
|
51
|
+
{
|
|
52
|
+
load,
|
|
53
|
+
add,
|
|
54
|
+
update,
|
|
55
|
+
updateMultiple,
|
|
56
|
+
remove,
|
|
57
|
+
get,
|
|
58
|
+
}
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
### Methods
|
|
62
|
+
|
|
63
|
+
#### `load(successCallback, failCallback, finallyCallback)`
|
|
64
|
+
|
|
65
|
+
Loads data using `GET`.
|
|
66
|
+
|
|
67
|
+
```js
|
|
68
|
+
await actions.load(
|
|
69
|
+
data => console.log(data),
|
|
70
|
+
error => console.error(error),
|
|
71
|
+
() => console.log('finished')
|
|
72
|
+
);
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
#### `add(data, successCallback, failCallback, finallyCallback)`
|
|
76
|
+
|
|
77
|
+
Adds a new item using `POST`.
|
|
78
|
+
|
|
79
|
+
```js
|
|
80
|
+
await actions.add({
|
|
81
|
+
name: 'Ali',
|
|
82
|
+
});
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
#### `update(id, newData, successCallback, failCallback, finallyCallback)`
|
|
86
|
+
|
|
87
|
+
Updates an item using `PATCH`.
|
|
88
|
+
|
|
89
|
+
```js
|
|
90
|
+
await actions.update(1, {
|
|
91
|
+
name: 'Ali Reza',
|
|
92
|
+
});
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
#### `updateMultiple(updates, successCallback, failCallback, finallyCallback)`
|
|
96
|
+
|
|
97
|
+
Updates multiple items using `PATCH`.
|
|
98
|
+
|
|
99
|
+
```js
|
|
100
|
+
await actions.updateMultiple([
|
|
101
|
+
{ id: 1, name: 'Ali' },
|
|
102
|
+
{ id: 2, name: 'Reza' },
|
|
103
|
+
]);
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
#### `remove(id, successCallback, failCallback, finallyCallback)`
|
|
107
|
+
|
|
108
|
+
Removes an item using `DELETE`.
|
|
109
|
+
|
|
110
|
+
```js
|
|
111
|
+
await actions.remove(1);
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
#### `get(which, key = 'id')`
|
|
115
|
+
|
|
116
|
+
Finds an item by a specified key.
|
|
117
|
+
|
|
118
|
+
```js
|
|
119
|
+
const user = actions.get(1);
|
|
120
|
+
|
|
121
|
+
const user = actions.get('Ali', 'name');
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
---
|
|
125
|
+
|
|
126
|
+
## useLocalStorage
|
|
127
|
+
|
|
128
|
+
A hook for storing and synchronizing a value with `localStorage`.
|
|
129
|
+
|
|
130
|
+
### Usage
|
|
131
|
+
|
|
132
|
+
```jsx
|
|
133
|
+
import useLocalStorage from 'tanbal-react-hooks';
|
|
134
|
+
|
|
135
|
+
const [value, setValue] = useLocalStorage('theme', 'light');
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
The hook returns:
|
|
139
|
+
|
|
140
|
+
```js
|
|
141
|
+
[value, setValue]
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
The stored value is automatically read from `localStorage` when the key changes and saved whenever the value changes.
|
|
145
|
+
|
|
146
|
+
---
|
|
147
|
+
|
|
148
|
+
## useClickOutside
|
|
149
|
+
|
|
150
|
+
A hook for detecting clicks outside of an element.
|
|
151
|
+
|
|
152
|
+
### Usage
|
|
153
|
+
|
|
154
|
+
```jsx
|
|
155
|
+
import useClickOutside from 'tanbal-react-hooks';
|
|
156
|
+
|
|
157
|
+
const ref = useClickOutside(() => {
|
|
158
|
+
console.log('Clicked outside');
|
|
159
|
+
}, true);
|
|
160
|
+
|
|
161
|
+
return (
|
|
162
|
+
<div ref={ref}>
|
|
163
|
+
Content
|
|
164
|
+
</div>
|
|
165
|
+
);
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
### Parameters
|
|
169
|
+
|
|
170
|
+
```js
|
|
171
|
+
useClickOutside(onOutsideClick, isActive)
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
* `onOutsideClick` — callback called when a click occurs outside the referenced element.
|
|
175
|
+
* `isActive` — enables or disables the listener. Defaults to `false`.
|
|
176
|
+
|
|
177
|
+
---
|
|
178
|
+
|
|
179
|
+
## useInputBlur
|
|
180
|
+
|
|
181
|
+
A hook for handling input events with a delay and executing a callback when the input loses focus.
|
|
182
|
+
|
|
183
|
+
### Usage
|
|
184
|
+
|
|
185
|
+
```jsx
|
|
186
|
+
import useInputBlur from 'tanbal-react-hooks';
|
|
187
|
+
|
|
188
|
+
const inputRef = useInputBlur(500, value => {
|
|
189
|
+
console.log(value);
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
return <input ref={inputRef} />;
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
### Parameters
|
|
196
|
+
|
|
197
|
+
```js
|
|
198
|
+
useInputBlur(time, callback)
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
* `time` — delay in milliseconds before the callback is executed after input.
|
|
202
|
+
* `callback` — callback receiving the current input value.
|
|
203
|
+
|
|
204
|
+
The callback is also executed immediately when the input loses focus.
|
|
205
|
+
|
|
206
|
+
---
|
|
207
|
+
|
|
208
|
+
## useQueryParams
|
|
209
|
+
|
|
210
|
+
A Next.js hook for managing URL query parameters and synchronizing them with React state.
|
|
211
|
+
|
|
212
|
+
> This hook uses `next/navigation` and is intended for Next.js App Router applications.
|
|
213
|
+
|
|
214
|
+
### Usage
|
|
215
|
+
|
|
216
|
+
```jsx
|
|
217
|
+
import useQueryParams from 'tanbal-react-hooks';
|
|
218
|
+
|
|
219
|
+
const [query, state, setState] = useQueryParams(
|
|
220
|
+
{
|
|
221
|
+
search: '',
|
|
222
|
+
},
|
|
223
|
+
{
|
|
224
|
+
search: {
|
|
225
|
+
path: 'search',
|
|
226
|
+
},
|
|
227
|
+
}
|
|
228
|
+
);
|
|
229
|
+
```
|
|
230
|
+
|
|
231
|
+
The hook returns:
|
|
232
|
+
|
|
233
|
+
```js
|
|
234
|
+
[actions, state, setState]
|
|
235
|
+
```
|
|
236
|
+
|
|
237
|
+
### Actions
|
|
238
|
+
|
|
239
|
+
```js
|
|
240
|
+
{
|
|
241
|
+
get,
|
|
242
|
+
set,
|
|
243
|
+
setMultiple,
|
|
244
|
+
remove,
|
|
245
|
+
removeParams,
|
|
246
|
+
reset,
|
|
247
|
+
getPath,
|
|
248
|
+
}
|
|
249
|
+
```
|
|
250
|
+
|
|
251
|
+
### `get(key)`
|
|
252
|
+
|
|
253
|
+
Gets a query parameter.
|
|
254
|
+
|
|
255
|
+
```js
|
|
256
|
+
const value = query.get('search');
|
|
257
|
+
```
|
|
258
|
+
|
|
259
|
+
Returns `null` when the parameter is empty or does not exist.
|
|
260
|
+
|
|
261
|
+
### `set(key, value, replace = true)`
|
|
262
|
+
|
|
263
|
+
Sets a query parameter and updates the corresponding state value.
|
|
264
|
+
|
|
265
|
+
```js
|
|
266
|
+
query.set('search', 'react');
|
|
267
|
+
```
|
|
268
|
+
|
|
269
|
+
### `setMultiple(newValues)`
|
|
270
|
+
|
|
271
|
+
Sets multiple query parameters at once.
|
|
272
|
+
|
|
273
|
+
```js
|
|
274
|
+
query.setMultiple({
|
|
275
|
+
search: 'react',
|
|
276
|
+
page: 2,
|
|
277
|
+
});
|
|
278
|
+
```
|
|
279
|
+
|
|
280
|
+
### `remove(key, replace = true)`
|
|
281
|
+
|
|
282
|
+
Removes a query parameter and clears its corresponding state value.
|
|
283
|
+
|
|
284
|
+
```js
|
|
285
|
+
query.remove('search');
|
|
286
|
+
```
|
|
287
|
+
|
|
288
|
+
### `removeParams(paramsList)`
|
|
289
|
+
|
|
290
|
+
Removes multiple query parameters.
|
|
291
|
+
|
|
292
|
+
```js
|
|
293
|
+
query.removeParams(['search', 'page']);
|
|
294
|
+
```
|
|
295
|
+
|
|
296
|
+
### `reset()`
|
|
297
|
+
|
|
298
|
+
Removes all configured query parameters.
|
|
299
|
+
|
|
300
|
+
```js
|
|
301
|
+
query.reset();
|
|
302
|
+
```
|
|
303
|
+
|
|
304
|
+
### `getPath(key)`
|
|
305
|
+
|
|
306
|
+
Returns the configured state path for a query parameter.
|
|
307
|
+
|
|
308
|
+
```js
|
|
309
|
+
const path = query.getPath('search');
|
|
310
|
+
```
|
|
311
|
+
|
|
312
|
+
### Configuration
|
|
313
|
+
|
|
314
|
+
Each query parameter can be configured with:
|
|
315
|
+
|
|
316
|
+
```js
|
|
317
|
+
{
|
|
318
|
+
search: {
|
|
319
|
+
path: 'filters.search',
|
|
320
|
+
emptyValues: [''],
|
|
321
|
+
inputTransform: value => value,
|
|
322
|
+
paramsTransform: value => value,
|
|
323
|
+
},
|
|
324
|
+
}
|
|
325
|
+
```
|
|
326
|
+
|
|
327
|
+
* `path` — path used to store the parameter in the state.
|
|
328
|
+
* `emptyValues` — values treated as empty.
|
|
329
|
+
* `inputTransform` — transforms values before storing them in the state or URL.
|
|
330
|
+
* `paramsTransform` — transforms values read from the URL before storing them in the state.
|
|
331
|
+
|
|
332
|
+
### Options
|
|
333
|
+
|
|
334
|
+
The third argument can be used to configure initial parameter synchronization:
|
|
335
|
+
|
|
336
|
+
```js
|
|
337
|
+
useQueryParams(initialState, keys, {
|
|
338
|
+
syncInitialParams: true,
|
|
339
|
+
});
|
|
340
|
+
```
|
|
341
|
+
|
|
342
|
+
`syncInitialParams` defaults to `true`.
|
|
343
|
+
|
|
344
|
+
---
|
|
345
|
+
|
|
346
|
+
## License
|
|
347
|
+
|
|
348
|
+
MIT
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
import { useState } from "react";
|
|
2
|
+
import Fetch from 'tanbal-utils';
|
|
3
|
+
|
|
4
|
+
const useDataManager = (url) => {
|
|
5
|
+
const [data, setData] = useState([]);
|
|
6
|
+
const [status, setStatus] = useState('idle');
|
|
7
|
+
|
|
8
|
+
const load = async (
|
|
9
|
+
successCallback = () => {},
|
|
10
|
+
failCallback = () => {},
|
|
11
|
+
finallyCallback = () => {},
|
|
12
|
+
) => {
|
|
13
|
+
try {
|
|
14
|
+
setStatus('loading');
|
|
15
|
+
const result = await Fetch(url);
|
|
16
|
+
setData(result.data);
|
|
17
|
+
setStatus('loaded');
|
|
18
|
+
successCallback(result.data);
|
|
19
|
+
}
|
|
20
|
+
catch (error) {
|
|
21
|
+
setStatus('failed');
|
|
22
|
+
failCallback(error);
|
|
23
|
+
throw error;
|
|
24
|
+
}
|
|
25
|
+
finally {
|
|
26
|
+
finallyCallback();
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const add = async (
|
|
31
|
+
data,
|
|
32
|
+
successCallback = () => {},
|
|
33
|
+
failCallback = () => {},
|
|
34
|
+
finallyCallback = () => {},
|
|
35
|
+
) => {
|
|
36
|
+
try {
|
|
37
|
+
const result = await Fetch(url, {
|
|
38
|
+
method: 'POST',
|
|
39
|
+
headers: {
|
|
40
|
+
'Content-Type': 'application/json',
|
|
41
|
+
},
|
|
42
|
+
body: JSON.stringify(data),
|
|
43
|
+
});
|
|
44
|
+
load();
|
|
45
|
+
successCallback(result);
|
|
46
|
+
return result;
|
|
47
|
+
}
|
|
48
|
+
catch (error) {
|
|
49
|
+
failCallback(error);
|
|
50
|
+
throw error;
|
|
51
|
+
}
|
|
52
|
+
finally {
|
|
53
|
+
finallyCallback();
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const update = async (
|
|
58
|
+
id,
|
|
59
|
+
newData,
|
|
60
|
+
successCallback = () => {},
|
|
61
|
+
failCallback = () => {},
|
|
62
|
+
finallyCallback = () => {},
|
|
63
|
+
) => {
|
|
64
|
+
|
|
65
|
+
try {
|
|
66
|
+
const result = await Fetch(`${url}/${id}`, {
|
|
67
|
+
method: 'PATCH',
|
|
68
|
+
headers: {
|
|
69
|
+
'Content-Type': 'application/json',
|
|
70
|
+
},
|
|
71
|
+
body: JSON.stringify(newData),
|
|
72
|
+
});
|
|
73
|
+
load();
|
|
74
|
+
successCallback(result);
|
|
75
|
+
return result;
|
|
76
|
+
}
|
|
77
|
+
catch (error) {
|
|
78
|
+
failCallback(error);
|
|
79
|
+
throw error;
|
|
80
|
+
}
|
|
81
|
+
finally {
|
|
82
|
+
finallyCallback();
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const updateMultiple = async (
|
|
87
|
+
updates,
|
|
88
|
+
successCallback = () => {},
|
|
89
|
+
failCallback = () => {},
|
|
90
|
+
finallyCallback = () => {},
|
|
91
|
+
) => {
|
|
92
|
+
try {
|
|
93
|
+
const result = await Fetch(url, {
|
|
94
|
+
method: 'PATCH',
|
|
95
|
+
headers: {
|
|
96
|
+
'Content-Type': 'application/json',
|
|
97
|
+
},
|
|
98
|
+
body: JSON.stringify(updates),
|
|
99
|
+
});
|
|
100
|
+
load();
|
|
101
|
+
successCallback(result);
|
|
102
|
+
return result;
|
|
103
|
+
}
|
|
104
|
+
catch (error) {
|
|
105
|
+
failCallback(error);
|
|
106
|
+
throw error;
|
|
107
|
+
}
|
|
108
|
+
finally {
|
|
109
|
+
finallyCallback();
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const remove = async (
|
|
114
|
+
id,
|
|
115
|
+
successCallback = () => {},
|
|
116
|
+
failCallback = () => {},
|
|
117
|
+
finallyCallback = () => {},
|
|
118
|
+
) => {
|
|
119
|
+
try {
|
|
120
|
+
const result = await Fetch(`${url}/${id}`, {
|
|
121
|
+
method: 'DELETE',
|
|
122
|
+
headers: {
|
|
123
|
+
'Content-Type': 'application/json',
|
|
124
|
+
},
|
|
125
|
+
body: JSON.stringify(id),
|
|
126
|
+
});
|
|
127
|
+
load();
|
|
128
|
+
successCallback(result);
|
|
129
|
+
return result;
|
|
130
|
+
}
|
|
131
|
+
catch (error) {
|
|
132
|
+
failCallback(error);
|
|
133
|
+
throw error;
|
|
134
|
+
}
|
|
135
|
+
finally {
|
|
136
|
+
finallyCallback();
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
const get = (
|
|
141
|
+
which,
|
|
142
|
+
key = 'id',
|
|
143
|
+
) => {
|
|
144
|
+
return data.find(item => item[key] === which);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
return [data, status, {
|
|
148
|
+
load,
|
|
149
|
+
add,
|
|
150
|
+
update,
|
|
151
|
+
updateMultiple,
|
|
152
|
+
remove,
|
|
153
|
+
get,
|
|
154
|
+
}];
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
export default useDataManager;
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { useEffect, useState } from "react";
|
|
2
|
+
|
|
3
|
+
function useLocalStorage(key, initialValue) {
|
|
4
|
+
|
|
5
|
+
const [value, setValue] = useState(initialValue);
|
|
6
|
+
const [initialized, setInitialized] = useState(false);
|
|
7
|
+
|
|
8
|
+
useEffect(() => {
|
|
9
|
+
try {
|
|
10
|
+
const item = localStorage.getItem(key);
|
|
11
|
+
if (item !== null) {
|
|
12
|
+
setValue(JSON.parse(item));
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
catch {
|
|
16
|
+
// use initialValue
|
|
17
|
+
}
|
|
18
|
+
finally {
|
|
19
|
+
setInitialized(true);
|
|
20
|
+
}
|
|
21
|
+
}, [key]);
|
|
22
|
+
|
|
23
|
+
useEffect(() => {
|
|
24
|
+
if (!initialized) return;
|
|
25
|
+
localStorage.setItem(key, JSON.stringify(value));
|
|
26
|
+
}, [key, value, initialized]);
|
|
27
|
+
|
|
28
|
+
return [value, setValue];
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export default useLocalStorage;
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { useEffect, useRef } from "react"
|
|
2
|
+
|
|
3
|
+
const useClickOutside = (
|
|
4
|
+
onOutsideClick,
|
|
5
|
+
isActive = false
|
|
6
|
+
) => {
|
|
7
|
+
const ref = useRef(null);
|
|
8
|
+
|
|
9
|
+
useEffect(() => {
|
|
10
|
+
if (!isActive) return;
|
|
11
|
+
const handleClick = event => {
|
|
12
|
+
if (ref?.current && !ref.current.contains(event.target)) {
|
|
13
|
+
onOutsideClick()
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
document.addEventListener('mousedown', handleClick)
|
|
18
|
+
return () => document.removeEventListener('mousedown', handleClick)
|
|
19
|
+
}, [ref, onOutsideClick, isActive]);
|
|
20
|
+
|
|
21
|
+
return ref;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export default useClickOutside;
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { useRef } from "react";
|
|
2
|
+
|
|
3
|
+
const useInputBlur = (
|
|
4
|
+
time,
|
|
5
|
+
callback = () => {}
|
|
6
|
+
) => {
|
|
7
|
+
const ref = useRef(null);
|
|
8
|
+
const timer = useRef(null);
|
|
9
|
+
const callbackRef = useRef(callback);
|
|
10
|
+
|
|
11
|
+
useEffect(() => {
|
|
12
|
+
const input = ref?.current;
|
|
13
|
+
if (!input) return;
|
|
14
|
+
const handleInput = () => {
|
|
15
|
+
clearTimeout(timer.current);
|
|
16
|
+
|
|
17
|
+
timer.current = setTimeout(() => {
|
|
18
|
+
callbackRef.current(input.value);
|
|
19
|
+
}, time);
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
const handleBlur = () => {
|
|
23
|
+
clearTimeout(timer.current);
|
|
24
|
+
callbackRef.current(input.value);
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
input.addEventListener('input', handleInput);
|
|
28
|
+
input.addEventListener('blur', handleBlur);
|
|
29
|
+
|
|
30
|
+
return () => {
|
|
31
|
+
input.removeEventListener('input', handleInput);
|
|
32
|
+
input.removeEventListener('blur', handleBlur);
|
|
33
|
+
clearTimeout(timer.current);
|
|
34
|
+
};
|
|
35
|
+
}, [ref, time]);
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
useEffect(() => {
|
|
39
|
+
callbackRef.current = callback;
|
|
40
|
+
}, [callback]);
|
|
41
|
+
|
|
42
|
+
return ref;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export default useInputBlur;
|
package/index.js
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import useDataManager from "./data-manager/useDataManager";
|
|
2
|
+
import useLocalStorage from "./data-manager/useLocalStorage";
|
|
3
|
+
import useClickOutside from "./event/useClickOutside";
|
|
4
|
+
import useInputBlur from "./event/useInputBlur";
|
|
5
|
+
import useQueryParams from './useQueryParams';
|
|
6
|
+
|
|
7
|
+
export {
|
|
8
|
+
useDataManager,
|
|
9
|
+
useLocalStorage,
|
|
10
|
+
useClickOutside,
|
|
11
|
+
useInputBlur,
|
|
12
|
+
useQueryParams,
|
|
13
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "tanbal-hooks",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "A collection of custom React and Next.js hooks.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"react",
|
|
7
|
+
"hooks",
|
|
8
|
+
"react-hooks",
|
|
9
|
+
"custom-hooks",
|
|
10
|
+
"nextjs"
|
|
11
|
+
],
|
|
12
|
+
"homepage": "https://github.com/tanbal8/hooks#readme",
|
|
13
|
+
"bugs": {
|
|
14
|
+
"url": "https://github.com/tanbal8/hooks/issues"
|
|
15
|
+
},
|
|
16
|
+
"repository": {
|
|
17
|
+
"type": "git",
|
|
18
|
+
"url": "git+https://github.com/tanbal8/hooks.git"
|
|
19
|
+
},
|
|
20
|
+
"license": "MIT",
|
|
21
|
+
"author": "Ali Radmard",
|
|
22
|
+
"type": "module",
|
|
23
|
+
"main": "index.js",
|
|
24
|
+
"scripts": {
|
|
25
|
+
"test": "echo \"Error: no test specified\" && exit 1"
|
|
26
|
+
},
|
|
27
|
+
"peerDependencies": {
|
|
28
|
+
"next": ">=13",
|
|
29
|
+
"react": ">=18"
|
|
30
|
+
},
|
|
31
|
+
"dependencies": {
|
|
32
|
+
"tanbal-utils": "^1.0.0"
|
|
33
|
+
}
|
|
34
|
+
}
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
import { useSearchParams, useRouter } from "next/navigation";
|
|
2
|
+
import { useEffect, useState } from "react";
|
|
3
|
+
import { set as setObject, get as getObject } from 'tanbal-utils';
|
|
4
|
+
|
|
5
|
+
const useQueryParams = (initialState = {}, keys = {}, options = {}) => {
|
|
6
|
+
const searchParams = useSearchParams();
|
|
7
|
+
const router = useRouter();
|
|
8
|
+
const [state, setState] = useState(initialState);
|
|
9
|
+
const { syncInitialParams = true } = options;
|
|
10
|
+
|
|
11
|
+
const isEmpty = (value, key) => {
|
|
12
|
+
const emptyValues = getEmptyValues(key);
|
|
13
|
+
return !value || emptyValues.includes(value);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const getPath = (key) => {
|
|
17
|
+
return keys?.[key]?.['path'];
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const getEmptyValues = (key) => {
|
|
21
|
+
return keys?.[key]?.emptyValues || [''];
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const inputTransform = (key, value) => {
|
|
25
|
+
const callback = keys?.[key]?.inputTransform || (value => value);
|
|
26
|
+
return callback(value);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const paramsTransform = (key, value) => {
|
|
30
|
+
const callback = keys?.[key]?.paramsTransform || (value => value);
|
|
31
|
+
return callback(value);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const replaceParams = (params) => {
|
|
35
|
+
router.replace(`?${params.toString()}`, { scroll: false });
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const validateKey = key => {
|
|
39
|
+
if (!Object.hasOwn(keys, key)) {
|
|
40
|
+
throw new Error(`Invalid query param key: ${key}`);
|
|
41
|
+
}
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
const forEachKey = (
|
|
45
|
+
keys,
|
|
46
|
+
path = value => value,
|
|
47
|
+
notEmptyCallback = () => {},
|
|
48
|
+
emptyCallback = () => {},
|
|
49
|
+
) => {
|
|
50
|
+
Object.entries(keys).forEach(([key, config]) => {
|
|
51
|
+
const newValue = inputTransform(key, path(config));
|
|
52
|
+
const empty = isEmpty(newValue, key);
|
|
53
|
+
if (empty) emptyCallback(key, newValue);
|
|
54
|
+
else notEmptyCallback(key, newValue);
|
|
55
|
+
});
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
const get = (key) => {
|
|
59
|
+
validateKey(key);
|
|
60
|
+
const value = searchParams.get(key);
|
|
61
|
+
if (isEmpty(value, key)) return null;
|
|
62
|
+
return value;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const getStateFromParams = (prevState) => {
|
|
66
|
+
let newState = { ...prevState };
|
|
67
|
+
Object.keys(keys).forEach(key => {
|
|
68
|
+
const value = get(key);
|
|
69
|
+
if (value !== null) {
|
|
70
|
+
newState = setObject(
|
|
71
|
+
newState,
|
|
72
|
+
getPath(key),
|
|
73
|
+
paramsTransform(key, value),
|
|
74
|
+
);
|
|
75
|
+
}
|
|
76
|
+
});
|
|
77
|
+
return newState;
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
const removeParam = key => {
|
|
81
|
+
const params = new URLSearchParams(searchParams);
|
|
82
|
+
params.delete(key);
|
|
83
|
+
replaceParams(params);
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
const setParam = (key, value) => {
|
|
88
|
+
const params = new URLSearchParams(searchParams);
|
|
89
|
+
params.set(key, value);
|
|
90
|
+
replaceParams(params);
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
const remove = (key, replace = true) => {
|
|
94
|
+
validateKey(key);
|
|
95
|
+
setState(prevState =>
|
|
96
|
+
setObject(prevState, getPath(key), '')
|
|
97
|
+
);
|
|
98
|
+
if (replace) removeParam(key);
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
const set = (key, value, replace = true) => {
|
|
102
|
+
validateKey(key);
|
|
103
|
+
const newValue = inputTransform(key, value);
|
|
104
|
+
if (isEmpty(newValue, key)) {
|
|
105
|
+
remove(key, replace);
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
setState(prevState =>
|
|
109
|
+
setObject(prevState, getPath(key), newValue)
|
|
110
|
+
);
|
|
111
|
+
if (replace) setParam(key, newValue);
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
const setMultiple = (newValues) => {
|
|
115
|
+
const params = new URLSearchParams(searchParams);
|
|
116
|
+
forEachKey(
|
|
117
|
+
newValues,
|
|
118
|
+
value => value,
|
|
119
|
+
(key, value) => params.set(key, value),
|
|
120
|
+
key => params.delete(key),
|
|
121
|
+
);
|
|
122
|
+
setState(prevState => {
|
|
123
|
+
let newState = prevState;
|
|
124
|
+
forEachKey(
|
|
125
|
+
newValues,
|
|
126
|
+
value => value,
|
|
127
|
+
(key, value) => {
|
|
128
|
+
newState = setObject(newState, getPath(key), value);
|
|
129
|
+
},
|
|
130
|
+
key => {
|
|
131
|
+
newState = setObject(newState, getPath(key), '');
|
|
132
|
+
},
|
|
133
|
+
);
|
|
134
|
+
return newState;
|
|
135
|
+
});
|
|
136
|
+
replaceParams(params);
|
|
137
|
+
};
|
|
138
|
+
|
|
139
|
+
const removeParams = (paramsList = []) => {
|
|
140
|
+
const params = new URLSearchParams(searchParams);
|
|
141
|
+
paramsList.forEach(key => {
|
|
142
|
+
params.delete(key);
|
|
143
|
+
});
|
|
144
|
+
setState(prevState => {
|
|
145
|
+
let newState = prevState;
|
|
146
|
+
paramsList.forEach(key => {
|
|
147
|
+
newState = setObject(newState, getPath(key), '');
|
|
148
|
+
});
|
|
149
|
+
return newState;
|
|
150
|
+
});
|
|
151
|
+
replaceParams(params);
|
|
152
|
+
};
|
|
153
|
+
|
|
154
|
+
const reset = () => {
|
|
155
|
+
removeParams(Object.keys(keys));
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
useEffect(() => {
|
|
159
|
+
setState(prevState => getStateFromParams(prevState));
|
|
160
|
+
}, [searchParams]);
|
|
161
|
+
|
|
162
|
+
useEffect(() => {
|
|
163
|
+
if (!syncInitialParams) return;
|
|
164
|
+
const params = new URLSearchParams(searchParams);
|
|
165
|
+
forEachKey(
|
|
166
|
+
keys,
|
|
167
|
+
config => getObject(initialState, config.path),
|
|
168
|
+
(key, value) => {
|
|
169
|
+
if (!params.has(key)) {
|
|
170
|
+
params.set(key, value);
|
|
171
|
+
}
|
|
172
|
+
},
|
|
173
|
+
);
|
|
174
|
+
replaceParams(params);
|
|
175
|
+
}, []);
|
|
176
|
+
|
|
177
|
+
return [{
|
|
178
|
+
get,
|
|
179
|
+
set,
|
|
180
|
+
setMultiple,
|
|
181
|
+
remove,
|
|
182
|
+
removeParams,
|
|
183
|
+
reset,
|
|
184
|
+
getPath,
|
|
185
|
+
}, state, setState];
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
export default useQueryParams;
|