@kaiserofthenight/human-js 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/README.md +545 -0
- package/examples/api-example/app.js +365 -0
- package/examples/counter/app.js +201 -0
- package/examples/todo-app/app.js +378 -0
- package/examples/user-dashboard/app.js +0 -0
- package/package.json +66 -0
- package/src/core/component.js +182 -0
- package/src/core/events.js +130 -0
- package/src/core/render.js +151 -0
- package/src/core/router.js +182 -0
- package/src/core/state.js +114 -0
- package/src/index.js +63 -0
- package/src/plugins/http.js +167 -0
- package/src/plugins/storage.js +181 -0
- package/src/plugins/validator.js +193 -0
- package/src/utils/dom.js +0 -0
- package/src/utils/helpers.js +209 -0
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* HTTP CLIENT PLUGIN
|
|
3
|
+
*
|
|
4
|
+
* Simple fetch wrapper with better ergonomics.
|
|
5
|
+
* Inspired by Axios but much simpler.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Create an HTTP client
|
|
10
|
+
* @param {Object} config - Default configuration
|
|
11
|
+
*/
|
|
12
|
+
export function createHttp(config = {}) {
|
|
13
|
+
const {
|
|
14
|
+
baseURL = '',
|
|
15
|
+
headers = {},
|
|
16
|
+
timeout = 30000,
|
|
17
|
+
onRequest = null,
|
|
18
|
+
onResponse = null,
|
|
19
|
+
onError = null
|
|
20
|
+
} = config;
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Make HTTP request
|
|
24
|
+
*/
|
|
25
|
+
async function request(url, options = {}) {
|
|
26
|
+
// Merge headers
|
|
27
|
+
const requestHeaders = {
|
|
28
|
+
'Content-Type': 'application/json',
|
|
29
|
+
...headers,
|
|
30
|
+
...options.headers
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
// Build full URL
|
|
34
|
+
const fullUrl = url.startsWith('http') ? url : `${baseURL}${url}`;
|
|
35
|
+
|
|
36
|
+
// Request config
|
|
37
|
+
const fetchOptions = {
|
|
38
|
+
method: options.method || 'GET',
|
|
39
|
+
headers: requestHeaders,
|
|
40
|
+
...options
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
// Add body for non-GET requests
|
|
44
|
+
if (options.body && typeof options.body === 'object') {
|
|
45
|
+
fetchOptions.body = JSON.stringify(options.body);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// Call onRequest interceptor
|
|
49
|
+
if (onRequest) {
|
|
50
|
+
const modified = onRequest(fetchOptions);
|
|
51
|
+
if (modified) Object.assign(fetchOptions, modified);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
try {
|
|
55
|
+
// Create abort controller for timeout
|
|
56
|
+
const controller = new AbortController();
|
|
57
|
+
const timeoutId = setTimeout(() => controller.abort(), timeout);
|
|
58
|
+
fetchOptions.signal = controller.signal;
|
|
59
|
+
|
|
60
|
+
// Make request
|
|
61
|
+
const response = await fetch(fullUrl, fetchOptions);
|
|
62
|
+
clearTimeout(timeoutId);
|
|
63
|
+
|
|
64
|
+
// Parse response
|
|
65
|
+
let data;
|
|
66
|
+
const contentType = response.headers.get('content-type');
|
|
67
|
+
|
|
68
|
+
if (contentType && contentType.includes('application/json')) {
|
|
69
|
+
data = await response.json();
|
|
70
|
+
} else {
|
|
71
|
+
data = await response.text();
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Build response object
|
|
75
|
+
const result = {
|
|
76
|
+
data,
|
|
77
|
+
status: response.status,
|
|
78
|
+
statusText: response.statusText,
|
|
79
|
+
headers: response.headers,
|
|
80
|
+
ok: response.ok
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
// Call onResponse interceptor
|
|
84
|
+
if (onResponse) {
|
|
85
|
+
const modified = onResponse(result);
|
|
86
|
+
if (modified) return modified;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// Throw on error status
|
|
90
|
+
if (!response.ok) {
|
|
91
|
+
throw new HttpError(result.statusText, result);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
return result;
|
|
95
|
+
} catch (error) {
|
|
96
|
+
// Call onError interceptor
|
|
97
|
+
if (onError) {
|
|
98
|
+
onError(error);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
throw error;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// Convenience methods
|
|
106
|
+
return {
|
|
107
|
+
request,
|
|
108
|
+
|
|
109
|
+
get(url, options = {}) {
|
|
110
|
+
return request(url, { ...options, method: 'GET' });
|
|
111
|
+
},
|
|
112
|
+
|
|
113
|
+
post(url, body, options = {}) {
|
|
114
|
+
return request(url, { ...options, method: 'POST', body });
|
|
115
|
+
},
|
|
116
|
+
|
|
117
|
+
put(url, body, options = {}) {
|
|
118
|
+
return request(url, { ...options, method: 'PUT', body });
|
|
119
|
+
},
|
|
120
|
+
|
|
121
|
+
patch(url, body, options = {}) {
|
|
122
|
+
return request(url, { ...options, method: 'PATCH', body });
|
|
123
|
+
},
|
|
124
|
+
|
|
125
|
+
delete(url, options = {}) {
|
|
126
|
+
return request(url, { ...options, method: 'DELETE' });
|
|
127
|
+
}
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Custom HTTP error
|
|
133
|
+
*/
|
|
134
|
+
class HttpError extends Error {
|
|
135
|
+
constructor(message, response) {
|
|
136
|
+
super(message);
|
|
137
|
+
this.name = 'HttpError';
|
|
138
|
+
this.response = response;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Default HTTP client instance
|
|
144
|
+
*/
|
|
145
|
+
export const http = createHttp();
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Example usage:
|
|
149
|
+
*
|
|
150
|
+
* // Simple request
|
|
151
|
+
* const { data } = await http.get('/api/users');
|
|
152
|
+
*
|
|
153
|
+
* // With custom client
|
|
154
|
+
* const api = createHttp({
|
|
155
|
+
* baseURL: 'https://api.example.com',
|
|
156
|
+
* headers: { 'Authorization': 'Bearer token' },
|
|
157
|
+
* onRequest: (config) => {
|
|
158
|
+
* console.log('Making request:', config);
|
|
159
|
+
* },
|
|
160
|
+
* onError: (error) => {
|
|
161
|
+
* console.error('Request failed:', error);
|
|
162
|
+
* }
|
|
163
|
+
* });
|
|
164
|
+
*
|
|
165
|
+
* const users = await api.get('/users');
|
|
166
|
+
* await api.post('/users', { name: 'John' });
|
|
167
|
+
*/
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* STORAGE PLUGIN
|
|
3
|
+
*
|
|
4
|
+
* Simple wrapper around localStorage and sessionStorage.
|
|
5
|
+
* Automatically handles JSON serialization.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Create a storage instance
|
|
10
|
+
* @param {Storage} storage - localStorage or sessionStorage
|
|
11
|
+
* @param {String} prefix - Key prefix for namespacing
|
|
12
|
+
*/
|
|
13
|
+
function createStorage(storage, prefix = '') {
|
|
14
|
+
return {
|
|
15
|
+
/**
|
|
16
|
+
* Set item
|
|
17
|
+
*/
|
|
18
|
+
set(key, value) {
|
|
19
|
+
try {
|
|
20
|
+
const fullKey = prefix + key;
|
|
21
|
+
const serialized = JSON.stringify(value);
|
|
22
|
+
storage.setItem(fullKey, serialized);
|
|
23
|
+
return true;
|
|
24
|
+
} catch (error) {
|
|
25
|
+
console.error('[Storage] Set failed:', error);
|
|
26
|
+
return false;
|
|
27
|
+
}
|
|
28
|
+
},
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Get item
|
|
32
|
+
*/
|
|
33
|
+
get(key, defaultValue = null) {
|
|
34
|
+
try {
|
|
35
|
+
const fullKey = prefix + key;
|
|
36
|
+
const item = storage.getItem(fullKey);
|
|
37
|
+
|
|
38
|
+
if (item === null) return defaultValue;
|
|
39
|
+
|
|
40
|
+
return JSON.parse(item);
|
|
41
|
+
} catch (error) {
|
|
42
|
+
console.error('[Storage] Get failed:', error);
|
|
43
|
+
return defaultValue;
|
|
44
|
+
}
|
|
45
|
+
},
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Remove item
|
|
49
|
+
*/
|
|
50
|
+
remove(key) {
|
|
51
|
+
try {
|
|
52
|
+
const fullKey = prefix + key;
|
|
53
|
+
storage.removeItem(fullKey);
|
|
54
|
+
return true;
|
|
55
|
+
} catch (error) {
|
|
56
|
+
console.error('[Storage] Remove failed:', error);
|
|
57
|
+
return false;
|
|
58
|
+
}
|
|
59
|
+
},
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Clear all items (with prefix)
|
|
63
|
+
*/
|
|
64
|
+
clear() {
|
|
65
|
+
try {
|
|
66
|
+
if (prefix) {
|
|
67
|
+
// Clear only items with prefix
|
|
68
|
+
const keys = Object.keys(storage);
|
|
69
|
+
keys.forEach(key => {
|
|
70
|
+
if (key.startsWith(prefix)) {
|
|
71
|
+
storage.removeItem(key);
|
|
72
|
+
}
|
|
73
|
+
});
|
|
74
|
+
} else {
|
|
75
|
+
// Clear everything
|
|
76
|
+
storage.clear();
|
|
77
|
+
}
|
|
78
|
+
return true;
|
|
79
|
+
} catch (error) {
|
|
80
|
+
console.error('[Storage] Clear failed:', error);
|
|
81
|
+
return false;
|
|
82
|
+
}
|
|
83
|
+
},
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Get all keys
|
|
87
|
+
*/
|
|
88
|
+
keys() {
|
|
89
|
+
const allKeys = Object.keys(storage);
|
|
90
|
+
return allKeys
|
|
91
|
+
.filter(key => !prefix || key.startsWith(prefix))
|
|
92
|
+
.map(key => prefix ? key.slice(prefix.length) : key);
|
|
93
|
+
},
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Check if key exists
|
|
97
|
+
*/
|
|
98
|
+
has(key) {
|
|
99
|
+
const fullKey = prefix + key;
|
|
100
|
+
return storage.getItem(fullKey) !== null;
|
|
101
|
+
},
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Get multiple items
|
|
105
|
+
*/
|
|
106
|
+
getMultiple(keys) {
|
|
107
|
+
return keys.reduce((result, key) => {
|
|
108
|
+
result[key] = this.get(key);
|
|
109
|
+
return result;
|
|
110
|
+
}, {});
|
|
111
|
+
},
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Set multiple items
|
|
115
|
+
*/
|
|
116
|
+
setMultiple(items) {
|
|
117
|
+
Object.keys(items).forEach(key => {
|
|
118
|
+
this.set(key, items[key]);
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Local storage (persists across sessions)
|
|
126
|
+
*/
|
|
127
|
+
export const local = createStorage(localStorage);
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Session storage (cleared when browser closes)
|
|
131
|
+
*/
|
|
132
|
+
export const session = createStorage(sessionStorage);
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Create namespaced storage
|
|
136
|
+
*/
|
|
137
|
+
export function createNamespace(namespace) {
|
|
138
|
+
return {
|
|
139
|
+
local: createStorage(localStorage, `${namespace}:`),
|
|
140
|
+
session: createStorage(sessionStorage, `${namespace}:`)
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Storage events - listen for changes
|
|
146
|
+
*/
|
|
147
|
+
export function onStorageChange(callback) {
|
|
148
|
+
const handler = (event) => {
|
|
149
|
+
if (event.storageArea === localStorage) {
|
|
150
|
+
callback({
|
|
151
|
+
key: event.key,
|
|
152
|
+
oldValue: event.oldValue ? JSON.parse(event.oldValue) : null,
|
|
153
|
+
newValue: event.newValue ? JSON.parse(event.newValue) : null,
|
|
154
|
+
storage: 'local'
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
};
|
|
158
|
+
|
|
159
|
+
window.addEventListener('storage', handler);
|
|
160
|
+
|
|
161
|
+
return () => window.removeEventListener('storage', handler);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Example usage:
|
|
166
|
+
*
|
|
167
|
+
* import { local, session } from './storage.js';
|
|
168
|
+
*
|
|
169
|
+
* // Save data
|
|
170
|
+
* local.set('user', { name: 'John', age: 30 });
|
|
171
|
+
*
|
|
172
|
+
* // Get data
|
|
173
|
+
* const user = local.get('user');
|
|
174
|
+
*
|
|
175
|
+
* // With default value
|
|
176
|
+
* const settings = local.get('settings', { theme: 'light' });
|
|
177
|
+
*
|
|
178
|
+
* // Namespaced storage
|
|
179
|
+
* const appStorage = createNamespace('myapp');
|
|
180
|
+
* appStorage.local.set('data', { ... });
|
|
181
|
+
*/
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* FORM VALIDATION PLUGIN
|
|
3
|
+
*
|
|
4
|
+
* Simple, human-readable validation rules for forms.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Validation rules
|
|
9
|
+
*/
|
|
10
|
+
export const rules = {
|
|
11
|
+
required: (value) => {
|
|
12
|
+
if (value === null || value === undefined || value === '') {
|
|
13
|
+
return 'This field is required';
|
|
14
|
+
}
|
|
15
|
+
return true;
|
|
16
|
+
},
|
|
17
|
+
|
|
18
|
+
email: (value) => {
|
|
19
|
+
if (!value) return true; // Use with required for mandatory
|
|
20
|
+
const regex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
21
|
+
return regex.test(value) || 'Invalid email address';
|
|
22
|
+
},
|
|
23
|
+
|
|
24
|
+
minLength: (min) => (value) => {
|
|
25
|
+
if (!value) return true;
|
|
26
|
+
return value.length >= min || `Minimum ${min} characters required`;
|
|
27
|
+
},
|
|
28
|
+
|
|
29
|
+
maxLength: (max) => (value) => {
|
|
30
|
+
if (!value) return true;
|
|
31
|
+
return value.length <= max || `Maximum ${max} characters allowed`;
|
|
32
|
+
},
|
|
33
|
+
|
|
34
|
+
min: (minValue) => (value) => {
|
|
35
|
+
const num = Number(value);
|
|
36
|
+
return num >= minValue || `Minimum value is ${minValue}`;
|
|
37
|
+
},
|
|
38
|
+
|
|
39
|
+
max: (maxValue) => (value) => {
|
|
40
|
+
const num = Number(value);
|
|
41
|
+
return num <= maxValue || `Maximum value is ${maxValue}`;
|
|
42
|
+
},
|
|
43
|
+
|
|
44
|
+
pattern: (regex, message = 'Invalid format') => (value) => {
|
|
45
|
+
if (!value) return true;
|
|
46
|
+
return regex.test(value) || message;
|
|
47
|
+
},
|
|
48
|
+
|
|
49
|
+
url: (value) => {
|
|
50
|
+
if (!value) return true;
|
|
51
|
+
try {
|
|
52
|
+
new URL(value);
|
|
53
|
+
return true;
|
|
54
|
+
} catch {
|
|
55
|
+
return 'Invalid URL';
|
|
56
|
+
}
|
|
57
|
+
},
|
|
58
|
+
|
|
59
|
+
number: (value) => {
|
|
60
|
+
if (!value) return true;
|
|
61
|
+
return !isNaN(value) || 'Must be a number';
|
|
62
|
+
},
|
|
63
|
+
|
|
64
|
+
integer: (value) => {
|
|
65
|
+
if (!value) return true;
|
|
66
|
+
return Number.isInteger(Number(value)) || 'Must be an integer';
|
|
67
|
+
},
|
|
68
|
+
|
|
69
|
+
match: (otherField, fieldName) => (value, allValues) => {
|
|
70
|
+
return value === allValues[otherField] || `Must match ${fieldName}`;
|
|
71
|
+
},
|
|
72
|
+
|
|
73
|
+
custom: (fn, message) => (value, allValues) => {
|
|
74
|
+
return fn(value, allValues) || message;
|
|
75
|
+
}
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Create a validator
|
|
80
|
+
* @param {Object} schema - Validation schema { field: [rules] }
|
|
81
|
+
*/
|
|
82
|
+
export function createValidator(schema) {
|
|
83
|
+
return {
|
|
84
|
+
/**
|
|
85
|
+
* Validate all fields
|
|
86
|
+
*/
|
|
87
|
+
validate(values) {
|
|
88
|
+
const errors = {};
|
|
89
|
+
|
|
90
|
+
Object.keys(schema).forEach(field => {
|
|
91
|
+
const fieldRules = schema[field];
|
|
92
|
+
const value = values[field];
|
|
93
|
+
|
|
94
|
+
for (const rule of fieldRules) {
|
|
95
|
+
const result = rule(value, values);
|
|
96
|
+
if (result !== true) {
|
|
97
|
+
errors[field] = result;
|
|
98
|
+
break; // Stop at first error
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
return {
|
|
104
|
+
isValid: Object.keys(errors).length === 0,
|
|
105
|
+
errors
|
|
106
|
+
};
|
|
107
|
+
},
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Validate single field
|
|
111
|
+
*/
|
|
112
|
+
validateField(field, value, allValues = {}) {
|
|
113
|
+
const fieldRules = schema[field];
|
|
114
|
+
if (!fieldRules) return { isValid: true, error: null };
|
|
115
|
+
|
|
116
|
+
for (const rule of fieldRules) {
|
|
117
|
+
const result = rule(value, allValues);
|
|
118
|
+
if (result !== true) {
|
|
119
|
+
return { isValid: false, error: result };
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
return { isValid: true, error: null };
|
|
124
|
+
}
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Form helper - extracts form data
|
|
130
|
+
*/
|
|
131
|
+
export function getFormData(formElement) {
|
|
132
|
+
const formData = new FormData(formElement);
|
|
133
|
+
const data = {};
|
|
134
|
+
|
|
135
|
+
for (const [key, value] of formData.entries()) {
|
|
136
|
+
// Handle multiple checkboxes with same name
|
|
137
|
+
if (data[key]) {
|
|
138
|
+
if (Array.isArray(data[key])) {
|
|
139
|
+
data[key].push(value);
|
|
140
|
+
} else {
|
|
141
|
+
data[key] = [data[key], value];
|
|
142
|
+
}
|
|
143
|
+
} else {
|
|
144
|
+
data[key] = value;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
return data;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Display validation errors in form
|
|
153
|
+
*/
|
|
154
|
+
export function displayErrors(formElement, errors) {
|
|
155
|
+
// Clear previous errors
|
|
156
|
+
formElement.querySelectorAll('.error-message').forEach(el => el.remove());
|
|
157
|
+
formElement.querySelectorAll('.error').forEach(el => el.classList.remove('error'));
|
|
158
|
+
|
|
159
|
+
// Display new errors
|
|
160
|
+
Object.keys(errors).forEach(field => {
|
|
161
|
+
const input = formElement.querySelector(`[name="${field}"]`);
|
|
162
|
+
if (!input) return;
|
|
163
|
+
|
|
164
|
+
// Add error class
|
|
165
|
+
input.classList.add('error');
|
|
166
|
+
|
|
167
|
+
// Create error message
|
|
168
|
+
const errorDiv = document.createElement('div');
|
|
169
|
+
errorDiv.className = 'error-message';
|
|
170
|
+
errorDiv.style.color = 'red';
|
|
171
|
+
errorDiv.style.fontSize = '14px';
|
|
172
|
+
errorDiv.style.marginTop = '4px';
|
|
173
|
+
errorDiv.textContent = errors[field];
|
|
174
|
+
|
|
175
|
+
// Insert after input
|
|
176
|
+
input.parentNode.insertBefore(errorDiv, input.nextSibling);
|
|
177
|
+
});
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* Example usage:
|
|
182
|
+
*
|
|
183
|
+
* const validator = createValidator({
|
|
184
|
+
* email: [rules.required, rules.email],
|
|
185
|
+
* password: [rules.required, rules.minLength(8)],
|
|
186
|
+
* age: [rules.required, rules.number, rules.min(18)]
|
|
187
|
+
* });
|
|
188
|
+
*
|
|
189
|
+
* const result = validator.validate(formData);
|
|
190
|
+
* if (!result.isValid) {
|
|
191
|
+
* displayErrors(formElement, result.errors);
|
|
192
|
+
* }
|
|
193
|
+
*/
|
package/src/utils/dom.js
ADDED
|
File without changes
|