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