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
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 tonamespace(string?): An optional namespace to avoid conflicts between different group decorators, defaults to an empty stringoptions(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. Returningundefinedfalls back to the global (unscoped) registry.getGroup((instance, scope) => string): Resolves the group name of an instance, optionally using its resolved scope. Defaults toinstance.$options.group.
Return value
Base: A child class of the given class with thegroupoption
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.
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;
}
}
}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);<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.
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);
}
}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);
}
}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.
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);
}
}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);
}
}
}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.