Skip to content

Commit 317941f

Browse files
committed
Merge branch 'release/3.3.0'
2 parents 17e05c6 + 76b56b3 commit 317941f

File tree

10 files changed

+304
-157
lines changed

10 files changed

+304
-157
lines changed

CHANGELOG.md

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,20 @@ All notable changes to this project will be documented in this file. The format
44

55
## [Unreleased]
66

7+
## [v3.3.0](https://github.com/studiometa/js-toolkit/compare/3.2.0..3.3.0) (2025-10-29)
8+
9+
### Added
10+
11+
- **withGroup:** add support for namespaces ([#681](https://github.com/studiometa/js-toolkit/pull/681), [3e50e1f9](https://github.com/studiometa/js-toolkit/commit/3e50e1f9))
12+
13+
### Changed
14+
15+
- **withGroup:** improve types ([#681](https://github.com/studiometa/js-toolkit/pull/681), [fa723cfd](https://github.com/studiometa/js-toolkit/commit/fa723cfd))
16+
17+
### Fixed
18+
19+
- **withGroup:** do not include instances disconnected from the DOM in groups ([#680](https://github.com/studiometa/js-toolkit/issues/680), [#681](https://github.com/studiometa/js-toolkit/pull/681), [e214ba38](https://github.com/studiometa/js-toolkit/commit/e214ba38))
20+
721
## [v3.2.0](https://github.com/studiometa/js-toolkit/compare/3.1.1..3.2.0) (2025-10-21)
822

923
### Added

package-lock.json

Lines changed: 149 additions & 140 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@studiometa/js-toolkit-workspace",
3-
"version": "3.2.0",
3+
"version": "3.3.0",
44
"private": true,
55
"type": "module",
66
"workspaces": [
@@ -29,12 +29,12 @@
2929
"build:types": "tsc --build tsconfig.build.json",
3030
"build:pkg": "node scripts/build.js",
3131
"build-for-export-size": "node scripts/add-utils-export.js && rm -rf dist && npm run build:pkg && npm run build:cp-files",
32-
"postversion": "npm version -ws $npm_package_version"
32+
"postversion": "npm version --workspaces $npm_package_version"
3333
},
3434
"devDependencies": {
3535
"@studiometa/prettier-config": "4.4.0",
36-
"@types/node": "22.18.11",
37-
"oxlint": "1.23.0",
36+
"@types/node": "22.18.12",
37+
"oxlint": "1.24.0",
3838
"prettier": "3.6.2",
3939
"typescript": "5.9.3"
4040
},

packages/demo/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@studiometa/js-toolkit-demo",
3-
"version": "3.2.0",
3+
"version": "3.3.0",
44
"private": true,
55
"type": "module",
66
"scripts": {

packages/docs/api/decorators/withGroup.md

Lines changed: 58 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ class Bar extends withGroup(Base) {
3333
### Parameters
3434

3535
- `BaseClass` (`Base`): The class to add grouping capabilities to
36+
- `namespace` (`string?`): An optional namespace to avoid conflicts between different group decorators, defaults to an empty string
3637

3738
### Return value
3839

@@ -94,7 +95,63 @@ export default createApp(App);
9495
<input data-component="SyncedInput" data-option-group="input" type="text" />
9596
<input data-component="SyncedInput" data-option-group="input" type="text" />
9697

97-
<input data-component="SyncedInput" data-option-group="other-group" type="text" />
98+
<input
99+
data-component="SyncedInput"
100+
data-option-group="other-group"
101+
type="text" />
102+
```
103+
104+
:::
105+
106+
::: tip 💡 Two-way binding with [`DataModel`](https://ui.studiometa.dev/-/components/DataModel/)
107+
108+
You should use the [`DataModel` component](https://ui.studiometa.dev/-/components/DataModel/) from the [@studometa/ui package](https://ui.studiometa.dev) along its accompanying [`DataBind`](https://ui.studiometa.dev/-/components/DataBind/), [`DataComputed`](https://ui.studiometa.dev/-/components/DataComputed/) and [`DataEffect`](https://ui.studiometa.dev/-/components/DataEffect/) components if you need to add some reactivity to your existing DOM.
109+
110+
:::
111+
112+
### Using a namespace to avoid group collision
113+
114+
When using multiple groups in the same DOM tree, it can be useful to namespace them to avoid collisions.
115+
116+
In the following example, both `Tabs` and `Accordion` components use a group decorator, but they are namespaced to avoid interference if they share the same `data-option-group` attribute value.
117+
118+
::: code-group
119+
120+
```js {3,9,15-18} twoslash [Tabs.js]
121+
import { Base, withGroup } from '@studiometa/js-toolkit';
122+
123+
export class Tabs extends withGroup(Base, 'tabs') {
124+
static config = {
125+
name: 'Tabs',
126+
};
127+
128+
mounted() {
129+
console.log('Tabs group:', this.$group);
130+
}
131+
}
132+
```
133+
134+
```js {3,9,15-18} twoslash [Accordion.js]
135+
import { Base, withGroup } from '@studiometa/js-toolkit';
136+
137+
export class Accordion extends withGroup(Base, 'accordion') {
138+
static config = {
139+
name: 'Accordion',
140+
};
141+
142+
mounted() {
143+
console.log('Accordion group:', this.$group);
144+
}
145+
}
146+
```
147+
148+
```js twoslash [app.js]
149+
import { registerComponent } from '@studiometa/js-toolkit';
150+
import { Tabs } from './Tabs.js';
151+
import { Accordion } from './Accordion.js';
152+
153+
registerComponent(Tabs);
154+
registerComponent(Accordion);
98155
```
99156

100157
:::

packages/docs/package.json

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@studiometa/js-toolkit-docs",
3-
"version": "3.2.0",
3+
"version": "3.3.0",
44
"type": "module",
55
"private": true,
66
"scripts": {
@@ -9,10 +9,10 @@
99
"preview": "vitepress preview ."
1010
},
1111
"devDependencies": {
12-
"@shikijs/vitepress-twoslash": "3.13.0",
12+
"@shikijs/vitepress-twoslash": "3.14.0",
1313
"@studiometa/tailwind-config": "2.1.0",
1414
"tailwindcss": "3.4.18",
1515
"vitepress": "2.0.0-alpha.12",
16-
"vitepress-plugin-llms": "1.8.0"
16+
"vitepress-plugin-llms": "1.8.1"
1717
}
1818
}

packages/js-toolkit/decorators/withGroup.ts

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,8 @@ export interface WithGroupInterface extends BaseInterface {
1414
/**
1515
* Get global groups map.
1616
*/
17-
function groups(): Map<string, Set<Base>> {
18-
return (globalThis.__JS_TOOLKIT_GROUPS__ ??= new Map<string, Set<Base>>());
17+
function groups<T extends Base = Base>(): Map<string, Set<T>> {
18+
return (globalThis.__JS_TOOLKIT_GROUPS__ ??= new Map<string, Set<T>>());
1919
}
2020

2121
/**
@@ -25,6 +25,7 @@ function groups(): Map<string, Set<Base>> {
2525
*/
2626
export function withGroup<S extends Base = Base>(
2727
BaseClass: typeof Base,
28+
namespace = '',
2829
): BaseDecorator<WithGroupInterface, S, WithGroupProps> {
2930
// @ts-expect-error Decorators can not be typed.
3031
return class WithGroup<T extends BaseProps = BaseProps> extends BaseClass<T & WithGroupProps> {
@@ -39,8 +40,16 @@ export function withGroup<S extends Base = Base>(
3940
* Get the group set.
4041
*/
4142
get $group() {
42-
const { group } = this.$options;
43-
return groups().get(group) ?? groups().set(group, new Set()).get(group);
43+
const group = `${namespace}${this.$options.group}`;
44+
const instances = groups<this>().get(group) ?? groups<this>().set(group, new Set()).get(group);
45+
46+
for (const instance of instances) {
47+
if (!instance.$el.isConnected) {
48+
instances.delete(instance);
49+
}
50+
}
51+
52+
return instances;
4453
}
4554

4655
constructor(element: HTMLElement) {

packages/js-toolkit/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@studiometa/js-toolkit",
3-
"version": "3.2.0",
3+
"version": "3.3.0",
44
"description": "A set of useful little bits of JavaScript to boost your project! 🚀",
55
"publishConfig": {
66
"access": "public"

packages/tests/decorators/withGroup.spec.ts

Lines changed: 60 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,12 +16,31 @@ describe('The `withGroup` decorator', () => {
1616
};
1717
}
1818

19-
const foo = new Foo(h('div',));
20-
const bar = new Bar(h('div',));
19+
const foo = new Foo(h('div', { data: { optionGroup: 'group' } }));
20+
const bar = new Bar(h('div', { data: { optionGroup: 'group' } }));
2121
await mount(foo, bar);
2222
expect(foo.$group).toBe(bar.$group);
2323
});
2424

25+
it('should group mounted components with namespace', async () => {
26+
class Foo extends withGroup(Base, 'ns:') {
27+
static config: BaseConfig = {
28+
name: 'Foo',
29+
};
30+
}
31+
32+
class Bar extends withGroup(Base) {
33+
static config: BaseConfig = {
34+
name: 'Bar',
35+
};
36+
}
37+
38+
const foo = new Foo(h('div', { data: { optionGroup: 'group' } }));
39+
const bar = new Bar(h('div', { data: { optionGroup: 'group' } }));
40+
await mount(foo, bar);
41+
expect(foo.$group).not.toContain(bar);
42+
});
43+
2544
it('should not include destroyed components', async () => {
2645
class Foo extends withGroup(Base) {
2746
static config: BaseConfig = {
@@ -42,4 +61,43 @@ describe('The `withGroup` decorator', () => {
4261
await destroy(foo);
4362
expect(foo.$group.has(foo)).toBe(false);
4463
});
64+
65+
it('should forget grouped instances not in the DOM anymore', async () => {
66+
class Foo extends withGroup(Base) {
67+
static config: BaseConfig = {
68+
name: 'Foo',
69+
};
70+
}
71+
72+
const fragment = new Document();
73+
const divA = h('div', {
74+
data: { optionGroup: 'group' },
75+
});
76+
const divB = h('div', {
77+
data: { optionGroup: 'group' },
78+
});
79+
80+
fragment.append(divA, divB);
81+
82+
const instanceA = new Foo(divA);
83+
const instanceB = new Foo(divB);
84+
85+
await mount(instanceA, instanceB);
86+
87+
expect(divA.isConnected).toBe(true);
88+
expect(divB.isConnected).toBe(true);
89+
expect(instanceA.$group).toContain(instanceA);
90+
expect(instanceA.$group).toContain(instanceB);
91+
92+
divB.replaceWith(
93+
h('div', {
94+
data: { optionGroup: 'group' },
95+
}),
96+
);
97+
98+
expect(divA.isConnected).toBe(true);
99+
expect(divB.isConnected).toBe(false);
100+
expect(instanceA.$group).toContain(instanceA);
101+
expect(instanceA.$group).not.toContain(instanceB);
102+
});
45103
});

packages/tests/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@studiometa/js-toolkit-tests",
3-
"version": "3.2.0",
3+
"version": "3.3.0",
44
"private": true,
55
"type": "module",
66
"scripts": {

0 commit comments

Comments
 (0)