无法设置自定义内置元素的 textContent,超时除外

Can't set textContent of a customised built-in element except in timeout

我正在制作一个自动本地化其视觉文本表示的自定义元素:

class LocalDate extends HTMLTimeElement {
  // Specify observed attributes so that
  // attributeChangedCallback will work
  static get observedAttributes() {
    return ["datetime"];
  }

  constructor() {
    // Always call super first in constructor
    const self = super();

    this.formatter = new Intl.DateTimeFormat(navigator.languages, {
      year: "numeric",
      month: "short",
      day: "numeric"
    });

    return self;
  }

  attributeChangedCallback(name, oldValue, newValue) {
    if (name === "datetime") {
      this.textContent = "";
      const dateMiliseconds = Date.parse(newValue);
      if (!Number.isNaN(dateMiliseconds)) {
        const dateString = this.formatter.format(new Date(dateMiliseconds));
        this.textContent = dateString;
      }
    }
  }
}

customElements.define('local-date', LocalDate, {
  extends: "time"
});
<time is="local-date" datetime="2022-01-13T07:13:00+10:00">13 Jan 2022 - Still here</time>

问题在于脚本标记恰好是 运行 - 如果在解析正文后它是 运行,那么它会按预期工作。否则,元素不会显示为日期,而是显示日期字符串 除了 元素中已有的文本。

JsFiddle 和 Whosebug 都把 script 标签放在 body 的底部,所以错误只能用一个 DataUrl 才能看到:

数据:text/html;charset=utf-8,%3C%21DOCTYPE%20html%3E%0D%0A%3Chtml%3E%0D%0A%3Chead%3E%0D%0A%3Cmeta% 20charset%3D%22utf-8%22%2F%3E%0D%0A%3Ctitle%3ETime%20since%3C%2Ftitle%3E%0D%0A%3Cscript%3E%0D%0A%09class%20LocalDate%20extends%20HTMLTimeElement% 20%7B%0D%0A%09%09%2F%2F%20Specify%20observed%20attributes%20so%20that%0D%0A%09%09%2F%2F%20attributeChangedCallback%20will%20work%0D%0A%09% 09static%20get%20observedAttributes%28%29%20%7B%0D%0A%09%09%09return%20%5B%22datetime%22%5D%3B%0D%0A%09%09%7D%09%0D% 0A%0D%0A%09%09构造函数%28%29%20%7B%0D%0A%09%09%09%2F%2F%20Always%20call%20super%20first%20in%20构造函数%0D%0A%09% 09%09const%20self%20%3D%20super%28%29%3B%0D%0A%0D%0A%09%09%09this.formatter%20%3D%20new%20Intl.DateTimeFormat%28navigator.languages%2C%20%7B%20year%3A%20%22numeric%22%2C%20month%3A%20%22short%22%2C%20day%3A%20%22numeric%22%20%7D%29% 3B%0D%0A%0D%0A%09%09%09return%20self%3B%0D%0A%09%09%7D%0D%0A%0D%0A%09%09attributeChangedCallback%28name%2C%20oldValue%2C% 20newValue%29%20%7B%0D%0A%09%09%09if%20%28name%20%3D%3D%3D%20%22date时间%22%29%20%7B%0D%0A%09%09%09%09this.textContent%20%3D%20%22%22%3B%0D%0A%09%09%09%09const% 20dateMiliseconds%20%3D%20Date.parse%28newValue%29%3B%0D%0A%09%09%09%09if%20%28%21Number.isNaN%28dateMiliseconds%29%29%20%7B% 0D%0A%09%09%09%09%09const%20dateString%20%3D%20this.formatter.format%28new%20Date%28dateMiliseconds%29%29%3B%0D%0A%09%09%09%09% 09%2F%2F%20Bizarrly%2C%20this%20doesn%27t%20seem%20to%20work%20without%20doing%20this%20in%20a%20timeout%3F%21%3F%21%0D%0A%09%09% 09%09%09this.textContent%20%3D%20dateString%3B%0D%0A%09%09%09%09%7D%0D%0A%09%09%09%7D%0D%0A%09% 09%7D%0D%0A%09%7D%0D%0A%09%0D%0A%09customElements.define%28%27local-date%27%2C%20LocalDate%2C%20%7B%20extends%3A% 20%22time%22%20%7D%29%3B%0D%0A%3C%2Fscript%3E%0D%0A%3C%2Fhead%3E%0D%0A%3Cbody%3E%0D%0A%3Cp%3ELast% 20updated%20%3Ctime%20is%3D%22local-date%22%20datetime%3D%222022-01-13T07%3A13%3A00%2B10%3A00%22%3E13%20Jan%202022%20-%20Still%20here%3C %2Ftime%3E%3C%2Fp%3E%0D%0A%3C%2Fbody%3E

我已经在 Firefox 和 Chrome 中复制了这个 - 知道这里发生了什么吗?

您的问题出现了,
因为 attributeChangedCallbackconnectedCallback 都在 开始标记
上触发 (并按此顺序!)

所以

  • 当自定义元素被 定义 BEFORE 用于 DOM,
  • attributeChangedCallbackopening 标签上触发,
  • 添加你的.textContent
  • after 您的自定义元素中的 lightDOM parsed
  • 默认情况下添加到现有内容

这就是为什么您会在下面的示例中看到 lightDOM #2

<style>
  time {
    display: block
  }
</style>

<time id=BEFORE is="local-date" datetime="2022-01-13T07:13:00+10:00"> lightDOM #1
  <script>console.log("time element BEFORE parsed")</script>
</time>

<script>
  customElements.define('local-date', class extends HTMLTimeElement {
    static get observedAttributes() {
      return ["datetime"];
    }
    attributeChangedCallback(name, oldValue, newValue) {
      console.warn("attributeChangedCallback", this.id, this.isConnected);
      this.textContent = (new Intl.DateTimeFormat(navigator.languages, {
        year: "numeric",
        month: "short",
        day: "numeric"
      })).format(new Date(Date.parse(newValue)));
    }
  }, {
    extends: "time"
  });
  console.warn("Custom Element: local-date defined");
</script>

<time id=AFTER is="local-date" datetime="2022-01-13T07:13:00+10:00"> lightDOM #2
  <script>console.log("time element AFTER parsed")</script>
</time>

不要在attributeChangedCallback

中进行初始化
  • 所以我们必须确保只在 解析所有 DOM 之后才渲染时间,使用 setTimeout
    另见:

  • and..(但在解决方案中没有任何作用)因为 Apple 自 2016 年以来就表示他们永远不会实施 Customized Built-In Elements,使它是一个 自治 元素 <local-date>

  • 您还可以再添加 N 千字节并使用 the 58 Tools for building Web Components 中的任何一个,有些会保护您免受这种低级行为的影响。 但是拥有工具的傻瓜仍然是傻瓜。

  • 回调执行时的良好参考:https://andyogo.github.io/custom-element-reactions-diagram/

  • 但是 attributeChangedCallbackconnectedCallback!
    之前 运行 请注意如何使用 this.isConnected

<style>
  local-date {
    display: block
  }
</style>

<local-date id=BEFORE datetime="2022-01-13T07:13:00+10:00"> lightDOM #1
  <script>console.log("time element BEFORE parsed")</script>
</local-date>

<script>
  customElements.define('local-date', class extends HTMLElement {
    static get observedAttributes() {
      return ["datetime"];
    }
    attributeChangedCallback(name, oldValue, newValue) {
      console.warn("attributeChangedCallback", this.id, this.isConnected);
      if (oldValue) this.renderTime(newValue);
    }
    connectedCallback(){
      console.warn("connectedCallback", this.id);
      setTimeout(()=>this.renderTime());
    }
    renderTime(dt=this.getAttribute("datetime")){
      console.warn("renderTime", this.id);
      this.textContent = (new Intl.DateTimeFormat(navigator.languages, {
        year: "numeric",
        month: "short",
        day: "numeric"
      })).format(new Date(Date.parse(dt)));
    }
  });
  console.warn("Custom Element: local-date defined");
</script>

<local-date id=AFTER datetime="2022-01-13T07:13:00+10:00"> lightDOM #2
  <script>console.log("time element AFTER parsed")</script>
</local-date>