Skip to content

Components

A component is a class that extends Base, a name in the registry, and a data-component attribute in the markup. All three are needed, and nothing else is.

Declaring a component

js
import { 
Base
,
registerComponent
} from '@studiometa/js-toolkit';
class
Counter
extends
Base
{
static
config
= {
name
: 'Counter',
refs
: ['output'],
options
: {
step
: {
type
:
Number
,
default
: 1 } },
};
count
= 0;
onClick
() {
this.
count
+= this.
$options
.
step
;
this.
$refs
.
output
.textContent =
String
(this.
count
);
} }
registerComponent
(
Counter
);
html
<button data-component="Counter" data-option-step="5">
  clicked
  <span data-ref="output">0</span>
  times
</button>

config.name is the name the registry uses and the token the markup writes. It is also what gives each instance its $id.

Registering

registerComponent(ComponentClass) puts one name in the registry and scans the document for it. registerComponents(...classes) does the same for several:

js
import { 
Base
,
registerComponents
} from '@studiometa/js-toolkit';
class
Accordion
extends
Base
{
static
config
= {
name
: 'Accordion' };
} class
Slider
extends
Base
{
static
config
= {
name
: 'Slider' };
}
registerComponents
(
Accordion
,
Slider
);

There is no createApp()

v4 has no root component and no application object. A component is registered, not mounted by a parent, so nothing has to own the page. Register the components a page uses and the registry does the rest.

One name gives one entry, as with customElements.define(). A second registration under a name already taken gives a registry.conflict warning and is ignored.

Several components on one element

data-component holds a whitespace-separated token list, and each token gets its own instance:

html
<a href="/next" data-component="Action Analytics Prefetch">Next</a>

The three instances are independent. They share the element and nothing else.

Nesting

Children are not constructed by their parent. A nested data-component is discovered by the same registry, on its own:

html
<div data-component="Accordion">
  <div data-component="AccordionItem">…</div>
  <div data-component="AccordionItem">…</div>
</div>

What the parent declares in config.components is a family, and it does two jobs:

  1. registering that family when the parent registers, so one registerComponent() call covers a whole tree;
  2. giving the name set that on<Child><Event> resolution needs.
js
import { 
Base
,
registerComponent
} from '@studiometa/js-toolkit';
class
AccordionItem
extends
Base
{
static
config
= {
name
: 'AccordionItem' };
} class
Accordion
extends
Base
{
static
config
= {
name
: 'Accordion',
components
: {
AccordionItem
},
}; } // Registers `Accordion` *and* `AccordionItem`.
registerComponent
(
Accordion
);

A value can also be a thunk, which makes the child its own chunk:

js
static config = {
  name: 'Accordion',
  components: {
    AccordionItem: () => import('./AccordionItem.js'),
  },
};

The key supplies the name, so the registry knows AccordionItem with nothing downloaded. See Autoloading.

config.components is not ownership

Declaring a child does not make the parent construct it, does not make the parent's unmount unmount it, and does not restrict where the child may appear. Nesting is DOM ancestry, always.

Finding other components

There is no $parent and no $children. A component that needs to reach another one has four channels, in order of preference:

DirectionUse
parent → children$watchChildren() or $query(name)
child → parent$emit() and the parent's on<Child><Event>
ancestor lookup, ad hoc$closest(name) — guard the result
shared state either wayprovide/inject
js
import { 
Base
} from '@studiometa/js-toolkit';
class
Accordion
extends
Base
{
static
config
= {
name
: 'Accordion',
components
: {} };
// A live collection, in document order, kept for the life of this instance.
items
= this.
$watchChildren
('AccordionItem');
closeAll
() {
for (const
item
of this.
items
)
item
.
$el
.
removeAttribute
('data-option-open');
} }

Page-wide lookups

Outside a component, four functions answer from the DOM. They keep no registry of instances:

ts
import {
  
getInstance
,
getInstances
,
getMountedInstances
,
getUnmountedInstances
,
} from '@studiometa/js-toolkit'; const
section
=
document
.
querySelector
('section')!;
getMountedInstances
('Dialog'); // the live ones — safe to call a method on
getInstances
('Dialog',
section
); // every one built in a region
getUnmountedInstances
('Dialog'); // built, then stood down
getInstance
(
section
, 'Dialog'); // the one on this element, mounted or not
getInstances
(
section
); // everything on one element

See the Registry reference for the full contract of each.

Extending a component

There is no withExtraConfig(). To extend a component with a different config, declare a class. $config walks the prototype chain, so refs, options and components merge:

js
import { 
Base
,
registerComponent
} from '@studiometa/js-toolkit';
class
AbstractControl
extends
Base
{
static
config
= {
name
: 'AbstractControl',
refs
: ['button'],
}; } class
NavigationControl
extends
AbstractControl
{
static
config
= {
name
: 'NavigationControl',
// `refs` merges: ['button', 'compass']
refs
: ['compass'],
options
: {
showCompass
:
Boolean
},
}; }
registerComponent
(
NavigationControl
);

To extend a class you cannot edit, do it in expression position:

js
registerComponent(
  class extends Vendor {
    static config = { name: 'CompactVendor', options: { compact: Boolean } };
  },
);

When a component mounts

By default an instance mounts as soon as its element enters the document. config.mountStrategy and the data-mount attribute change that:

html
<div data-component="Map" data-mount="visible"></div>
<div data-component="Chat" data-mount="interaction:page"></div>

A component that waits has no instance: it is invisible to $query(), $closest(), $watchChildren() and getInstances(), and it announces nothing. See Mount strategies.

Responsive declarations

data-component takes one breakpoint-scoped companion, so a component can exist at some widths and not others:

html
<div
  data-component="Action Analytics"
  data-component:xxs="MobileMenu"
  data-component:m="DesktopMenu"></div>

The unconditional set is always active. The scoped set is resolved by walking from the widest active suffix down to the first attribute present, and it replaces rather than merges. A name that stops being declared is unmounted and dropped from the element; a crossing back builds a new instance. See data-component.

MIT Licensed