be-decorated
    TypeScript icon, indicating that this package has built-in type declarations

    0.0.114 • Public • Published

    be-decorated

    Playwright Tests

    be-decorated provides a base class that enables attaching ES6 proxies onto other "Shadow DOM peer citizens" -- native DOM or custom elements in the same Shadow DOM realm.

    be-decorated provides a much more "conservative" alternative approach to enhancing existing DOM elements, in place of the controversial "is"-based customized built-in element standard-ish.

    In contrast to the "is" approach, we can apply multiple behaviors / decorators to the same element:

    #shadow-root (open)
    
        <black-eyed-peas 
            be-on-the-next-level=11
            be-rocking-over-that-bass-tremble
            be-chilling-with-my-motherfuckin-crew
        ></black-eyed-peas>
    
        <!-- Becomes, after upgrading -->
        <black-eyed-peas 
            is-on-the-next-level=11
            is-rocking-over-that-bass-tremble
            is-chilling-with-my-motherfuckin-crew
        ></black-eyed-peas>

    which seems slightly more readable than:

    <is-on-the-next-level level=11>
        <is-rocking-over-that-base-tremble>
            <is-chilling-with-my-motherfunckin-crew>
                <black-eyed-peas></black-eyed-peas>
            </is-chilling-with-my-motherfuckin-crew>
        </is-rocking-over-that-base-tremble>
    </is-on-the-next-level>

    Priors

    be-decorated's goals are quite similar to what is achieved via things that go by many names.

    We prefer "decorator" as the term, but "custom attribute", "directive", "behavior" is fine also.

    Differences to these solutions (perhaps):

    1. This can be used independently of any framework (web component based).
    2. Each decorator can be imported independently of others via ES6 module.
    3. Definition is class-based.
    4. Applies exclusively within Shadow DOM realms.
    5. Reactive properties are managed declaratively via JSON syntax.
    6. Namespace collisions easily avoidable within each shadow DOM realm.
    7. Use of ES6 proxies for extending properties allows us to avoid future conflicts.

    Prior to that, there was the heretical htc behaviors.

    Basic Syntax

    To define a decorator, define a "controller" class. The structure of the class is fairly wide open. The lifecycle event methods can have any name you want. For example:

    export class ButterbeerController{
        #self: ButterbeerCounterProps | undefined;
        init(self: ButterbeerCounterProps, btn: HTMLButtonElement){
            this.#self = self;
            btn.addEventListener('click', this.handleClick)
            self.count = 0;
            
        }
        onCountChange(){
            console.log(this.#self!.count);
        }
        handleClick = (e: MouseEvent) => {
            this.#self!.count++;
        }
    }

    Then use (mostly) JSON configuration to instruct be-decorated how to apply the decorator onto elements:

    import {ButterbeerController} from '[wherever]';
    import {define} from 'be-decorated/be-decorate.js';
    
    define({
        config:{
            tagName: 'be-a-butterbeer-counter',
            propDefaults:{
                virtualProps: ['count'],
                upgrade: 'button',
                ifWantsToBe: 'a-butterbeer-counter',
                intro: 'init'
            },
            actions:{
                'onCountChange': {
                    ifKeyIn: ['count']
                }
            }
        },
        complexPropDefaults:{
            controller: ButterbeerController,
        }
    });

    Note the specification of "virtualProps". Use of virtualProps is critical if we want to be guaranteed that our component doesn't break, should the native DOM element or custom element be enhanced with a new property with the same name.

    Within each shadow DOM realm, our decorator web component will only have an effect if an instance of the web component is plopped somewhere inside that shadow DOM.

    Although it is a bit of a nuisance to remember to plop an instance in each shadow DOM realm, it does gives us the ability to avoid name conflicts with other libraries that use custom attributes. In the example above, if we plop an instance inside the shadow DOM with no overrides:

    <button be-a-butterbeer-counter-bahrus-github='{"count": 30}'>Count</button>
    ...
    
    <be-a-butterbeer-counter-bahrus-github></be-a-butterbeer-counter-bahrus-github>

    then it will affect all buttons with attribute be-a-butterbeer-counter-bahrus-github within that shadow DOM.

    To specify a different attribute, override the default "ifWantsToBe" property thusly:

    <button be-a-b-c='{"count": 30}'>Count</button>
    ...
    
    <be-a-butterbeer-counter-bahrus-github if-wants-to-be=a-b-c></be-a-butterbeer-counter-bahrus-github>

    Another silver lining to this nuisance: It provides more transparency where the behavior modification is coming from.

    The be-hive component makes managing this nuisance almost seamless. If developing a component that uses more than a few decorators, it is probably worth the extra dependency.

    Note the use of long names of the web component. Since the key name used in the markup is configurable via if-wants-to-be, using long names for the web component, like guid's even, will really guarantee no namespace collisions, even without the help of pending standards. If be-hive is used to help manage the integration, developers don't really need to care too much what the actual name of the web component is, only the value of if-wants-to-be, which is configurable within each shadow DOM realm.

    Setting properties of the proxy externally

    Just as we need to be able to pass property values to custom elements, we need a way to do this with be-decorated elements. But how?

    The tricky thing about proxies is they're great if you have access to them, useless if you don't.

    Approach I. Programmatically, but carefully.

    be-decorated applies a "cardinal sin" and attaches a field onto the adorned element called beDecorated. Inside of which all the proxies based off of be-decorated are linked. So to set the property of a proxy via the element in adorns, we need to act gingerly:

    if(myElement.beDecorated === undefined) myElement.beDecorated = {};
    if(myElement.beDecorated.aButterbeerCounter === undefined) myElement.beDecorated.aButterbeerCounter = {};
    myElement.beDecorated.aButterbeerCounter.count = 7;

    The intention here is even if the element hasn't been upgraded yet, property settings set this way should be absorbed into the proxy once it becomes attached. And if the proxy is already attached, than those undefined checks will be superfluous, but better to play it safe.

    Approach II. Setting properties via the controlling attribute:

    A more elegant solution, perhaps, which xtal-decor supports, is to pass in properties via its custom attribute:

    <list-sorter upgrade=* if-wants-to-be=sorted></list-sorter>
    
    ...
    
    <ul be-sorted='{"direction":"asc","nodeSelectorToSortOn":"span"}'>
        <li>
            <span>Zorse</span>
        </li>
        <li>
            <span>Aardvark</span>
        </li>
    </ul>

    By the way, a vscode plug-in is available that makes editing JSON attributes like these much less susceptible to human fallibility.

    After list-sorter does its thing, the attribute "be-sorted" switches to "is-sorted":

    <ul is-sorted='{"direction":"asc","nodeSelectorToSortOn":"span"}'>
        <li>
            <span>Aardvark</span>
        </li>
        <li>
            <span>Zorse</span>
        </li>
    </ul>

    You cannot pass in new values by using the is-sorted attribute. Instead, you need to continue to use the be-sorted attribute:

    <ul id=list is-sorted>
        <li>
            <span>Aardvark</span>
        </li>
        <li>
            <span>Zorse</span>
        </li>
    </ul>
    
    <script>
        list.setAttribute('be-sorted', JSON.stringify({direction: 'desc'}))
    </script>

    The disadvantage of this approach is we are limited to JSON-serializable properties, and there is a cost to stringifying / parsing.

    Approach III. Integrate with other decorators -- binding decorators -- that hide the complexity

    be-observant provides a pattern, and exposes some reusable functions, for "pulling-in" bindings from the host or neighboring siblings. This can often be a sufficient and elegant way to deal with this concern.

    API

    This web component base class builds on the provided api:

    import { upgrade } from 'xtal-decor/upgrade.js';
    upgrade({
        shadowDOMPeer: ... //Apply trait to all elements within the same ShadowDOM realm as this node.
        upgrade: ... //CSS query to monitor for matching elements within ShadowDOM Realm.
        ifWantsToBe: // monitor for attributes that start with be-[ifWantsToBe], 
    }, callback);

    API example:

    import {upgrade} from 'xtal-decor/upgrade.js';
    upgrade({
        shadowDOMPeer: document.body,
        upgrade: 'black-eyed-peas',
        ifWantsToBe: 'on-the-next-level',
    }, target => {
        ...
    });

    The API by itself is much more open-ended, as you will need to entirely define what to do in your callback. In other words, the api provides no built-in support for creating a proxy and passing it to a controller.

    For the sticklers

    If you are concerned about using attributes that are prefixed with the non standard be-, use data-be instead:

    <list-sorter upgrade=* if-wants-to-be=sorted></list-sorter>
    
    ...
    
    <ul data-be-sorted='{"direction":"asc","nodeSelectorToSortOn":"span"}'>
        <li>
            <span>Zorse</span>
        </li>
        <li>
            <span>Aardvark</span>
        </li>
    </ul>

    Notifying

    Any be-decorated based decorator/behavior can be configured to emit namespaced events via the emitEvents property.

    If set to true, then all property changes will emit an event whenever a property change is made via the proxy.

    For example, if a property "foo" is modified via the proxy, and emitEvents is set to either true, or an array containing "foo", then an event will be dispatched from the adorned element with name "[if-wants-to-be]::foo-changed".

    Reserved, Universal Events

    If emitEvents is defined, then when the proxy has been established, the target element will emit event:

    "[if-wants-to-be]::is-[if-wants-to-be]".

    For example, this behavior:

    <form be-reformable='{}'>
    </form>

    will emit event "reformable::is-reformable" when the proxy has been created.

    The detail of the event contains the proxy, and the controllerInstance.

    Alternatively or in addition, be-noticed provides a pattern as far as syntax, as well as reusable code, that can pass things more directly, to the hosting (custom) element, or neighboring elements, similar to be-observant (but in the opposite direction).

    Primary prop

    Sometimes a decorator will only have a single, primitive-type property value to configure, at least for the time being. Or maybe there are multiple props, but one property in particular is clearly the most important, and the other properties will rarely deviate from the default value. In that case, the extra overhead from typing and parsing JSON just to read that value seems like overkill.

    So be-decorated provides a way of defining a "primary" property, and just set it based on the string value, if the string value doesn't start with a { or a [.

    Name of the property: "primaryProp"

    Viewing example from git clone or git fork:

    Install node.js. Then, from a command prompt from the folder of your git clone or github fork:

    $ npm install
    $ npm run serve
    
    Open http://localhost:3030/demo/dev.html
    

    Install

    npm i be-decorated

    DownloadsWeekly Downloads

    819

    Version

    0.0.114

    License

    MIT

    Unpacked Size

    48.2 kB

    Total Files

    15

    Last publish

    Collaborators

    • bahrus