brython-dev / brython

Brython (Browser Python) is an implementation of Python 3 running in the browser
BSD 3-Clause "New" or "Revised" License
6.37k stars 509 forks source link

If the Web Component has a shadow DOM, iterating over childNodes will return only text nodes #2469

Open moepnse opened 3 months ago

moepnse commented 3 months ago

Hi!

If self._childs_root is a shadow DOM, then only text nodes are returned when iterating over self.childNodes. A default slot is missing on purpose.

I tried the same with JavaScript in the developer console and it returns every node inside the web component. Is this a bug or am I missing something?

Thanks in advance!

<!DOCTYPE html>
<html>
    <head>
        <!-- Required meta tags-->
        <meta charset="utf-8">

        <title>Issue Demo</title>

        <!-- Brython -->
        <script src="https://raw.githack.com/brython-dev/brython/master/www/src/brython.js"></script>
        <script src="https://raw.githack.com/brython-dev/brython/master/www/src/brython_stdlib.js"></script>
        <script type="text/python">
import copy
from browser import webcomponent, html, window, console, document, aio

class BaseComponent:

    _registry = []
    _initialized = False
    _logic_obj = None
    _is_container: bool = False
    _observed_attributes = []

    def __init__(self):
        # Create a shadow root
        shadow = self._shadow = self.attachShadow({'mode': 'open'})
        self._childs_root = self._shadow
        #self._childs_root = self

    @property
    def observedAttributes(self):
        return self._observed_attributes

    def attributeChangedCallback(self, name, old, new, ns):
        print(f"attribute {name} changed from {old} to {new}")

    def _append_childs(self):
        console.debug("append_childs", self, self.childNodes)
        root = self._childs_root
        # If root is a shadow DOM, then only TextNodes are returned from the iterator.
        for child in self.childNodes:
            console.debug("appending child", child, "to", root)
            root.appendChild(child)

    def connectedCallback(self):
        if not self._initialized:
            self._append_childs()
            self._initialized = True

    @staticmethod
    def un_camel(word: str) -> str:
        upper_chars: str = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'
        last_char: str = word[0]
        output: list = [last_char.lower()]
        for c in word[1:]:
            if c == "_":
                output.append("-")
                continue
            if c in upper_chars:
                if last_char not in upper_chars:
                    output.append('-')
                output.append(c.lower())
            else:
                output.append(c)
            last_char = c
        return "".join(output)

    @classmethod
    def __init_subclass__(cls, **kwargs):
        BaseComponent._registry.append(cls)

    @classmethod
    def remove_from_registry(cls, component):
        print(cls._registry, component)
        if component in cls._registry:
            cls._registry.remove(component)

    @classmethod
    def register(cls):
        registry = cls._registry
        for web_component in registry:
            web_component_name = cls.un_camel(web_component.__name__)
            component_name = f"ui-{web_component_name}"
            console.debug(f"registering web component {web_component} as {component_name}...")
            webcomponent.define(component_name, web_component)

class Page(BaseComponent):
    pass

class Icon(BaseComponent):

    def connectedCallback(self):
        if not self._initialized:
            self._initialized = True

BaseComponent.register()
        </script>
    </head>
    <body onload="brython({debug: 10})">
        <ui-page>
            text node 1 
           <ui-icon id="icon" class="demo">save</ui-icon>
           text node 2
           <!-- comment -->
        </ui-page>
    </body>
</html>
moepnse commented 3 months ago

Here is an equivalent example written in JavaScript that behaves as expected.

<!DOCTYPE html>
<html>
    <head>
        <meta charset="utf-8">
        <script type="text/javascript">
window.onload = function() {
    class FooBar extends HTMLElement {
        constructor(){
            super();
            this.shadow = this.attachShadow({ mode: 'open' })
        }
        connectedCallback() {
            for (var i = 0; i < this.childNodes.length; i++) {
                console.debug(this.childNodes[i]);
            }
        }
    }

    customElements.define('foo-bar', FooBar);

    class UIIcon extends HTMLElement {
        constructor(){
            super();
            this.shadow = this.attachShadow({ mode: 'open' })
        }
    }

    customElements.define('ui-icon', UIIcon);
}
      </script> 
  </head>
  <body>
    <foo-bar>
      <h1>Test</h1>
      <ui-icon></ui-icon>
    </foo-bar>
</body>
PierreQuentel commented 3 months ago

Hello !

In _append_childs() it seems that root.appendChild(child) removes the child from self.childNodes (I didn't test but it's probably the same in Javascript) ; if you remove it, the 5 child nodes are printed.

I think the problem can be solved by replacing for child in self.childNodes: by for child in list(self.childNodes):