Search code examples
javascriptarraystypescriptoopreact-tsx

Type 'HTMLCollection | undefined' must have a '[Symbol.iterator]()' method that returns an iterator


I don't know what I am missing out here. I am following a tutorial in building a custom date-picker using a web component here https://www.youtube.com/watch?v=g1Zd0Y7OJuI&t=723s and am translating the JavaScript logic to Typescript. I have reached a line inside connectedCallback() {...} that throws the following error: Type 'HTMLCollection | undefined' must have a '[Symbol.iterator]()' method that returns an iterator on the line const [prevBtn, calendarDateElement, nextBtn] = this.calendarDropdown?.querySelector(".calendar-header")?.children; I have looked at similar questions here on Stackoverflow but found that their suggested solutions are not working for me. Kindly assist me in understanding what I need to take into account to eliminate this horrific error!

class DatePicker extends HTMLElement {
  shadow: ShadowRoot;
  calendar: Calendar;
  mounted: boolean = false;

  /** Elements */
  calendarDropdown: Element | null = null;
  calendarDateElement: HTMLHeadingElement | null | undefined = null;

  constructor() {
    super();
    ...
}

  connectedCallback() {
    this.mounted = true;

    this.toggleButton = this.shadow.querySelector(".date-toggle");
    this.calendarDropdown = this.shadow.querySelector(".calendar-dropdown");
    const [prevBtn, calendarDateElement, nextBtn] = this.calendarDropdown?.querySelector(".calendar-header")?.children; // <--- This is the line complain 
    this.calendarDateElement = calendarDateElement;

    this.toggleButton?.addEventListener("click", () => this.toggleCalendar());
    prevBtn.addEventListener("click", () => this.prevMonth());
    nextBtn.addEventListener("click", () => this.nextMonth());
  }
}

Solution

  • I looked up this resource https://www.javascripttutorial.net/javascript-dom/javascript-get-child-element/ and found a workaround for my problem but I would like an even better solution to this issue. Instead of destructuring child nodes of the parent node like this const [prevBtn, calendarDateElement, nextBtn] = this.calendarDropdown?.querySelector(".calendar-header")?.children; I found each node and assigned it to a variable separately as in the following code:

    connectedCallback() {
    ...
       this.calendarDropdown = this.shadow.querySelector(".calendar-dropdown");
       const prevBtn = this.shadow.querySelector(".previous-month");
       const calendarDateElement =
         this.shadow.querySelector(".previous-month")?.nextElementSibling;
       const nextBtn = this.shadow.querySelector(".next-month");
       this.calendarDateElement = calendarDateElement;
    
       ...
       prevBtn?.addEventListener("click", () => this.prevMonth());
       nextBtn?.addEventListener("click", () => this.nextMonth());
    }
    

    Here below is the entire class:

    import { Calendar, Day } from ".";
    
    class DatePicker extends HTMLElement {
      date: any = null;
      format = "MMM DD, YYYY";
      position: string | null = "bottom";
      visible: boolean | undefined = false;
      shadow: ShadowRoot;
      calendar: Calendar;
      mounted: boolean = false;
    
      /** Elements */
      toggleButton: HTMLButtonElement | null = null;
      calendarDropdown: Element | null = null;
      calendarDateElement: ChildNode | null | undefined = null;
    
      constructor() {
        super();
    
        const lang = window.navigator.language;
        const date = new Date(
          this.date ?? (this.getAttribute("date") || Date.now())
        );
    
        this.shadow = this.attachShadow({ mode: "open" });
        this.date = new Day(date);
        this.calendar = new Calendar(this.date.year, this.date.monthNumber, lang);
    
        this.format = this.getAttribute("format") || this.format;
        this.position = DatePicker.position.includes(
          this.getAttribute("position") as string
        )
          ? this.getAttribute("position")
          : this.position;
    
        this.visible =
          this.getAttribute("visible") === "" ||
          this.getAttribute("visible") === "true" ||
          this.visible;
    
        this.render();
      }
    
      connectedCallback() {
        this.mounted = true;
    
        this.toggleButton = this.shadow.querySelector(".date-toggle");
        this.calendarDropdown = this.shadow.querySelector(".calendar-dropdown");
        const prevBtn = this.shadow.querySelector(".previous-month");
        const calendarDateElement =
          this.shadow.querySelector(".previous-month")?.nextElementSibling;
        const nextBtn = this.shadow.querySelector(".next-month");
        this.calendarDateElement = calendarDateElement;
    
        this.toggleButton?.addEventListener("click", () => this.toggleCalendar());
        prevBtn?.addEventListener("click", () => this.prevMonth());
        nextBtn?.addEventListener("click", () => this.nextMonth());
      }
    
      // = this.calendarDropdown.sec
      //this.calendarDropdown?.querySelector(".calendar-header")?.children;
    
      prevMonth() {
        console.log("Prev Clicked");
        this.calendar.goToPreviousMonth();
        this.updateCalendarHeaderText();
      }
      nextMonth() {
        console.log("Next Clicked");
        this.calendar.goToNextMonth();
        this.updateCalendarHeaderText();
      }
    
      updateCalendarHeaderText() {
        if (this.calendarDateElement)
          this.calendarDateElement.textContent = `${this.calendar.month.name}, ${this.calendar.year}`;
      }
    
      toggleCalendar(visible: boolean | null = null) {
        visible === null
          ? this.calendarDropdown?.classList.toggle("visible")
          : visible
          ? this.calendarDropdown?.classList.add("visible")
          : this.calendarDropdown?.classList.remove("visible");
    
        this.visible = this.calendarDropdown?.className.includes("visible");
      }
    
      static get position() {
        return ["top", "right", "bottom", "left"];
      }
    
      get styles() {
        return `
          :host {
            position: relative;
          }
    
          .date-toggle {
            background: #eee;
            border: none;
            border-radius: 0.5em;
            color: var(--teal);
            cursor: pointer;
            font-size: medium;
            font-weight: lighter;
            margin: 1em 0;
            padding: 1.1em;
            text-transform: capitalize;
            width: 100%;
          }
    
          .calendar-dropdown {
            background: #008080;
            border-radius: 0.5em;
            box-shadow: 0 0 8px rgba(0, 0, 0, 0.2);
            color: var(--white);
            display: block;
            // height: 300px;
            left: 50%;
            min-width: 200px;
            padding: 20px;
            position: absolute;
            top: 100%;
            transform: translate(-52%, 15px);
            width: 95%;
            z-index: 3;
          }
    
          .calendar-dropdown.visible {
            display: block;
          }
    
          .calendar-header {
            display: flex;
            justify-content: space-between;
            align-items: center;
            margin: 10px 0 30px;
          }
    
          .calendar-header h4 {
            margin: 0;
            font-size: 21px;
            font-weight: lighter;
            text-transform: capitalize;
          }
    
          .calendar-header button {
            background: none;
            border: 8px solid transparent;
            border-radius: 0.1em;
            border-top-color: var(--white);
            cursor: pointer;
            height: 0;
            padding: 0;
            position: relative;
            transform: rotate(90deg);
            width: 0;
          }
    
          .calendar-header button::after {
            content: '';
            display: block;
            height: 25px;
            left: 50%;
            position: absolute;
            top: 50%;
            transform: translate(-50%, -50%);
            width: 25px;
          }
    
          .calendar-header button:last-of-type {
            transform: rotate(-90deg);
          }
        `;
      }
    
      render() {
        const monthYear = `${this.calendar.month.name}, ${this.calendar.year}`;
        const date = this.date.format(this.format);
        this.shadow.innerHTML = `
        <style>${this.styles}</style>
        <button type="button" class="date-toggle">${date}</button>
        <div class="calendar-dropdown ${this.visible ? "visible" : ""} 
        ${this.position}">
          <div class="calendar-header">
            <button aria-label="previous month" class="previous-month" type="button"></button>
            <h4 class="month-year">
                ${monthYear}
              </h4>
            <button aria-label="next month" class="next-month" type="button"></button>
          </div>
        </div>
        `;
      }
    }
    
    export default DatePicker;
    
    window.customElements.get("date-picker") ||
      window.customElements.define("date-picker", DatePicker);