Skip to content

withGroup

Groups components together. Use this when a component needs to be aware of its siblings, for example to create a tabs system.

This decorator adds a data-option-group="<GROUPNAME>" option which can be used to group components together from the DOM. An instance of a component will be present in a group, exposed with the $group getter, only when it is mounted.

Usage

js
import { 
Base
,
withGroup
} from '@studiometa/js-toolkit';
class
Foo
extends
withGroup
(
Base
) {
static
config
= {
name
: 'Foo',
}; } class
Bar
extends
withGroup
(
Base
) {
static
config
= {
name
: 'Bar',
};
mounted
() {
console
.
log
(this.
$group
); // Set [Foo, Bar]
} }

Parameters

  • BaseClass (Base): The class to add grouping capabilities to
  • namespace (string?): An optional namespace to avoid conflicts between different group decorators, defaults to an empty string
  • options (WithGroupOptions?): An optional configuration object to scope groups to a parent and/or resolve the group name dynamically, defaults to an empty object. It accepts two optional callbacks:
    • getScope ((instance) => object | undefined): Resolves the scope an instance belongs to. Members are then grouped per scope identity instead of globally, so two scopes can reuse the same group name without colliding. Returning undefined falls back to the global (unscoped) registry.
    • getGroup ((instance, scope) => string): Resolves the group name of an instance, optionally using its resolved scope. Defaults to instance.$options.group.

Return value

  • Base: A child class of the given class with the group option

Examples

Keeping <input> in sync

The group decorator can be used to keep instances of a same component in sync by dispatching updates on all instances belonging to the same group.

In the following example, both input[data-option-group="input"] element seen in the index.html file will have their value kept in sync when it changes. The third input input[data-option-group="other-group"] will be left untouched.

js
import { 
Base
,
withGroup
} from '@studiometa/js-toolkit';
export class
SyncedInput
extends
withGroup
(
Base
) {
static
config
= {
name
: 'SyncedInput',
}; get
value
() {
return this.
$el
.value;
} set
value
(
value
) {
this.
$el
.value =
value
;
}
onInput
() {
const {
value
,
$group
} = this;
for (const
instance
of
$group
) {
if (
instance
=== this) continue; // Skip updating itself
instance
.value =
value
;
} } }
js
import { 
Base
,
createApp
} from '@studiometa/js-toolkit';
import {
SyncedInput
} from './SyncedInput.js';
class
App
extends
Base
{
static
config
= {
name
: 'App',
components
: {
SyncedInput
,
}, }; } export default
createApp
(
App
);
html
<input data-component="SyncedInput" data-option-group="input" type="text" />
<input data-component="SyncedInput" data-option-group="input" type="text" />

<input
  data-component="SyncedInput"
  data-option-group="other-group"
  type="text" />

💡 Two-way binding with DataModel

You should use the DataModel component from the @studometa/ui package along its accompanying DataBind, DataComputed and DataEffect components if you need to add some reactivity to your existing DOM.

Using a namespace to avoid group collision

When using multiple groups in the same DOM tree, it can be useful to namespace them to avoid collisions.

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.

js
import { 
Base
,
withGroup
} from '@studiometa/js-toolkit';
export class
Tabs
extends
withGroup
(
Base
, 'tabs') {
static
config
= {
name
: 'Tabs',
};
mounted
() {
console
.
log
('Tabs group:', this.
$group
);
} }
js
import { 
Base
,
withGroup
} from '@studiometa/js-toolkit';
export class
Accordion
extends
withGroup
(
Base
, 'accordion') {
static
config
= {
name
: 'Accordion',
};
mounted
() {
console
.
log
('Accordion group:', this.
$group
);
} }
js
import { 
registerComponents
} from '@studiometa/js-toolkit';
import {
Tabs
} from './Tabs.js';
import {
Accordion
} from './Accordion.js';
registerComponents
(
Tabs
,
Accordion
);

Scoping groups to a parent component

By default, groups are global: every mounted instance sharing the same data-option-group value belongs to the same group, wherever it lives in the DOM. When you have several independent regions on a page that each reuse the same group names, use the getScope option to scope membership to a parent component. Two regions can then share the same group name without their members leaking into each other.

In the following example, each Field is scoped to its closest FieldSet ancestor, so fields only sync with the other fields of the same FieldSet. The FieldSet can enumerate its own scoped groups with the getScopedGroups helper.

js
import { 
Base
,
withGroup
} from '@studiometa/js-toolkit';
export class
Field
extends
withGroup
(
Base
, 'field:', {
// Scope each field to its closest `FieldSet` ancestor.
getScope
: (
instance
) =>
instance
.
$closest
('FieldSet'),
}) { static
config
= {
name
: 'Field',
};
mounted
() {
// Only fields within the same `FieldSet` are grouped together, even if // they share the same `data-option-group` value.
console
.
log
(this.
$group
);
} }
js
import { 
Base
,
getScopedGroups
} from '@studiometa/js-toolkit';
import {
Field
} from './Field.js';
export class
FieldSet
extends
Base
{
static
config
= {
name
: 'FieldSet',
components
: {
Field
,
}, };
mounted
() {
// Enumerate every group scoped to this `FieldSet`, keyed by // `${namespace}${group}`. for (const [
key
,
members
] of
getScopedGroups
(this)) {
console
.
log
(
key
,
members
);
} } }
js
import { 
registerComponent
} from '@studiometa/js-toolkit';
import {
FieldSet
} from './FieldSet.js';
registerComponent
(
FieldSet
);

TIP

When getScope returns undefined — for example when a Field is mounted outside of any FieldSet — the instance falls back to the global (unscoped) registry, using the same ${namespace}${group} key.

Membership is resolved once, at mount time

Group membership is resolved a single time when the component is mounted. Changing the group option — or the scope returned by getScope — while the instance is mounted does not move it to another group: $group keeps returning the set it joined at mount. The new group is only taken into account after the instance is destroyed and mounted again.

MIT Licensed