be-observant
v0.0.178
Published
Glue DOM and custom elements together via JSON declarations.
Downloads
442
Maintainers
Readme
be-observant 🔭 [WIP]
Observe properties of peer elements or the host.
be-observant takes less of a "top-down" approach to binding than traditional frameworks. It places less emphasis (but certainly not none) on binding exclusively from the (custom element) component container. Yes, it can do that, but it can also provide for "Democratic Web Component Organisms" where the host container acts as a very thin "Skin Layer" which can be passed a small number of "stimuli" values into. Inside the body of the web component, we might have a non visible "brain" component that dispatches events. be-observant allows other peer elements within the "body" to receive messages that the brain component emits, without forcing the outer "skin" layer to have to micromanage this all.
[!Note] An extra thin layer can be applied on top of be-observant, so that the original HTML that is streamed from the server can provide the initial values of the property that be-observant observes, and then once that initial handshake is established, lean exclusively on be-observant for all subsequent updates. This is handled by be-entrusting.
The most quintessential example
The example below follows the traditional "pass props down" from the host approach, only really it is "pulling props in". You say tomāto, I say tomäto kind of thing.
<mood-stone>
#shadow
<input
name=isHappy
disabled
type=checkbox
be-observant>
</mood-stone>
What this does: Observes and one-way passes mood-stone's isHappy property value to the input element's checked property.
be-observant is making a few inferences:
- The name of the input element ("isHappy") will match with the name of the host property from which we would want to bind it. Why adopt confusing mappings if we can possibly avoid it?
- Since the type of input element is of type checkbox, it sets the local "checked" property to match the "isHappy" property from the host.
[!Note] be-observant is a rather lengthy word to have to type over and over again, and this element enhancement would likely be sprinkled around quite a bit in a web application. The name is registered in the optional file behivior.js so to use whatever name makes sense to you (🔭, be-obs?) within your application, just don't reference that file, and instead create and reference your own registration file. Names can also be overridden within a Shadow scope as well. Throughout much of the rest of this document, we will use 🔭 instead of be-observant, and ask that you make a "mental map" of 🔭 to "be-observant". In fact, this package does provide an alternative registration file, 🔭.js, that registers the enhancement via attribute "🔭". The developer could easily copy/modify an additional registration file, to adopt their own preferred name.
If you only use this enhancement once in a large application, spelling out the full name (and referencing the canonical behivior.js file) would probably make the most sense, for "locality of behavior" reasons, and also tapping into google searches. But I would strongly consider using a (custom) shortcut in any application that intends to rely on this enhancement in a heavy way.
Back to our quintessential example
As we already discussed, in the example above, we made the assumption that if the user gives the input element name "isHappy", that the choice of name will most likely match the identical property name coming from the host web component container.
If this assumption doesn't hold in some cases, then we can specify the name of the property we want to observe from the host:
Specifying the host property to observe
<mood-stone>
#shadow
<input
type=checkbox
disabled
be-observant='of /isHappy.'
>
</mood-stone>
Now that we've spelled out the full word twice (be-observant), from now on, we will use "🔭" as our shortcut for be-observant, but please apply the mental mapping from 🔭 to the full name, for the statements to make the most sense.
The slash ("/") symbol indicates to get the value from the host. If omitted, it is assumed:
Reducing cryptic syntax
<mood-stone>
#shadow
<input
type=checkbox
disabled
🔭='of isHappy.'
>
</mood-stone>
Hosts that do not use shadow DOM.
If Shadow DOM is not used, add the "itemscope" attribute so that be-observant knows what to look for:
<mood-stone itemscope>
<input
type=checkbox
disabled
🔭='of isHappy.'
>
</mood-stone>
Binding based on microdata attribute.
<mood-stone>
<template shadowrootmode=open>
<div itemscope>
<span itemprop=isHappy 🔭></span>
</div>
<xtal-element
prop-defaults='{
"isHappy": true
}'
></xtal-element>
<be-hive></be-hive>
</template>
</mood-stone>
This sets the span's textContent to the .toString value of moon-stone's isHappy property, and monitors for changes, i.e. one-way binds.
By Id also works:
<mood-stone>
<template shadowrootmode=open>
<div itemscope>
<span itemprop=isHappy></span>
</div>
<input
id=isHappy
disabled
type=checkbox
🔭
>
<xtal-element
prop-defaults='{
"isHappy": true
}'
xform='{
"| isHappy": 0
}'
></xtal-element>
<be-hive></be-hive>
</template>
</mood-stone>
Note that the itemprop attribute takes precedence over the name attribute, which takes precedence over the id attribute.
DSS Specifier Syntax
In the example above, we mentioned using the / symbol to indicate to observe a property from the host. But be-observant can also observe peer elements within the ShadowRoot (or outside any shadow root be-observant adorns an element sitting outside any ShadowRoot).
The syntax adopts what we refer as the DSS specification, where DSS stands for "directed scoped specifier". It is inspired by CSS selectors, but it is optimized for binding scenarios.
This is documented in (increasingly) painstaking detail where the library is maintained.
Binding to peer elements
Now we will start to see how be-observant provides for more "grass-roots" democratic organism (web component) support.
By name attribute
<input name=search type=search>
<div 🔭='of @search.'></div>
As the user types in the input field, the div's text content reflects the value that was typed.
The search for the element with name=search is done within the closest form element, and if not found, within the (shadow)root node.
By id
This also works:
<input id=searchString type=search>
<div 🔭='of #searchString.'></div>
The search for element with id=searchString is done within the (shadow)root node, since id's are supposed to be unique with a (shadow)root node.
By markers
<mood-stone>
#shadow
<my-peer-element -some-bool-prop></my-peer-element>
<input
type=checkbox
disabled
🔭='of -some-bool-prop'
>
</mood-stone>
This observes the my-peer-element's someBoolProp property for changes and sets the adorned element's checked property based on the current value.
By itemprop
<link itemprop=isHappy href=https://schema.org/True>
...
<input
type=checkbox
🔭='of |isHappy.'
>
What this does: If necessary, auto attaches the be-value-added enhancement to the link element, which recognizes the True/False values of schema.org as far as the href attribute, and provides a property oHTMLLinkElement.beValueAdded.value through which updated values can be passed / listened to. Essentially it provides a hidden boolean "signal" we can bind to and also use for styling purposes.
The editable checkbox element can observe changes to this "signal".
We saw earlier that we can adorn elements with the itemprop attribute with 🔭 attribute, and it will automatically pull in values from the host. This allows us to create a code-free "chain" of bindings from the host to Shadow children, and from the Shadow children to peer elements.
Specifying the property to assign the observed value(s) to.
What we've seen above is a lot of mind reading about what our intentions are, as far as how we want to apply what we are observing to the element adorned by be-observant. Sometimes we are setting the "checked" property. Sometimes we are setting the textContent.
But sometimes we need to be more explicit because it isn't always transparent what we intend.
Single mapping from what to observe, specifying the property to target.
<input name=someCheckbox type=checkbox>
<my-peer-element enh-🔭='
and set someBoolProp from @someCheckbox.
'></my-peer-element>
This watches the input element for input events and passes the checked property to someBoolProp of oMyPeerElement. The word "and" is optional, there to allow for people who like to read complete sentences (including the (mentally mapped) attribute name)
The enh- prefix is there to avoid possible conflicts with attributes recognized by my-peer-element, in the absence of any tending loving care from the platform.
Multiple parallel observers
This example works, where each observing statement is treated independently:
<input name=someCheckbox type=checkbox>
<input name=someOtherCheckbox type=checkbox>
<mood-stone enh-🔭='and set isHappy from @someCheckbox.
Set isWealthy from @someOtherCheckbox.
'>
<template shadowrootmode=open>
<div itemscope>
is happy
<div itemprop=isHappy></div>
is wealthy
<div itemprop=isWealthy></div>
</div>
<xtal-element infer-props
prop-defaults='{
"isHappy": true,
"isWealthy": false
}'
xform='{
"| isHappy": 0,
"| isWealthy": 0
}'
></xtal-element>
<be-hive></be-hive>
</template>
</mood-stone>
Unionizing
If multiple remote endpoints are observed that map to a single local prop, by default, the "truthy" conjunction (&&) is applied to them all. This will often result in passing in the value of the last property, unless the properties are actual booleans as they are below:
<input name=someCheckbox type=checkbox>
<input name=someOtherCheckbox type=checkbox>
<mood-stone enh-🔭='and set isHappy from @someCheckbox and @someOtherCheckbox.'>
<template shadowrootmode=open>
<div itemscope>
is happy
<div itemprop=isHappy></div>
</div>
<xtal-element
prop-defaults='{
"isHappy": true,
}'
xform='{
"| isHappy": 0,
}'
></xtal-element>
<be-hive></be-hive>
</template>
</mood-stone>
In other words, in this example, the mood-stone's "isHappy" property will be set if either checkbox is checked.
The number of things we can observe is limited only by when the developer tires of typing the word "and".
be-observant also support additional ways of combining multiple remote endpoints into one local prop.
They are:
- Union
<mood-stone enh-🔭='(and) set isHappy to the union of @someCheckbox and @someOtherCheckbox.'>
- Sum [Untested]
<mood-stone enh-🔭='and set mySum to the sum of @someNumericInput and @someOtherNumericInput.'>
- Product [Untested]
<mood-stone enh-🔭='and set myProduct to the product of @someNumericInput and @someOtherNumericInput.'>
- Interpolation [TODO]
<mood-stone enh-🔭='and set sentenceProp to ${0} eats $1}
weaving in @name and @food.'>
- Object Assignment [Untested]
<mood-stone enh-🔭='and set myObjectProp to an object structure by assigning @name and @food.'>
For the power hungry JS-firsters
As our business logic becomes more complex, I suspect many readers will begin asking themselves:
This is all great, but what if I just want to do some coding? Why learn all this contrived syntax?
Fair enough.
We now provide an interlude where we indicate how to inject JavaScript into the picture, and set properties, and derive properties as we need, with full, unfettered access to the JavaScript run time.
Scripting bravely
be-observant empowers the developer to tap into the full power of the JavaScript run time engine by adding script to the onload event of the adorned element.
If we know that this enhancement is the only enhancement affecting the adorned element that leverages the onload event, we can skip some defensive maneuvers that avoid collisions with other enhancements (discussed in the next example), resulting in a fairly compact script:
<label>
Name:
<input name=name>
</label>
<label>
Food:
<input name=food>
</label>
<mood-stone enh-🔭='of @name and @food.'
onload="
const {factors, setProps} = event;
Object.assign(setProps, {
myFirstProp: `${factors.name} eats ${factors.food}`,
});
"
>
<template shadowrootmode=open>
<div itemscope>
<div itemprop=myFirstProp></div>
</div>
<xtal-element infer-props></xtal-element>
<be-hive></be-hive>
</template>
</mood-stone>
Note that it is also to add event listener to the adorned element (mood-stone in this case) with event 'load' and add additional properties to set.
Scripting defensively
If using an enhancement from the be-enhanced family of behaviors/enhancements, a key identifier that distinguishes enhancements from one another is the "enh" key. So this is how to code the onload event in such a way that it doesn't interfere with any other enhancements that make use of the onload event. Basically, just an added if condition:
<label>
Name:
<input name=name>
</label>
<label>
Food:
<input name=food>
</label>
<mood-stone enh-🔭='of @name and @food.'
onload="
const {factors, setProps, enh} = event;
switch(enh){
case '🔭':{
Object.assign(setProps, {
myFirstProp: `${factors.name} eats ${factors.food}`,
});
}
}
"
>
<template shadowrootmode=open>
<div itemscope>
<div itemprop=myFirstProp></div>
</div>
<xtal-element infer-props></xtal-element>
<be-hive></be-hive>
</template>
</mood-stone>
Attaching and setting other enhancement values [TODO]
<input name=search type=search>
<div 🔭='
and set +beSearching:forText from @search.
'>
supercalifragilisticexpialidocious
</div>
The plus symbol: + is indicating to tap into a custom enhancement.
The example above happens to refer to this enhancement.
Observing a specified property of a peer custom element
<tr itemscope>
<td>
<my-item-view-model></my-item-view-model>
<div 🔭="of ~myItemViewModel:myProp1.">My First column information</div>
</td>
<td>
<div 🔭="of ~myItemViewModel:myProp2."></div>
</td>
</tr>
The search for the my-item-view-model custom element is done within the closest "itemscope" attribute.
This can be useful for scenarios where we want to display repeated data, and can't use a custom element to host each repeated element (for example, rows of an HTML table), but we want to provide a custom element as the "view model" for each row.
This will one-way synchronize my-item-view-model's myProp 1/2 values to the adorned element's textContent property.
Inferring the property to observe from a peer custom element
This also works:
<tr itemscope>
<td>
<my-item-view-model></my-item-view-model>
<div itemprop=myProp1 🔭="of ~myItemViewModel.">My First column information</div>
</td>
<td>
<div itemprop=myProp2 🔭="of ~myItemViewModel."></div>
</td>
</tr>
We can specify what property of the peer custom element to bind to as follows:
Negation [TODO]
<mood-stone>
#shadow
<input name=someCheckbox type=checkbox>
<my-peer-element enh-🔭='
and set someBoolProp from the negation of @someCheckbox.
'></my-peer-element>
</mood-stone>
Toggle [TODO]
To simply toggle a property anytime the observed element changes:
<mood-stone>
#shadow
<input name=someCheckbox type=checkbox>
<my-peer-element enh-🔭='
and toggle someBoolProp on @someCheckbox::input.
'></my-peer-element>
</mood-stone>
PlusEq, MinusEq, TimeEq, DivEq [TODO]
Increment, Decrement [TODO]
Interpolating [TODO]
<mood-stone>
#shadow
<input name=name>
<input name=food>
<my-peer-element enh-🔭='
and set mySecondProp to `$1 eats $2` from @name and @food.
'></my-peer-element>
</mood-stone>
Adding / removing css classes / styles / parts declaratively [TODO]
<mood-stone>
#shadow
<input name=yourCheckbox type=checkbox>
<input name=myCheckbox type=checkbox>
<my-peer-element enh-🔭='
Of @yourCheckbox and @myCheckbox.
Set my-class to $1.
SetClass my-second-class to $2.
'></my-peer-element>
</mood-stone>
Since we are binding to booleans, adds class if true, otherwise removes.
If we bind to a string, simply sets the class to the value of the string.
Same with SetPart, SetStyle
Observing a specified property [TODO]
<my-peer-element></my-peer-element>
<your-peer-element enh-🔭="
of ~myPeerElement:myProp.
Set yourProp.
">
This will one-way synchronize my-peer-element's myProp value to the adorned element's yourProp property.
Example 5 Mapping [TODO]
Example 6
<-->
[!Note] be-observant provides similar functionality to be-bound. The difference is be-bound provides two-way binding between the adorned element and an upstream element, whereas be-observant is strictly one-way. Because it is one way, be-observant can apply some declarative adjustments to the value it is observing before applying to the adorned element.
Viewing Demos Locally
Any web server that can serve static files will do, but...
- Install git.
- Fork/clone this repo.
- Install node.js.
- Open command window to folder where you cloned this repo.
npm install
npm run serve
- Open http://localhost:3030/demo/ in a modern browser.
Running Tests
> npm run test
Using from ESM Module:
import 'be-observant/behivior.js';
or
import 'be-observant/🔭.js';
Using from CDN:
<script type=module crossorigin=anonymous>
import 'https://esm.run/be-observant';
</script>