Skip to content

Options

An option is a value a component reads from its element. It is declared in config.options and written in the markup as data-option-<name>.

Two rules carry everything else on this page:

An option is an input, never a store.

Every option is responsive. There is nothing to declare for it.

Declaring options

js
import { 
Base
} from '@studiometa/js-toolkit';
class
Slider
extends
Base
{
static
config
= {
name
: 'Slider',
options
: {
// The short form: just the type.
label
:
String
,
// The long form: a type and a default.
speed
: {
type
:
Number
,
default
: 1 },
loop
: {
type
:
Boolean
,
default
: true },
// Array and Object defaults must be factory functions.
tween
: {
type
:
Object
,
default
: () => ({
ease
: 'linear' }) },
}, };
mounted
() {
console
.
log
(this.
$options
.
speed
);
} }
html
<div data-component="Slider" data-option-speed="2" data-option-label="Gallery"></div>

The attribute name is the option name in kebab case: dragThreshold is data-option-drag-threshold.

$options is a read-only view

buildOptions() defines every property with a getter and no setter. The value is derived from the element and the viewport on each access, and nothing is written in. An assignment throws, because a module is strict code:

js
this.$options.open = true;
// TypeError: Cannot set property open of #<Object> which has only a getter

Two idioms replace the write, and which one you want depends on what the value is.

The DOM is meant to change — write the attribute. It is the same statement the markup makes:

js
import { 
Base
} from '@studiometa/js-toolkit';
class
Panel
extends
Base
{
static
config
= {
name
: 'Panel',
options
: {
disabled
:
Boolean
} };
enable
() {
this.
$el
.
removeAttribute
('data-option-disabled');
}
disable
() {
this.
$el
.
setAttribute
('data-option-disabled', '');
} }

The next read gives the new value, and option<Name>Changed() announces it to anyone listening.

The value was never an input — keep a private field seeded from the option. The attribute says where the component starts; the field carries where it has got to:

js
import { Base } from '@studiometa/js-toolkit';

class Carousel extends Base {
  static config = { name: 'Carousel', options: { start: { type: Number, default: 0 } } };

  #index = this.$options.start;

  next() {
    this.#index += 1;
  }
}

An option a component only reads keeps neither: it reads this.$options.x where it needs it.

Defaults

A primitive can be a default. Every other data type needs a factory function.

js
options: {
  speed: { type: Number, default: 1 },                       // fine
  tween: { type: Object, default: () => ({ ease: 'linear' }) }, // required form
  items: { type: Array, default: () => [] },                    // required form
}
  • Function is not an option type, so a default that is a function is always a factory.
  • A default is built once per instance and then kept. Two instances never share one default.
  • A factory is lazy. Nothing is built for an option nobody reads, and a component whose attribute is present never runs its factory.
  • Array and Object with no declared default get an empty value per instance.
  • A literal object or array default gives one option.literal-default warning, naming the component, the option and the correction. The value is then used as declared and shared between instances.

A default is the one option value that lives on the instance, so mutating the object a factory built sticks — this.$options.tween.ease = 'ease-out' is kept, for that instance. Replacing the option itself does not: this.$options.tween = {} throws, like every other write to the view.

Boolean options read presence

data-option-open is on, an absent attribute is the declared default, and the string the attribute carries is never read — the way disabled and checked work on the platform:

html
<div data-component="Panel" data-option-open></div>
<!-- true -->
<div data-component="Panel" data-option-open="false"></div>
<!-- true — the value says nothing -->
<div data-component="Panel"></div>
<!-- the declared default -->

Update one from code by adding or removing the attribute:

js
flag ? el.setAttribute('data-option-open', '') : el.removeAttribute('data-option-open');

A template must therefore write the attribute conditionally rather than interpolate a boolean into it:

html
{# Correct #}
<div data-component="Panel" {% if isOpen %}data-option-open{% endif %}></div>

{# Wrong — always true #}
<div data-component="Panel" data-option-open="{{ isOpen }}"></div>

Turning one off — data-option-no-<name>

A boolean option is turned off by an attribute that only has to be there:

html
<div data-component="Dialog" data-option-no-trap-focus></div>
  • It is how an option declared default: true is turned off, since removing an attribute that is not there says nothing.
  • Only an option that can hold false has one — a declared Boolean, or a union containing it. A String option has nothing to turn off, so data-option-no-label is not an attribute and the observer never watches for it.
  • Presence is the whole statement. data-option-no-x="false" is not a double negative; it is the same flag.
  • It is responsive like every other spelling.
  • An option whose own name starts with no negates independently: noSort owns data-option-no-sort, and its off spelling doubles the prefix, data-option-no-no-sort.

Several types

An option can accept several types. Declare the constructors in order:

js
import { 
Base
} from '@studiometa/js-toolkit';
class
Sticky
extends
Base
{
static
config
= {
name
: 'Sticky',
options
: {
// "10" reads as a number; "[10, 20]" reads as an array.
offset
: [
Number
,
Array
],
}, }; }

Each parser must give a value of its declared type before the next parser runs. An absent union option uses its declared default, or the empty value of its first type.

Responsive options

Every option takes a breakpoint suffix, with no flag to declare:

html
<div
  data-component="Grid"
  data-option-columns="1"
  data-option-columns:s="2"
  data-option-columns:l="4"></div>
  • The suffix names one breakpoint and it cascades upwards. $options.columns walks from the active breakpoint down to the base value and gives the first attribute present. v3 spelled a set (:xs:s); v4 does not.
  • The separator is a colon, because an option name in kebab case can contain a dash.
  • The value is derived on read. Nothing is stored and nothing is written.
  • A suffix naming no configured breakpoint gives one responsive.unknown-breakpoint warning per mount.
  • A matchMedia subscription opens only for a component that declares option<Name>Changed(). A page that only reads options holds no listener.

See setBreakpoints() to replace the named set.

Live effects

A declared method named option<Name>Changed() makes that option a live effect. It is how an option that chooses a resource stays correct:

js
import { 
Base
} from '@studiometa/js-toolkit';
function
connect
(
url
) {
return {
dispose
() {} };
} class
Feed
extends
Base
{
static
config
= {
name
: 'Feed',
options
: {
source
:
String
} };
optionSourceChanged
({
value
,
previousValue
,
initial
}) {
const
connection
=
connect
(
value
);
// The returned function is the cleanup for *this* value. return () =>
connection
.
dispose
();
} }
  • The hook runs before mounted() on each mount cycle, with initial: true.
  • Several writes in one mutation batch give one change, from the first old raw value to the final DOM value. A write that ends where it started is not a change.
  • The previous cleanup runs before an update. Every active cleanup runs on $unmount().
  • Removal of the attribute applies the declared default.
  • A breakpoint crossing reports through the same hook. A crossing to the same resolved value announces nothing.
  • A component without the convention pays no setup cost.

The payload is an OptionChange:

ts
import { 
Base
, type
OptionChange
} from '@studiometa/js-toolkit';
class
Feed
extends
Base
{
static
config
= {
name
: 'Feed',
options
: {
source
:
String
} };
optionSourceChanged
({
value
,
previousValue
,
rawValue
,
initial
}:
OptionChange
<string>) {
console
.
log
(
value
,
previousValue
,
rawValue
,
initial
);
} }

MIT Licensed