Итерировать по HTMLCollection в пользовательском элементе - PullRequest
0 голосов
/ 03 ноября 2018

Как я могу перебрать экземпляры одного пользовательского элемента в тени другого пользовательского элемента? HTMLCollections, кажется, не ведет себя так, как ожидалось. (Я jQuerian и новичок, когда дело доходит до vanilla js, поэтому я уверен, что где-то допускаю очевидную ошибку).

HTML

<spk-root>
  <spk-input></spk-input>
  <spk-input></spk-input>
</spk-root>

Определения пользовательских элементов

Для spk-input:

class SpektacularInput extends HTMLElement {
  constructor() {
    super();
  }
}
window.customElements.define('spk-input', SpektacularInput);

Для spk-root:

let template = document.createElement('template');
template.innerHTML = `
  <canvas id='spektacular'></canvas>
  <slot></slot>
`;

class SpektacularRoot extends HTMLElement {
  constructor() {
    super();
    let shadowRoot = this.attachShadow({mode: 'open'});
    shadowRoot.appendChild(template.content.cloneNode(true));
  }
  update() {
    let inputs = this.getElementsByTagName('spk-input')
  }
  connectedCallback() {
    this.update();
  }
}
window.customElements.define('spk-root', SpektacularRoot);

Вот часть, которую я не понимаю. Внутри update() метод: console.log(inputs) возвращает HTMLCollection:

console.log(inputs)

// output
HTMLCollection []
  0: spk-input
  1: spk-input
  length: 2
  __proto__: HTMLCollection

Однако HTMLCollection не повторяется с использованием цикла for, потому что он не имеет длины.

console.log(inputs.length)

// output
0

Поиск SO показал, что HTMLCollections похожи на массивы, но не на массивы. Попытка сделать его массивом, используя Array.from(inputs) или оператор распространения, приводит к пустому массиву.

Что здесь происходит? Как я могу перебрать элементы spk-input в spk-root из метода update()?

Я использую gulp-babel и gulp-concat и использую Chrome. Дайте мне знать, если нужно больше информации. Заранее спасибо.


Редактировать : Чтобы уточнить, вызов console.log(inputs.length) с в пределах update() выводит 0 вместо 2.

Ответы [ 2 ]

0 голосов
/ 04 ноября 2018

HTMLCollection inputs действительно имеет свойство длины, и если вы зарегистрируете его в функции обновления, вы увидите, что его значение равно 2. Вы также можете перебирать коллекцию входных данных в цикле for, пока она находится внутри обновления. () функция.

Если вы хотите получить доступ к значениям в цикле вне функции обновления, вы можете сохранить HTMLCollection в переменной, объявленной вне области действия класса SpektacularInput.

Полагаю, существуют другие способы хранения значений в зависимости от того, что вы пытаетесь выполнить, но, надеюсь, это ответит на ваш первоначальный вопрос: «Как я могу перебирать элементы spk-input в spk-root из update () метод?»

class SpektacularInput extends HTMLElement {
  constructor() {
    super();
  }
}
window.customElements.define('spk-input', SpektacularInput);
let template = document.createElement('template');
template.innerHTML = `
  <canvas id='spektacular'></canvas>
  <slot></slot>
`;
// declare outside variable
let inputsObj = {};
class SpektacularRoot extends HTMLElement {
  constructor() {
    super();
    let shadowRoot = this.attachShadow({mode: 'open'});
    shadowRoot.appendChild(template.content.cloneNode(true));
  }
  update() {
    // store on outside variable
    inputsObj = this.getElementsByTagName('spk-input');
    // use in the function
    let inputs = this.getElementsByTagName('spk-input');
    console.log("inside length: " + inputs.length)
    for(let i = 0; i < inputs.length; i++){
      console.log("inside input " + i + ": " + inputs[i]);
    }
  }
  connectedCallback() {
    this.update();
  }
}
window.customElements.define('spk-root', SpektacularRoot);

console.log("outside length: " + inputsObj.length);
for(let i = 0; i < inputsObj.length; i++){
  console.log("outside input " + i + ": " + inputsObj[i]);
}
<spk-root>
  <spk-input></spk-input>
  <spk-input></spk-input>
</spk-root>

Надеюсь, это поможет, Ура!

0 голосов
/ 04 ноября 2018

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

Вот почему let inputs = this.getElementsByTagName('spk-input') в вашем update() методе внешнего <spk-root> не может найти никаких элементов. Не дайте себя одурачить, введя туда неверный вывод console.log.

Я только недавно глубоко погрузился в эту тему и предложил решение с использованием класса HTMLBaseElement:

https://gist.github.com/franktopel/5d760330a936e32644660774ccba58a7

Андреа Джаммарки (автор document-register-element polyfill для пользовательских элементов в неподдерживающих браузерах) воспользовался этим решением и создал из него пакет npm:

https://github.com/WebReflection/html-parsed-element

Пока вам не нужно динамическое создание пользовательских элементов, самое простое и надежное решение - создать сценарий upgrade , поместив сценарии определения вашего элемента в конце body. .

Если вам интересно обсуждение темы (долго читайте!):

https://github.com/w3c/webcomponents/issues/551

Вот полный смысл:

Класс HTMLBaseElement, решающий проблему вызова connectedCallback до разбора потомков

Существует огромная практическая проблема с веб-компонентами spec v1:

В некоторых случаях connectedCallback вызывается, когда дочерние узлы элемента еще не доступны.

Это делает веб-компоненты неработоспособными в тех случаях, когда они полагаются на своих детей при настройке.

См. https://github.com/w3c/webcomponents/issues/551 для справки.

Чтобы решить эту проблему, мы создали в нашей команде класс HTMLBaseElement, который служит новым классом для расширения автономных пользовательских элементов.

HTMLBaseElement, в свою очередь, наследует от HTMLElement (из каких автономных пользовательских элементов должен происходить какой-то момент в их цепочке прототипов).

HTMLBaseElement добавляет две вещи:

  • setup метод, который заботится о правильной синхронизации (то есть гарантирует, что дочерние узлы доступны), а затем вызывает childrenAvailableCallback() на экземпляре компонента.
  • a parsed Логическое свойство, которое по умолчанию равно false и должно быть установлено в true, когда начальная настройка компонентов будет выполнена. Это должно служить в качестве охраны, чтобы убедиться, например, слушатели дочерних событий никогда не подключаются более одного раза.

HTMLBaseElement

class HTMLBaseElement extends HTMLElement {
  constructor(...args) {
    const self = super(...args)
    self.parsed = false // guard to make it easy to do certain stuff only once
    self.parentNodes = []
    return self
  }

  setup() {
    // collect the parentNodes
    let el = this;
    while (el.parentNode) {
      el = el.parentNode
      this.parentNodes.push(el)
    }
    // check if the parser has already passed the end tag of the component
    // in which case this element, or one of its parents, should have a nextSibling
    // if not (no whitespace at all between tags and no nextElementSiblings either)
    // resort to DOMContentLoaded or load having triggered
    if ([this, ...this.parentNodes].some(el=> el.nextSibling) || document.readyState !== 'loading') {
      this.childrenAvailableCallback();
    } else {
      this.mutationObserver = new MutationObserver(() => {
        if ([this, ...this.parentNodes].some(el=> el.nextSibling) || document.readyState !== 'loading') {
          this.childrenAvailableCallback()
          this.mutationObserver.disconnect()
        }
      });

      this.mutationObserver.observe(this, {childList: true});
    }
  }
}

Пример компонента, расширяющего вышеупомянутое:

class MyComponent extends HTMLBaseElement {
  constructor(...args) {
    const self = super(...args)
    return self
  }

  connectedCallback() {
    // when connectedCallback has fired, call super.setup()
    // which will determine when it is safe to call childrenAvailableCallback()
    super.setup()
  }

  childrenAvailableCallback() {
    // this is where you do your setup that relies on child access
    console.log(this.innerHTML)

    // when setup is done, make this information accessible to the element
    this.parsed = true
    // this is useful e.g. to only ever attach event listeners once
    // to child element nodes using this as a guard
  }
}
...