Skip to content

Attribute Parser

Esther Brunner edited this page Oct 9, 2024 · 9 revisions

Motivation

All Web Components that should be able to receive initial values for client-side state from the server or updated values from outside UIElement applications need to observe attributes. This is therefore a very common scenario. It serves the goal of consistency across components, if this is handled by the library. Reuse of components and parser functions is easier, if we can rely on the same behavior in all UIElement components. We also save on component size, if developers don't have to implement the same or very similar behavior in every component.

Current Implementation

Web Components can have observed attributes, that trigger the attributeChangedCallback:

static observedAttributes = ['value']

UIElement parses these attributes according to a declarative static property called attributeMap:

static attributeMap = {
    value: asInteger
}

The UIElement base class will assign them to signals in the component with the same name as key:

this.get('value') // will return the current value of the `value` signal as an integer `number` or `undefined` if not a number

Comparison to Native Alternative

To achieve the same, developers would have to write the following code:

static observedAttributes = ['value']

attributeChangedCallback(name, oldValue, newValue) {
    if (oldValue === newValue) return

    // Hardcoded asInteger parser for 'value' observed attribute
    const asInteger = value => value.map(v => parseInt(v, 10)).filter(Number.isFinite)
    this.set(name, asInteger([newValue])[0])
}

This is tedious. It gets trickier and error prone if switching cases for different attributes is required.

Conclusion: The implementation in UIElement let's us do the same in a declarative manner, with pattern matching to predefined or custom parser functions.

API Design Considerations

  • Developers should not have to implement attributeChangedCallback in their components
  • The API should feel natural to use in the context of other conventions for Web Components
  • Attribute parser functions should be composable pure functions
  • Developers should not have to worry whether the value is defined or not in attribute parser functions
  • Developers should be able to pass the attribute parser function as value in attributeMap
  • For simple attribute parser functions, that just transform the value to a certain type, no argument should be required
  • Complex attribute parser functions, that use the component context or the old value, should be possible

Name of Property

Decision: attributeMap - since v0.6.0

Reason: shorter than attributeMapping

Possible alternatives:

  • attributeMapping - before v0.6.0
  • parseAttributes - sounds more active
  • signalTypes - focus on outcome rather than input
  • types – short

Static or Not?

Decision: static - since v0.7.3

Reason: align it with static observedAttributes; in most cases there is no need for per-instance differentiation

Possible alternative:

  • instance property - before v0.7.3; no need to pass this context to parser function

Object or Map?

Decision: object - since v0.1.0

Reason: easier to write

Possible alternative:

  • Map - easier for iteration

Key Format

Decision: string, same as attribute name - since v0.1.0

No alternatives.

Value Format

Decision: pure function - since v0.7.3

Reasons:

  • aligns with strive for more functional programming approach
  • array notation was confusing
  • fixed string function keys were limiting

Possible alternative:

  • string function key or array of signal key + string function key - before v0.7.3

Parser Function Input Arguments

Decision:

  • Maybe<string>, array of 0 or 1 current values - since 0.8.0
  • UIElement, this context of component – since 0.7.3
  • string|undefined, previous value

Reason: array for current value allows to .map() over and .filter() out invalid values, without checking whether the value is undefined

Possible alternative:

  • string|undefined for first argument - before v0.8.0

Parser Function Return Value

Decision: Maybe<unknown>, array of 0 or 1 current values - since 0.8.0

Reason: consistent with input of first argument

Possible alternative:

  • unknown|undefined - before v0.8.0