chromium/chrome/browser/resources/ash/settings/os_settings_page/settings_idle_load.ts

// Copyright 2020 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

/**
 * @fileoverview
 * settings-idle-load is a simple variant of dom-if designed for lazy
 * loading and rendering of elements that are accessed imperatively. A URL is
 * given that holds the elements to be loaded lazily.
 */

import {assert} from 'chrome://resources/js/assert.js';
import {PolymerElement, TemplateInstanceBase, templatize} from 'chrome://resources/polymer/v3_0/polymer/polymer_bundled.min.js';

import {ensureLazyLoaded} from '../ensure_lazy_loaded.js';

import {getTemplate} from './settings_idle_load.html.js';

export class SettingsIdleLoadElement extends PolymerElement {
  static get is() {
    return 'settings-idle-load';
  }

  static get template() {
    return getTemplate();
  }

  static get properties() {
    return {};
  }

  private child_: Element|null;
  private instance_: TemplateInstanceBase|null;
  private loading_: Promise<Element>|null;
  private idleCallback_: number;

  constructor() {
    super();

    this.child_ = null;
    this.instance_ = null;
    this.loading_ = null;
    this.idleCallback_ = 0;
  }


  override connectedCallback(): void {
    super.connectedCallback();

    this.idleCallback_ = requestIdleCallback(() => {
      this.get();
    });
  }

  override disconnectedCallback(): void {
    super.disconnectedCallback();

    // No-op if callback already fired.
    window.cancelIdleCallback(this.idleCallback_);
  }

  /**
   * @param requestFn Requests the lazy module.
   * @return Resolves with the stamped child element after the lazy module has
   *    been loaded.
   */
  private requestLazyModule_(): Promise<Element> {
    return new Promise((resolve, reject) => {
      ensureLazyLoaded().then(() => {
        const slot = this.shadowRoot!.querySelector('slot');
        assert(slot);
        const template =
            slot.assignedNodes({flatten: true})
                .filter(n => n.nodeType === Node.ELEMENT_NODE)[0] as
            HTMLTemplateElement;

        const TemplateClass = templatize(template, this, {
          mutableData: false,
          forwardHostProp: this._forwardHostPropV2,
        });

        this.instance_ = new TemplateClass();

        assert(!this.child_);
        this.child_ = this.instance_.root.firstElementChild;
        assert(this.child_);

        this.parentNode!.insertBefore(this.instance_.root, this);
        resolve(this.child_);

        const event =
            new CustomEvent('lazy-loaded', {bubbles: true, composed: true});
        this.dispatchEvent(event);
      }, reject);
    });
  }

  /**
   * @return Child element which has been stamped into the DOM tree.
   */
  override get(): Promise<Element> {
    if (this.loading_) {
      return this.loading_;
    }

    this.loading_ = this.requestLazyModule_();
    return this.loading_;
  }

  /* eslint-disable-next-line @typescript-eslint/naming-convention */
  private _forwardHostPropV2(prop: string, value: any): void {
    if (this.instance_) {
      this.instance_.forwardHostProp(prop, value);
    }
  }
}

declare global {
  interface HTMLElementTagNameMap {
    'settings-idle-load': SettingsIdleLoadElement;
  }
}

customElements.define(SettingsIdleLoadElement.is, SettingsIdleLoadElement);