Прокси - конструктор WebComponent, который расширяет HTMLElement

Итак, в библиотеке, которую я создаю, которая использует пользовательские элементы, вам, очевидно, нужно определить класс в CustomElementsRegistry, прежде чем вы сможете его создать.

Как сейчас, это решается с помощью декоратора:

class Component extends HTMLElement {

    static register (componentName) {
        return component => {
            window.customElements.define(componentName, component);
            return component;
        }
    }
}

@Component.register('my-element')
class MyElement extends Component { }

document.body.appendChild(new MyElement());

Это работает, однако я бы хотел автоматически зарегистрировать настраиваемый элемент при создании экземпляра класса (чтобы автор не добавлял декоратор к каждому отдельному компоненту, который они пишут). Это может быть достигнуто с помощью Proxy.


Моя проблема заключается в том, что когда я пытаюсь использовать прокси-сервер в конструкторе и пытаюсь вернуть экземпляр объекта, я все равно получаю Illegal Constructor, как если бы этот элемент никогда не был определен в реестре.

Это, очевидно, связано с тем, как я создаю экземпляр класса внутри прокси-сервера, но я не уверен, как это сделать в противном случае. Мой код выглядит следующим образом:

Пожалуйста, запустите последний Chrome:

class Component extends HTMLElement {

    static get componentName () {
        return this.name.replace(/[A-Z]/g, char => `-${ char.toLowerCase() }`).substring(1);
    }
}

const ProxiedComponent = new Proxy(Component, {

    construct (target, args, extender) {
        const { componentName } = extender;
  	
        if (!window.customElements.get(componentName)) {
            window.customElements.define(componentName, extender);
        }
    
        return new target(); // culprit
    }
});

class MyElement extends ProxiedComponent { }

document.body.appendChild(new MyElement());

Ответ 1

Было 2 проблемы:

  • new target() создан LibElement экземпляр, который не зарегистрирован как пользовательский элемент. И здесь вы получили ошибку Illegal Constructor.
  • даже если вы зарегистрируете LibElement, в результате элемент DOM будет <lib-element>, вы вызовите new target, и в этот момент javascript не имеет понятия о дочернем классе.

Единственный способ, которым я нашел, - использовать API Reflect для создания правильного экземпляра объекта.

class LibElement extends HTMLElement {
    static get componentName () {
        return this.name.replace(/[A-Z]/g, char => `-${ char.toLowerCase() }`).substring(1);
    }
}

const LibElementProxy = new Proxy(LibElement, {
    construct (base, args, extended) {
        if (!customElements.get(extended.componentName)) {
            customElements.define(extended.componentName, extended);
        }
    
        return Reflect.construct(base, args, extended);
    }
});

class MyCustomComponent extends LibElementProxy {}
class MyCustomComponentExtended extends MyCustomComponent {}

document.body.appendChild(new MyCustomComponent());
document.body.appendChild(new MyCustomComponentExtended());

Ответ 2

Здесь вы очень близки к решению, и единственная проблема заключается в том, что native HTMLElement не может быть создан с ключевым словом new и должен быть создан с помощью document.createElement. Вы можете повторно использовать все, что у вас есть, и только заменить возвращаемое значение метода construct в прокси:

class Component extends HTMLElement {

  static get componentName() {
    return this.name.replace(/[A-Z]/g, char => `-${ char.toLowerCase() }`).substring(1);
  }
}

const ProxiedComponent = new Proxy(Component, {

  construct(target, arguments, extender) {
    const {
      componentName
    } = target;

    if (!window.customElements.get(componentName)) {
      window.customElements.define(componentName, extender);
    }

    return document.createElement(target.componentName); // Properly constructs the new element
  }
});

class MyElement extends ProxiedComponent {}

document.body.appendChild(new MyElement());