Пользовательские элементы с проксированными параметрами has, get & set

Моя цель — создать класс ObservableElement, расширяющий HTMLElement, чтобы его можно было использовать для определения пользовательских элементов, например:

customElements.define('an-observable-elem', class extends ObservableElement {
  construct() {
    super()
    ...
  }
  ...
})

Суть любого элемента, основанного на ObservableElement, заключается в том, что он должен иметь определенное поведение по отношению к свойствам.

Во-первых, 'whatever' in myElem должно всегда быть истинным. Другими словами, мне нужна прокси-ловушка для has в экземпляре, которая всегда возвращает true.

Во-вторых, установка и получение любых реквизитов должны работать. Но всякий раз, когда устанавливается реквизит, который явно не определен для элемента или в цепочке прототипов, я хочу создать пользовательское событие с именем set${propname} и detail: theValue.

Кажется, должен быть способ использовать es6-прокси. По наивности я сначала попробовал:

class ObservableElement extends HTMLElement {
  constructor () {
    super()
    const vals = {}
    return Proxy(this, {
      has: _ => true,
      get: name => {
        if (name in this) return this[name]
        if (name in vals) return vals[name]
        return null
      },
      set: (name, value) => {
        if (name in this) {
          this[name] = value
          return
        }
        if (vals[name] === value) return
        vals[name] = value
        this.dispatchEvent(new CustomEvent(`set${name}`, {detail: value}))
      }
    })
  }
}

Но, конечно, это не сработало. Возврат прокси из конструктора не изменил значения this в конструкторах расширенных классов. Я возился со всякими комбинациями проксирования construct на класс, Object.setPrototypeOf(...) и т.д. безрезультатно.

Я был бы очень признателен, если бы кто-нибудь, кто понимает, как эти вещи могут сочетаться друг с другом для достижения того, чего я хочу, объяснил бы мне это. Спасибо!


person Zach    schedule 07.06.2019    source источник
comment
В ваших обработчиках прокси отсутствует параметр target, поэтому вы используете неправильные значения.   -  person Patrick Roberts    schedule 07.06.2019
comment
Кстати, я исправил ваш пример и попробовал сам. Вы не можете переопределить constructor, вернув объект, отличный от this, иначе вы получите ошибку Uncaught DOMException: custom element constructors must call super() first and must not return a different object, поэтому инкапсуляция экземпляра класса с помощью Proxy невозможна.   -  person Patrick Roberts    schedule 07.06.2019
comment
@PatrickRoberts, спасибо! Вы правы, ловушкам нужен целевой параметр, но это не проблема, с которой я столкнулся, а скорее вторая проблема, о которой вы упомянули. Однако я пока не верю, что полностью невозможно добавить прокси-ловушки в строящийся экземпляр. Вы можете проксировать сам класс и свойство construct. Я просто не могу понять, как собрать части вместе...   -  person Zach    schedule 10.06.2019
comment
Здесь нет подвоха. API веб-компонента спроектирован таким образом, что в DOM будут вставлены только экземпляры, расширяющие класс HTMLElement. Proxy — это экзотический объект, не имеющий встроенного prototype (его прототип унаследован от объекта, который он инкапсулирует), поэтому DOM не примет его как экземпляр HTMLElement.   -  person Patrick Roberts    schedule 10.06.2019
comment
Я понимаю. какая жалость. Любые другие идеи, как достичь цели создания пользовательского элемента, который может инициировать событие при назначении любого свойства (и будет действовать так, как если бы у него было какое-либо свойство)?   -  person Zach    schedule 15.06.2019


Ответы (1)


То, что вы пытаетесь сделать, не поддерживается спецификацией веб-компонентов W3C.

Согласно §2.4 спецификации веб-компонентов W3C, шаг 10.6.2. :

Если observedAttributesIterable не равно undefined, то установите observedAttributes в результат преобразования observedAttributesIterable в sequence<DOMString>. Восстановите все исключения из преобразования.

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

Если компонент не находит измененное свойство в этой последовательности, он не вызывает attributeChangedCallback().

Аналогично невозможно вернуть прокси из constructor из-за ограничение в §2.2:

Оператор return не должен появляться нигде внутри тела конструктора, если только он не является простым ранним возвратом (return или return this).

person Patrick Roberts    schedule 16.06.2019
comment
Спасибо за четкое объяснение с хорошими ссылками! - person Zach; 04.09.2019