0.0.26 • Published 1 month ago

be-bound v0.0.26

Weekly downloads
-
License
MIT
Repository
github
Last release
1 month ago

be-bound

be-bound is an attribute-based custom enhancement that provides limited "two-way binding" support.

It follows almost identical patterns to other be-enhanced based binding enhancements, especially be-observant.

NPM version How big is this package in your project? Playwright Tests

Limitations:

  1. Binding is 100% equal -- no computed binding, just direct copy of primitives.
  2. Object support is there also, with special logic to avoid infinite loops. A guid key is assigned to the object to avoid this calamity. TODO, only if strong use case is found.
  3. If the two values are equal, no action is taken.
  4. The two properties must be class properties with setters and getters, either defined explicitly, or dynamically via Object.defineProperty. Exceptions are if the child is a(n):
    1. input element.
    2. form element.
    3. HTML Element with contentEditable attribute.
    4. Microdata element (meta, link, data)

Special Symbols

In the examples below, we will encounter special symbols used in order to keep the statements small:

SymbolMeaningNotes
/propName"Hostish"Attaches listeners to a "propagator" EventTarget.
@propNameName attributeListens for input events by default.
|propNameItemprop attributeIf contenteditible, listens for input events by default. Otherwise, uses be-value-added.
#propNameId attributeListens for input events by default.
-prop-nameMarker indicates propAttaches listeners to a "propagator" EventTarget.
~customElementNameInCamelCasePeer custom element within the nearest itemscope perimeter (recursively)Attaches listeners to a "propagator" EventTarget.

"Hostish" means:

  1. First, do a "closest" for an element with attribute itemscope, where the tag name has a dash in it. Do that search recursively.
  2. If no match found, use getRootNode().host.

Part I Full Inference

The most quintessential example

<mood-stone>
    <template shadowrootmode=open>
        <div itemscope>
            <span itemprop=currentMood></span>
        </div>
        <input 
            name=currentMood 
            be-bound
        >
        <xtal-element
            prop-defaults='{
                "currentMood": "Happy"
            }'
            xform='{
                "| currentMood": 0
            }'
        ></xtal-element>
        <be-hive></be-hive>
    </template>
</mood-stone>

xtal-element is a declarative custom element solution that takes the live DOM element it belongs to and turns it into a web component for repeated use. The thing to focus on is:

<mood-stone>
    <template shadowrootmode=open>
        <input 
            name=currentMood 
            be-bound
        >
    </template>
</mood-stone>

be-bound two-way binds the input element's value property to mood-stone's currentMood property. Here, be-bound is "piggy-backing" on the name of the input element, in the common use case that the name matches the property name from the host that we are binding to. Scroll down to see how the syntax changes a bit to support scenarios where we can't rely on the name of the input field matching the host's property.

What value from the adorned element (input) should be two-way bound the host's someStringProp property if it isn't specified? The rules are as follows:

Some type aware inferencing:

<mood-stone>
    <template shadowrootmode=open>
        <div itemscope>
            <span itemprop=isHappy></span>
        </div>
        <input 
            name=isHappy
            type=checkbox
            be-bound
        >
        <xtal-element
            prop-defaults='{
                "isHappy": true
            }'
            xform='{
                "| isHappy": 0
            }'
        ></xtal-element>
        <be-hive></be-hive>
    </template>
</mood-stone>

If type=checkbox, property "checked" is used in the two way binding.

If type=number, valueAsNumber is used.

During the initial handshake, what if both the input element has a value, and so does my-host-element's hostProp property and they differ? Which property value "trumps"?

We decide this based on "specificity":

Object type trumps number type which trumps boolean type which trumps string type which trumps null type which trumps undefined type.

If the two types are the same, if the two types aren't of type object, the longer toString() trumps the shorter toString(). For object types, use JSON.stringify, and compare lengths.

As mentioned, we can't alway rely on using the name attribute to specify the host property name we want to bind to.

So now we start adding some information into the be-bound attribute.

For that, we use what I call "Hemingway notation" within the attribute, where the text of the attribute is meant to form a complete, grammatically correct sentence, ideally. Strictly speaking, the sentence sounds more complete if the "be-bound" attribute name is considered as part of the sentence. So please apply a little bit of generous artistic license to the principle we are trying to follow here, dear reader.

Specifying the host property name.

<mood-stone>
    #shadow
        ...
        <input be-bound='with /currentMood.'>
</mood-stone>

Using smaller words.

I find this a bit more readable, personally (but it is admittedly subjective). Anyway, it is supported:

<mood-stone>
    #shadow
        ...
        <input be-bound='with / current mood.'>
</mood-stone>

Both will work, so it is a matter of taste which is more readable/easier to type.

The slash (/) is a special symbol which we use to indicate that someStringProp comes from the host.

We don't have to two-way bind with a property from the host. We can also two way bind with peer elements within the HTML markup of the web component, based on other single character symbols, which indicates what we are binding to.

However, because we anticipate this element enhancement would most typically be used to two-way bind to a property coming from the host, we assume that that is the intention if no symbol is provided, making the syntax a little more readable / Hemingway like:

Least cryptic?

<mood-stone>
    #shadow
        ...
        <input be-bound='with current mood.'>
</mood-stone>

Note that the first word can either be capitalized or not capitalized, whichever seems more readable.

In the examples that follow, we will use these forms interchangeably, whatever seems more readable.

Non form-associated bindings with contentEditable

<mood-stone>
    #shadow
        ...
        <span contentEditable be-bound='with current mood.'></span>
</mood-stone>

Use of itemprop microdata attribute

<my-custom-element>
    #shadow
        <div itemscope>
            <span contenteditable itemprop=someStringProp be-bound>i am here</span>
        </div>
</my-custom-element>

Two way binding with peer elements

By Name

<input name=search>
...
<span contenteditable be-bound='with @search.'>

Perimeter support

In the example above, the search for the matching element is done within the nearest form, or within the (shadow)root node.

To specify to search within a closest perimeter, use the ^ symbol:

<section>
    Ignore this section
    <input name=search>
</section>
<section>
    Use this section
    <input name=search>
    ...
    <span contenteditable be-bound='with ^section@search.'>
</section>

By itemprop

<span contenteditable itemprop=search>
...
<input be-bound='with |search.'>

In this case, the span's textContent property is kept in synch with the value of the search input element.

The search for the bound element is done, recursively, within itemscope attributed elements, and if not found, within the root node. Similar perimeterizing can be done done with the ^ qualifier.

Binding with non visible HTML Signals

<meta itemprop=searchProp>
...
<input be-bound='with | search prop.'>

By id

<input id=search>

...

<span contenteditable be-bound='with # search.'></span>

By marker

<mood-stone -current-mood>
    <template shadowrootmode=open>
        <div itemscope>
            <span itemprop=currentMood></span>
        </div>
        <!-- This turns mood-stone into a custom element -->
        <xtal-element
            prop-defaults='{
                "currentMood": "Happy"
            }'
            xform='{
                "| currentMood": 0
            }'
        ></xtal-element>
        <be-hive></be-hive>
    </template>
</mood-stone>

<input be-bound="with -current-mood">

This can also work with built-in elements.

By peer custom element TODO

This is quite similar to the example above, but doesn't involve adding a non-standard attribute to the peer custom element. It's a less less transparent that there is a two way connection, but it opens up more opportunities for customizations. Anyway..

<mood-stone>
    <template shadowrootmode=open>
        <div itemscope>
            <span itemprop=currentMood></span>
        </div>
        <!-- This turns mood-stone into a custom element -->
        <xtal-element
            prop-defaults='{
                "currentMood": "Happy"
            }'
            xform='{
                "| currentMood": 0
            }'
        ></xtal-element>
        <be-hive></be-hive>
    </template>
</mood-stone>

<input be-bound="with ~MoodStone:currentMode">

Being more explicit

In all the examples we've seen so far, the element adorned by this be-bound enhancement was a built-in element, where we can usually infer the property we would want to bind to ("value" for input element, "textContent" from other types, for example).

What happens if our local element we are adorning isn't a built-in element. What we need to (or simply want to) be more explicit about what's happening? To support this, we need to switch from "with" statements, like we've seen thus far with "between" statements, as demonstrated below:

Specifying local property to bind to

<label>
    <input name=howAmIFeeling>
</label>
...
<mood-stone enh-be-bound='between currentMood and @howAmIFeeling.'></my-custom-element>

We add the extra enh- prefix to hopefully avoid "stepping on the toes" of some other custom enhancement, based on the recommended reserved prefix for this purpose.

So, when the attribute starts with the word "Between" or "between", as opposed to "With" or "with", it means we are specifying, first, the name of the local property name of the adorned element that we want to "sync up" with an "upstream" element. In this case, with the input element based on the name attribute. (But we can also synchronize with host properties if we use the "/" "sigil" as we've seen previously, or no sigil at all).

Specifying remote property to bind to TODO

Special logic for forms

<input id=alternativeRating type=number>
<form be-bound='between rating:value::change and #alternativeRating.'>
    <div part=rating-stars class="rating__stars">
        <input id="rating-1" class="rating__input rating__input-1" type="radio" name="rating" value="1">
        <input id="rating-2" class="rating__input rating__input-2" type="radio" name="rating" value="2">
        <input id="rating-3" class="rating__input rating__input-3" type="radio" name="rating" value="3">
        <input id="rating-4" class="rating__input rating__input-4" type="radio" name="rating" value="4">
        <input id="rating-5" class="rating__input rating__input-5" type="radio" name="rating" value="5">
    </div>  
</form>

Real world examples TODO: update to use the current syntax

scratch-box

Viewing Demos Locally

Any web server that can serve static files will do, but...

  1. Install git.
  2. Fork/clone this repo.
  3. Install node.js.
  4. Open command window to folder where you cloned this repo.
  5. npm install

  6. npm run serve

  7. Open http://localhost:3030/demo/ in a modern browser.

Running Tests

> npm run test

Using from ESM Module:

import 'be-bound/be-bound.js';

Using from CDN:

<script type=module crossorigin=anonymous>
    import 'https://esm.run/be-bound';
</script>
0.0.26

1 month ago

0.0.25

2 months ago

0.0.24

4 months ago

0.0.22

4 months ago

0.0.23

4 months ago

0.0.21

4 months ago

0.0.20

7 months ago

0.0.11

10 months ago

0.0.12

10 months ago

0.0.13

10 months ago

0.0.14

8 months ago

0.0.15

7 months ago

0.0.16

7 months ago

0.0.17

7 months ago

0.0.18

7 months ago

0.0.19

7 months ago

0.0.10

11 months ago

0.0.9

11 months ago

0.0.8

11 months ago

0.0.7

12 months ago

0.0.3

1 year ago

0.0.5

1 year ago

0.0.4

1 year ago

0.0.6

1 year ago

0.0.2

2 years ago

0.0.1

2 years ago

0.0.0

2 years ago