kubetail-org / sentineljs

Detect new DOM nodes using CSS selectors (650 bytes)
MIT License
1.13k stars 51 forks source link

multiple calls with multiple selectors not working combined #16

Closed petermonte closed 4 years ago

petermonte commented 4 years ago

Context:

I'm using the Sentinel.js to initialise several components and functionalities on the DOM.

Issue:

Most of these components get to use plenty of css and js functionalities by using classnames or attributes. It seems that some of them get ignored or overwritten.

I've created a demo to show this behaviour: https://codepen.io/petermonte/pen/XWWKRNv?editors=0010

Code:

HTML

<h1 id="h1" class="h1" data-h1="dataset">This is an Heading 1</h1>

CSS

h1[data-underline="true"] {
  text-decoration: underline;
}

JS

// By classname
sentinel.on('.h1', function(el) {
  // BECOMES BLUE
  el.style.color = 'blue';
});

// By node name
sentinel.on('h1', function(el) {
  // CHANGES TEXT TO: This is an Heading 1 + H1
  el.textContent += ' + ' + el.nodeName;
});

// By ID
sentinel.on('#h1', function(el) {
  // CHANGES TEXT TO: This is an Heading 1 + H1 + h1
  el.textContent += ' + ' + el.id;
});

// By data attribute
sentinel.on('[data-h1]', function(el) {
  // MAKES TEXT UNDERLINED
  el.dataset.underline = true;
});

Expected:

the idea is that all calls should run adding up all the changes to the element. Play arround by commenting all js sentinel calls and see how the element only reacts the a single call.

petermonte commented 4 years ago

Note: I've read the documentation stating that an extra custom animation can be added, but how would this be possible if all calls are dynamic?

amorey commented 4 years ago

Thanks for the pen. The problem is that the browser only triggers one animation per element unless multiple animations are explicitly defined in the css.

Here's how you can do it with custom animation names: https://codepen.io/muicss/pen/JjjKgYB

And here's how you can do it with multiple listeners: https://codepen.io/muicss/pen/LYYZwmo

petermonte commented 4 years ago

thanks @amorey,

Unfortunately, like I said, I'm building an entire UI framework kit that as an ocean of helpers/utilities, components, modules and widgets and all should trigger by themselves once added to the DOM. This means that I can't control all the css animation properties of all possible combinations that all apps can create.

For example I can have something like this:

<tabs class="tabs-fill" data-class-xs="tabs-justify tabs-outline">

Two triggers are created with this tabs element:

TABS is triggered to initialise the default behaviour

sentinel.on('tabs', function(el) {
    uikit.tabs(el);
});

CLASSSWAP is triggered to initialise the desired responsive behaviour

sentinel.on(['[data-class-xs]', '[data-class-sm]', '[data-class-md]', '[data-class-lg]'], function(el) {
    uikit.swappclass(el);
});

So this is just a small combination of so many others that can exist. It's only until until now that I'm facing this problem since I'm starting to make use of multiple combination of components with class helpers.

I really don't know how to overcome this. I might use an hybrid approach like described here where the animationstart is triggered for the entire document and the event.target is parsed and directed to the respective classes.

amorey commented 4 years ago

Triggering animationStart for every element will degrade performance but you can probably limit the number of triggers by being more selective with the CSS:

tabs, [data-uikit-*] {
  animation-duration: .0001s;
  animation-name: uikit-node-inserted;
}
sentinel.on('!uikit-node-inserted', function(el) {
  if (el.tagName === 'TABS') { /* do something */ }
  if (/data-uikit-class-xs/.test(el.className)) { /* do something */ }
});
petermonte commented 4 years ago

@amorey thanks.

Gonna drop here a simple example of how I adapted Sentinel to my App, in case someone encounters the same issue. This example is to show how we can have a component running with multiple calls with Sentinel avoiding css animation name conflicts.

You can check the code running at https://codepen.io/petermonte/pen/Vwwqgjv

I've used a component like a simple Tabs using a classname selector and another functionality using an attribute definition. So this is a very likely scenario to happen on an APP.

CSS for each component

.tabs,
[data-attribute] {
  animation-duration: .0001s;
  animation-name: node-inserted;
}

APP main class

This is where we focus our APP methods for component registration

;(function(){
    "use strict";

    const _components = [];

    window.APP = function (){
        // APP MAIN FUNCTION
    }

    APP.getRegisteredComponents = function () {
        return _components;
    };

    APP.registerComponent = function (_name, _callback, _selector) {
        if (!APP.hasOwnProperty(_name)) {
            // Register the component function on our main APP
            Object.defineProperty(APP, _name, {
                value: _callback,
                enumerable: true,
                configurable: true
            });

            // Record component for Sentinel to run 
            _components.push({
                name: _name,
                callback: _callback,
                selector: _selector
            });
        }

        return true;
    };

    APP.runSentinel = function () {
        sentinel.on('!node-inserted', function(el) {
            APP.getRegisteredComponents().forEach(function(component){
                if(el.matches(component.selector)){
                    component.callback(el);
                }
            });
        });
        return true;
    };
}());

APP components by selector type.

So we can have a component that is based on a classname, or an attribute, etc.

;(function(){
    "use strict";

    function tabs (el){
        // Lets prevent processing a component that as already been processed
        if (el.dataset.tabsInit == 'true'){
            return el;
        }

        // RUN EVERYTHING NEEDED FOR THE COMPONENT
        console.log('component of type tabs :', el);

        // Register the init state
        el.dataset.tabsInit = true;        
        return el;
    }

    APP.registerComponent(
        'tabs',
        tabs,
        '.tabs:not([data-tabs-init="true"])'
    );
}());

;(function(){
    "use strict";

    function functionalityByAttr (el){
        // Lets prevent processing a component that as already been processed
        if (el.dataset.functionalityByAttrInit == 'true'){
            return el;
        }

        // RUN EVERYTHING NEEDED FOR THE COMPONENT
        console.log('functionality by attribute :', el);

        // Register the init state
        el.dataset.functionalityByAttrInit = true;        
        return el;
    }

    APP.registerComponent(
        'functionalityByAttr',
        functionalityByAttr,
        '[data-attribute]:not([data-functionality-by-attribute="true"])'
    );
}());

Run Sentinel

Now that we have all our css and script configured we run Sentinel to listen to any added element to the DOM

APP.runSentinel();

Example of an element with multiple calls

<div class="tabs" data-attribute="abcd">
    ...
</div>
amorey commented 4 years ago

Thanks for sharing! Nice use of .matches().