frontacles 0.4.0 → 0.5.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/CHANGELOG.md +15 -1
- package/README.md +125 -8
- package/package.json +15 -12
- package/src/dom/index.js +121 -0
- package/src/index.js +1 -0
- package/types/index.d.ts +11 -0
package/CHANGELOG.md
CHANGED
|
@@ -6,9 +6,23 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm
|
|
|
6
6
|
|
|
7
7
|
Nothing for now.
|
|
8
8
|
|
|
9
|
+
<!-- Compare with [last published version](https://github.com/frontacles/frontacles/compare/0.5.0...main). -->
|
|
10
|
+
|
|
9
11
|
<!-- ⚠️ Before a new release, make sure the documentation doesn't contain any **unreleased** mention. -->
|
|
10
12
|
|
|
11
|
-
|
|
13
|
+
## v0.5.0 (2026-01-24)
|
|
14
|
+
|
|
15
|
+
Compare with [last published version](https://github.com/frontacles/frontacles/compare/0.4.0...0.5.0).
|
|
16
|
+
|
|
17
|
+
### New
|
|
18
|
+
|
|
19
|
+
- Add [`setAttributes`](./README.md#setattributes), a function to update HTML attributes on any number of elements.
|
|
20
|
+
|
|
21
|
+
### Under the hood
|
|
22
|
+
|
|
23
|
+
- Setup tests for DOM utilities using Playwright via Vitest browser mode.
|
|
24
|
+
- Migrate types testing from `tsd` to TSTyche.
|
|
25
|
+
- Bump Node version from 20 to 22.
|
|
12
26
|
|
|
13
27
|
## v0.4.0 (2025-03-08)
|
|
14
28
|
|
package/README.md
CHANGED
|
@@ -7,14 +7,131 @@ Cool utilities for front-end development (and potentially for Node).
|
|
|
7
7
|
|
|
8
8
|
We love tiny bits (using brotli compression):
|
|
9
9
|
|
|
10
|
-
|
|
|
11
|
-
| --- | --- | --- |
|
|
12
|
-
|
|
|
13
|
-
| math | [`
|
|
14
|
-
|
|
|
15
|
-
|
|
|
16
|
-
|
|
|
17
|
-
|
|
|
10
|
+
| category | util | size | description |
|
|
11
|
+
| --- | --- | --- | --- |
|
|
12
|
+
| DOM | [`setAttributes`](#setattributes) | 338 B | Updates attributes of HTML or SVG element(s). |
|
|
13
|
+
| math | [`clamp`](#clamp) | 35 B | Make sure a number stays in a given range. |
|
|
14
|
+
| math | [`round`](#round) | 38 B | Round a number to a given precision. |
|
|
15
|
+
| string | [`capitalize`](#capitalize) | 40 B | Capitalize the first letter of a string. |
|
|
16
|
+
| URL | [`isEmail`](#isemail) | 86 B | Wheither a variable is a valid email address. |
|
|
17
|
+
| URL | [`Email`](#email) | 173 B | An `Email` object with validation and separate access to an email username and domain. |
|
|
18
|
+
| | **everything** | 645 B | |
|
|
19
|
+
|
|
20
|
+
## DOM utils
|
|
21
|
+
|
|
22
|
+
### `setAttributes`
|
|
23
|
+
|
|
24
|
+
Updates attributes of HTML element(s).
|
|
25
|
+
|
|
26
|
+
```js
|
|
27
|
+
import { setAttributes } from 'frontacles/dom'
|
|
28
|
+
|
|
29
|
+
const widget = getElementById('animal-widget')
|
|
30
|
+
|
|
31
|
+
// `<div name="Animal widget" loading="true">`
|
|
32
|
+
setAttributes(widget, {
|
|
33
|
+
name: 'Animal widget'
|
|
34
|
+
loading: true,
|
|
35
|
+
})
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
To remove an attribute, set its value to `false`, `null` or `undefined`.
|
|
39
|
+
|
|
40
|
+
```js
|
|
41
|
+
// `<div name="Animal widget">`
|
|
42
|
+
setAttributes(widget, { loading: false })
|
|
43
|
+
|
|
44
|
+
// `<div name="Animal widget" loading="false">`
|
|
45
|
+
setAttributes(widget, { loading: 'false' })
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
`setAttributes` also accepts multiple elements (array or [HTML collection](https://developer.mozilla.org/en-US/docs/Web/API/HTMLCollection)):
|
|
49
|
+
|
|
50
|
+
```js
|
|
51
|
+
const animals = widget.getElementsbyClassName('.list-item')
|
|
52
|
+
|
|
53
|
+
// `<li class="list-item" data-cat="animals">cat</li>`
|
|
54
|
+
setAttributes(animals, { 'data-cat': 'animals' })
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
When an attribute is an object, its properties are converted to `attrName-propName` attributes, making it helpful for any bulk update of `aria-*` or `data-*` attributes:
|
|
58
|
+
|
|
59
|
+
```js
|
|
60
|
+
setAttributes(el, {
|
|
61
|
+
loading: true,
|
|
62
|
+
aria: {
|
|
63
|
+
busy: true,
|
|
64
|
+
live: 'polite',
|
|
65
|
+
},
|
|
66
|
+
data: {
|
|
67
|
+
category: 'boats',
|
|
68
|
+
'max-items': 12,
|
|
69
|
+
},
|
|
70
|
+
user: {
|
|
71
|
+
id: 4,
|
|
72
|
+
name: 'Liz',
|
|
73
|
+
},
|
|
74
|
+
})
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
The previous example gives:
|
|
78
|
+
|
|
79
|
+
```html
|
|
80
|
+
<div
|
|
81
|
+
loading="true"
|
|
82
|
+
aria-busy="true" aria-live="polite"
|
|
83
|
+
data-category="boats" data-max-items="12"
|
|
84
|
+
user-id="4" user-name="Liz"
|
|
85
|
+
>
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
Like for non-object attributes, using `false`, `null` or `undefined` as property value will remove the matching attribute from the HTML:
|
|
89
|
+
|
|
90
|
+
```js
|
|
91
|
+
setAttributes(el, { data: { 'max-items': null }}) // no more `data-max-items`
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
> [!NOTE]
|
|
95
|
+
> `data` is using [`dataset`](https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/dataset) under the hood, but differs from it: `setAttributes` removes any attribute with a `null` value while `dataset` turn `null` into a string. In the previous example, `el.dataset.maxItems = null` would have given `data-max-items="null"` instead of removing the attribute.
|
|
96
|
+
|
|
97
|
+
`class` updates… CSS classes. It can be an array of CSS classes, a string containing one or more (space-separated) classes, or an object in the form of `{ className: state }`, defining which classes should be added or removed from the element.
|
|
98
|
+
|
|
99
|
+
```js
|
|
100
|
+
// <div class="btn btn--xl">
|
|
101
|
+
setAttributes(el, { class: { btn: true, 'btn--xl': true }})
|
|
102
|
+
|
|
103
|
+
// <div class="btn btn--xl card__btn">
|
|
104
|
+
setAttributes(el, { class: ['card__btn'] })
|
|
105
|
+
|
|
106
|
+
// <div class="btn btn--xl card__btn card__btn--special">
|
|
107
|
+
setAttributes(el, { class: 'card__btn--special' })
|
|
108
|
+
|
|
109
|
+
// <div class="btn btn--xl card__btn card__btn--special card__btn--1 card__btn--2">
|
|
110
|
+
setAttributes(el, { class: 'card__btn--1 card__btn--2' })
|
|
111
|
+
|
|
112
|
+
// <div class="btn card__btn card__btn--special card__btn--1 card__btn--2">
|
|
113
|
+
setAttributes(el, { class: { 'btn--xl': false }})
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
When `style` is an object, it behaves like [`HTMLElement.style`](https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/style). When it’s a string, it completely replaces the attribute.
|
|
117
|
+
|
|
118
|
+
```js
|
|
119
|
+
// <div style="color: red; opacity: 0.9">
|
|
120
|
+
setAttributes(el, {
|
|
121
|
+
style: {
|
|
122
|
+
color: 'red',
|
|
123
|
+
opacity: .9
|
|
124
|
+
}
|
|
125
|
+
})
|
|
126
|
+
|
|
127
|
+
// <div style="color: red;">
|
|
128
|
+
setAttributes(el, { style: { opacity: null }})
|
|
129
|
+
|
|
130
|
+
// <div style="gap: 2px; opacity: .9;">
|
|
131
|
+
setAttributes(el, { style: 'gap: 2px; opacity: .9;')
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
`setAttributes` also works on any SVG or descendant elements.
|
|
18
135
|
|
|
19
136
|
## Math utils
|
|
20
137
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "frontacles",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0",
|
|
4
4
|
"description": "Front-end utilities for artisans",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"sideEffects": false,
|
|
@@ -12,21 +12,22 @@
|
|
|
12
12
|
},
|
|
13
13
|
"types": "./types/index.d.ts",
|
|
14
14
|
"engines": {
|
|
15
|
-
"node": ">=
|
|
15
|
+
"node": ">=22.12.0"
|
|
16
16
|
},
|
|
17
17
|
"scripts": {
|
|
18
18
|
"types": "tsc && dts-bundle-generator --silent --no-banner=true -o types/index.d.ts types-transitive/index.d.ts",
|
|
19
19
|
"posttypes": "node scripts/inject-jsdoc.mjs",
|
|
20
20
|
"test": "vitest run",
|
|
21
|
-
"test:types": "
|
|
21
|
+
"test:types": "tstyche",
|
|
22
22
|
"test:ui": "vitest --ui --coverage.enabled --coverage.exclude=types",
|
|
23
|
+
"test:watch": "vitest",
|
|
23
24
|
"coverage": "vitest run --coverage --coverage.exclude=types",
|
|
24
|
-
"watch": "
|
|
25
|
+
"watch": "npm run test:watch",
|
|
25
26
|
"build": "echo \"Nothing to build, this command is only here to please size-limit GitHub action\" && exit 0",
|
|
26
27
|
"size": "size-limit",
|
|
27
28
|
"lint": "eslint",
|
|
28
29
|
"lint:fix": "eslint --fix",
|
|
29
|
-
"lint:inspect": "eslint --inspect-config
|
|
30
|
+
"lint:inspect": "eslint --inspect-config"
|
|
30
31
|
},
|
|
31
32
|
"files": [
|
|
32
33
|
"CHANGELOG.md",
|
|
@@ -56,16 +57,18 @@
|
|
|
56
57
|
},
|
|
57
58
|
"devDependencies": {
|
|
58
59
|
"@eslint/js": "^9.9.0",
|
|
59
|
-
"@size-limit/preset-small-lib": "^
|
|
60
|
-
"@vitest/
|
|
60
|
+
"@size-limit/preset-small-lib": "^12",
|
|
61
|
+
"@vitest/browser-playwright": "^4",
|
|
62
|
+
"@vitest/coverage-v8": "^4",
|
|
61
63
|
"@vitest/eslint-plugin": "^1.0.1",
|
|
62
|
-
"@vitest/ui": "^
|
|
64
|
+
"@vitest/ui": "^4",
|
|
63
65
|
"dts-bundle-generator": "^9.5.1",
|
|
64
66
|
"eslint": "^9.9.0",
|
|
65
|
-
"
|
|
66
|
-
"
|
|
67
|
+
"globals": "^17",
|
|
68
|
+
"size-limit": "^12",
|
|
69
|
+
"tstyche": "^6.0.2",
|
|
67
70
|
"typescript": "^5.5.4",
|
|
68
|
-
"typescript-eslint": "^8
|
|
69
|
-
"vitest": "^
|
|
71
|
+
"typescript-eslint": "^8",
|
|
72
|
+
"vitest": "^4"
|
|
70
73
|
}
|
|
71
74
|
}
|
package/src/dom/index.js
ADDED
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Bulk update attributes of HTML element(s).
|
|
3
|
+
*
|
|
4
|
+
* Specific behaviour for the following types of values:
|
|
5
|
+
*
|
|
6
|
+
* - boolean, `null` and `undefined`:
|
|
7
|
+
* - `true` adds the attribute to the HTML, with no value (`<p hidden="">`)
|
|
8
|
+
* - `false`, `null` and `undefined` removes the attribute from the HTML
|
|
9
|
+
* - objects:
|
|
10
|
+
* - `class` will add/remove CSS classes from `{ className: state }` entries
|
|
11
|
+
* - `data` is pushed to `Element.dataset`, but a `data` property that is
|
|
12
|
+
* `null` or `undefined` removes its `data-*` attribute from the HTML:
|
|
13
|
+
* `{ data: { id: 1, enabled: false }}` gives `<p data-id="1">`
|
|
14
|
+
* - `style` is pushed to `Element.style` (inline CSS):
|
|
15
|
+
* `{ style: { color: 'red', gap: '2px' }}` gives
|
|
16
|
+
* `<p style="color: red; gap: 2px;">`
|
|
17
|
+
* - all other objects are converted the same way:
|
|
18
|
+
* `aria: { label: 'Hello!' }` gives `<p aria-label="Hello!">`
|
|
19
|
+
* - `class` can also be a string with one or more classes (space-separated),
|
|
20
|
+
* or an array of classes.
|
|
21
|
+
*
|
|
22
|
+
* @template {Element | Element[] | HTMLCollection} T
|
|
23
|
+
* @param {T} elements
|
|
24
|
+
* @param {Attributes} attributes
|
|
25
|
+
* @returns {T} The received element(s). Use it for method chaining.
|
|
26
|
+
*/
|
|
27
|
+
export const setAttributes = (elements, attributes) => {
|
|
28
|
+
const items = elements instanceof Element
|
|
29
|
+
? [elements]
|
|
30
|
+
: [...elements]
|
|
31
|
+
|
|
32
|
+
attributes = normalizeAttributes(attributes)
|
|
33
|
+
|
|
34
|
+
items.forEach(element =>
|
|
35
|
+
attributes.forEach(([name, value]) => {
|
|
36
|
+
if (value == null) {
|
|
37
|
+
return element.removeAttribute(name)
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// CSS `class`, as array or object.
|
|
41
|
+
if (name == 'class') {
|
|
42
|
+
return Array.isArray(value)
|
|
43
|
+
? element.classList.add(...value)
|
|
44
|
+
: Object.entries(value).forEach(([className, classState]) =>
|
|
45
|
+
element.classList.toggle(className, classState)
|
|
46
|
+
)
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Only `data` and `style` can go in this `if`.
|
|
50
|
+
if (typeof value == 'object') {
|
|
51
|
+
|
|
52
|
+
// `style` attribute (inline styles)
|
|
53
|
+
if (name == 'style') {
|
|
54
|
+
return Object.assign(element.style, value)
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// `data-*` attributes (using `dataset`)
|
|
58
|
+
Object.assign(element.dataset, value)
|
|
59
|
+
|
|
60
|
+
return Object.entries(value).forEach(([dataKey, dataValue]) => {
|
|
61
|
+
/**
|
|
62
|
+
* Remove `data-*` prop if their value is `null` or `undefined`,
|
|
63
|
+
* because `dataset` stringify them but `setAttributes` apply
|
|
64
|
+
* the logic of regular HTML attributes to all attributes.
|
|
65
|
+
*/
|
|
66
|
+
if (dataValue == null) {
|
|
67
|
+
delete element.dataset[dataKey]
|
|
68
|
+
}
|
|
69
|
+
})
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
element.setAttribute(name, value)
|
|
73
|
+
})
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
return elements
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/** @param {Record<string, any>} fnAttributes */
|
|
80
|
+
const normalizeAttributes = fnAttributes => {
|
|
81
|
+
const attributes = { ...fnAttributes }
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Normalize object attributes.
|
|
85
|
+
* It turns `{ key: { name: value }}` into `'key-name="value"'`.
|
|
86
|
+
*/
|
|
87
|
+
Object.entries(attributes)
|
|
88
|
+
.filter(([name, value]) =>
|
|
89
|
+
value
|
|
90
|
+
&& typeof value == 'object'
|
|
91
|
+
&& !['class', 'data', 'style'].includes(name)
|
|
92
|
+
)
|
|
93
|
+
.forEach(([name, value]) => {
|
|
94
|
+
delete attributes[name]
|
|
95
|
+
|
|
96
|
+
Object.entries(value).forEach(([attrName, attrValue]) => {
|
|
97
|
+
attributes[`${name}-${attrName}`] = attrValue
|
|
98
|
+
})
|
|
99
|
+
})
|
|
100
|
+
|
|
101
|
+
// Normalize `class` attribute from string to array.
|
|
102
|
+
if (typeof attributes.class == 'string') {
|
|
103
|
+
attributes.class = attributes.class.split(' ')
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// Normalize boolean attributes: `true` becomes `''`, `false` becomes `null`.
|
|
107
|
+
return Object.entries(attributes)
|
|
108
|
+
.map(([name, value]) => [
|
|
109
|
+
name,
|
|
110
|
+
typeof value == 'boolean'
|
|
111
|
+
? (value ? '' : null)
|
|
112
|
+
: value // not boolean
|
|
113
|
+
])
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/** @typedef {boolean|string|number|null|undefined} AttributePrimitive */
|
|
117
|
+
/** @typedef {AttributePrimitive | Record<string, AttributePrimitive>} AttributeValue */
|
|
118
|
+
/** @typedef {Record<string, AttributeValue>} BaseAttributes */
|
|
119
|
+
/** @typedef {{ class?: string | string[] | Record<string, boolean> }} ClassAttribute */
|
|
120
|
+
/** @typedef {{ style?: AttributePrimitive | CSSStyleDeclaration }} StyleAttribute */
|
|
121
|
+
/** @typedef {BaseAttributes | ClassAttribute | StyleAttribute} Attributes */
|
package/src/index.js
CHANGED
package/types/index.d.ts
CHANGED
|
@@ -1,3 +1,14 @@
|
|
|
1
|
+
export function setAttributes<T extends Element | Element[] | HTMLCollection>(elements: T, attributes: Attributes): T;
|
|
2
|
+
export type AttributePrimitive = boolean | string | number | null | undefined;
|
|
3
|
+
export type AttributeValue = AttributePrimitive | Record<string, AttributePrimitive>;
|
|
4
|
+
export type BaseAttributes = Record<string, AttributeValue>;
|
|
5
|
+
export type ClassAttribute = {
|
|
6
|
+
class?: string | string[] | Record<string, boolean>;
|
|
7
|
+
};
|
|
8
|
+
export type StyleAttribute = {
|
|
9
|
+
style?: AttributePrimitive | CSSStyleDeclaration;
|
|
10
|
+
};
|
|
11
|
+
export type Attributes = BaseAttributes | ClassAttribute | StyleAttribute;
|
|
1
12
|
export function clamp(val: number, min: number, max: number): number;
|
|
2
13
|
export function round(number: number, precision?: number): number;
|
|
3
14
|
export function capitalize(str: string): string;
|