交互式 Web 组件比你想象的更容易

Avatar of John Rhea
John Rhea

DigitalOcean 提供适合您旅程每个阶段的云产品。立即开始使用 $200 免费积分!

在我的上一篇文章中,我们发现 Web 组件并不像看起来那么可怕。我们看一个超级简单的设置,并制作了一个僵尸约会服务配置文件,其中包含一个自定义的 <zombie-profile> 元素。我们对每个配置文件重用该元素,并使用 <slot> 元素用唯一信息填充每个元素。

这是它的所有内容。

这很酷而且很有趣(好吧,反正玩得很开心…),但如果我们通过使其具有交互性来更进一步呢?我们的僵尸资料很棒,但为了让这成为一种有用的、后启示录式的约会体验,你会想,你知道,“喜欢”一个僵尸,甚至给他们发消息。这就是我们将在本文中要做的。我们将把滑动留给另一篇文章。(对僵尸来说,向左滑动是正确的事吗?)

文章系列

本文假设您对 Web 组件有一定程度的了解。如果您是新手,完全没问题——上一篇文章 将为您提供所需的一切。继续。阅读它。我会等你的。*扭动拇指*准备好了吗?好的。

首先,更新原始版本

让我们暂停一秒钟(好吧,也许更长)并看一下 ::slotted() 伪元素。它是在上一篇文章发布后被我注意到的(谢谢,罗斯!),它解决了我在封装方面遇到的一些(虽然不是全部)问题。如果你还记得,我们在组件的 <template> 之外有一些 CSS 样式,在 <template> 内的 <style> 元素中有一些样式。<template> 中的样式是封装的,但外部的样式则不是。

但这就是 ::slotted 发挥作用的地方。我们在选择器中声明一个元素,如下所示

::slotted(img) {
  width: 100%;
  max-width: 300px;
  height: auto;
  margin: 0 1em 0 0;
}

现在,放置在任何插槽中的任何 <img> 元素都将被选中。这帮助很大!

但这并不能解决我们所有的封装问题。虽然我们可以直接选择插槽中的任何内容,但我们无法选择插槽中元素的任何后代。因此,如果我们有一个带有子元素的插槽——例如僵尸配置文件的兴趣部分——我们无法从 <style> 元素中选择它们。此外,虽然 ::slotted 具有 出色的浏览器支持,但某些事情(例如选择伪元素,例如 ::slotted(span)::after)将在某些浏览器(您好,Chrome)中运行,但在其他浏览器(您好,Safari)中将无法运行。

因此,虽然它并不完美,但 ::slotted 确实提供了比我们之前更多的封装。以下是更新以反映这一点的约会服务

回到交互式 Web 组件!

首先我想做的是添加一点动画来增添趣味。让我们让我们的僵尸配置文件图片在加载时淡入并向上平移。

当我第一次尝试这样做时,我使用 img::slotted(img) 选择器来直接动画化图像。但我只得到了 Safari 支持。Chrome 和 Firefox 不会在插槽图像上运行动画,但默认图像动画运行良好。为了使它正常工作,我在一个带有 .pic 类的 div 中包装了插槽,并将动画应用于该 div。

.pic {
  animation: picfadein 1s 1s ease-in forwards;
  transform: translateY(20px);
  opacity: 0;
}

@keyframes picfadein {
  from { opacity: 0; transform: translateY(20px); }
  to { opacity: 1; transform: translateY(0); }
}

“喜欢”僵尸

对那个可爱的僵尸说“喜欢”不是很好吗?我是说从用户的角度来看,当然。这似乎是至少在线约会服务应该具备的功能。

我们将添加一个复选框“按钮”,它在点击时会启动一个心脏动画。让我们将这段 HTML 添加到 .info div 的顶部

<input type="checkbox" id="trigger"><label class="likebtn" for="trigger">Like</label>

这是一个我整理好的心脏 SVG。我们知道僵尸喜欢可怕的东西,所以他们的心脏将是令人眼花缭乱的黄绿色

<svg viewBox="0 0 160 135" class="heart" xmlns="http://www.w3.org/2000/svg" fill-rule="evenodd" clip-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="2"><path d="M61 12V0H25v12H12v13H0v36h12v13h13v12h12v12h12v12h12v13h13v12h12v-12h13v-13h11V98h13V86h-1 13V74h12V61h12V25h-12V12h-12V0H98v12H85v13H74V12H61z" fill="#7aff00"/></svg>

以下是添加到模板的 <style> 元素中的重要 CSS 部分

#trigger:checked + .likebtn {
  /* Checked state of the .likebtn. Flips the foreground/background color of the unchecked state. */
  background-color: #960B0B;
  color: #fff;
}

#trigger {
  /* With the label attached to the input with the for attribute, clicking the label checks/unchecks the box, so we can remove the checkbox. */
  display: none;
}

.heart {
  /* Start the heart off so small it's nigh invisible */
  transform: scale(0.0001);
}

@keyframes heartanim {
  /* Heart animation */
  0% { transform: scale(0.0001); }
  50% { transform: scale(1); }
  85%, 100% { transform: scale(0.4); }
}

#trigger:checked ~ .heart {
  /* Checking the checkbox initiates the animation */
  animation: 1s heartanim ease-in-out forwards;
}

那里几乎是标准的 HTML 和 CSS。没有什么花哨的或严格的 Web 组件风格。但是,嘿,它有效!而且由于它技术上是一个复选框,所以“不喜欢”一个僵尸和“喜欢”一个僵尸一样容易。

给僵尸发消息

如果你是一个准备与人交往的末世单身人士,并且看到一个性格和兴趣与你相符的僵尸,你可能想给他们发消息。(请记住,僵尸并不关心外表——他们只对你的大脑感兴趣。)

让我们在“喜欢”一个僵尸后显示一个消息按钮。喜欢按钮是一个复选框这一事实再次派上用场,因为我们可以使用它的选中状态来有条件地显示消息选项,使用 CSS。这是添加到心脏 SVG 正下方的 HTML。只要它是 #trigger 元素的同级元素并且位于其后面,它几乎可以放置在任何位置。

<button type="button" class="messagebtn">Message</button>

一旦 #trigger 复选框被选中,我们就可以将消息按钮显示出来

#trigger:checked ~ .messagebtn {
  display: block;
}

到目前为止,我们做得很好,避免了复杂性,但我们需要在这里使用一点 JavaScript。如果我们点击消息按钮,我们希望能够给那个僵尸发消息,对吧?虽然我们可以将该 HTML 添加到我们的 <template> 中,但为了演示目的,让我们使用一些 JavaScript 动态创建它。

我最初(天真)的假设是我们只需将 <script> 元素添加到模板中,创建一个封装的脚本,然后继续我们的快乐旅程。是的,这行不通。在模板中实例化的任何变量都会被多次实例化,而且,JavaScript 对彼此无法区分的变量很反感。*对恼人的 JavaScript 挥舞拳头*

你可能会更聪明地说:“嘿,我们已经为这个元素创建了一个 JavaScript 构造函数,那么为什么不把 JavaScript 放进去呢?”好吧,你说得对,你比我聪明。

让我们这样做,并将 JavaScript 添加到构造函数中。我们将添加一个监听器,一旦点击,就会创建一个表单并显示它,用于发送消息。以下是现在的样子,构造函数,聪明人

customElements.define('zombie-profile',
class extends HTMLElement {
  constructor() {
    super();
    let profile = document.getElementById('zprofiletemplate');
    let myprofile = profile.content;
    const shadowRoot = this.attachShadow({
      mode: 'open'
    }).appendChild(myprofile.cloneNode(true));

    // The "new" code
    // Grabbing the message button and the div wrapping the profile for later use
    let msgbtn = this.shadowRoot.querySelector('.messagebtn'),
        profileEl = this.shadowRoot.querySelector('.profile-wrapper');
    
    // Adding the event listener
    msgbtn.addEventListener('click', function (e) {

      // Creating all the elements we'll need to build our form
      let formEl = document.createElement('form'),
          subjectEl = document.createElement('input'),
          subjectlabel = document.createElement('label'),
          contentEl = document.createElement('textarea'),
          contentlabel = document.createElement('label'),
          submitEl = document.createElement('input'),
          closebtn = document.createElement('button');
        
      // Setting up the form element. The action just goes to a page I built that spits what you submitted back at you
      formEl.setAttribute('method', 'post');
      formEl.setAttribute('action', 'https://johnrhea.com/undead-form-practice.php');
      formEl.classList.add('hello');

      // Setting up a close button so we can close the message if we get shy
      closebtn.innerHTML = "x";
      closebtn.addEventListener('click', function () {
        formEl.remove();
      });

      // Setting up form fields and labels
      subjectEl.setAttribute('type', 'text');
      subjectEl.setAttribute('name', 'subj');
      subjectlabel.setAttribute('for', 'subj');
      subjectlabel.innerHTML = "Subject:";
      contentEl.setAttribute('name', 'cntnt');
      contentlabel.setAttribute('for', 'cntnt');
      contentlabel.innerHTML = "Message:";
      submitEl.setAttribute('type', 'submit');
      submitEl.setAttribute('value', 'Send Message');

      // Putting all the elments in the Form
      formEl.appendChild(closebtn);
      formEl.appendChild(subjectlabel);
      formEl.appendChild(subjectEl);
      formEl.appendChild(contentlabel);
      formEl.appendChild(contentEl);
      formEl.appendChild(submitEl);

      // Putting the form on the page
      profileEl.appendChild(formEl);
    });
  }
});

到目前为止,一切都好!

在我们结束之前,还有一件事需要解决。没有什么比第一次尴尬的介绍更糟糕的了,所以让我们为这些末世约会提供便利,将僵尸的名字添加到默认消息文本中。这对用户来说是一个很好的便利。

由于我们知道 <zombie-profile> 元素中的第一个 span 始终是僵尸的名字,我们可以获取它并将它的内容放到一个变量中。(如果你的实现不同,并且元素的顺序发生变化,你可能想使用一个类来确保你始终获得正确的那个。)

let zname = this.getElementsByTagName("span")[0].innerHTML;

然后在事件监听器中添加这个

contentEl.innerHTML = "Hi " + zname + ",\nI like your braaains...";

这并不难,是吗?现在我们知道交互式 Web 组件与僵尸约会场景一样不吓人……好吧,你知道我的意思。一旦你克服了理解 Web 组件结构的最初障碍,它就开始变得更有意义了。现在你已经掌握了交互式 Web 组件技能,让我们看看你能想出什么!还有什么其他类型的组件或交互可以使我们的僵尸约会服务更好?把它做出来,并在评论中分享。