สร้างคอมโพเนนต์เรื่องราวที่มีองค์ประกอบที่สว่าง

1. บทนำ

เรื่องราวเป็นองค์ประกอบ UI ที่ได้รับความนิยมในปัจจุบัน แอปโซเชียลและแอปข่าวจะรวมแอปดังกล่าวลงในฟีด ใน Codelab นี้ เราจะสร้างคอมโพเนนต์เรื่องราวด้วย lit-element และ TypeScript

นี่คือลักษณะของคอมโพเนนต์เรื่องราวในตอนท้าย

คอมโพเนนต์ของโปรแกรมดูเรื่องราวที่สมบูรณ์ซึ่งแสดงรูปภาพกาแฟ 3 ภาพ

เราอาจมอง "เรื่องราว" ของโซเชียลมีเดียหรือข่าวเป็นคอลเล็กชันการ์ดที่จะแสดงตามลำดับ คล้ายกับสไลด์โชว์ จริงๆ แล้วเรื่องราวคือสไลด์โชว์ การ์ดมักเป็นรูปภาพหรือวิดีโอที่เล่นอัตโนมัติ และใส่ข้อความเพิ่มเติมด้านบนได้ นี่คือสิ่งที่เราจะสร้าง

รายการฟีเจอร์

  • การ์ดที่มีพื้นหลังเป็นภาพหรือวิดีโอ
  • ปัดไปทางซ้ายหรือขวาเพื่อไปยังส่วนต่างๆ ของเรื่องราว
  • วิดีโอที่เล่นอัตโนมัติ
  • ความสามารถในการเพิ่มข้อความหรือปรับแต่งการ์ด

สำหรับประสบการณ์การใช้งานคอมโพเนนต์นี้ของนักพัฒนาซอฟต์แวร์ คุณควรระบุการ์ดเรื่องราวในมาร์กอัป HTML ธรรมดา เช่น

<story-viewer>
  <story-card>
    <img slot="media" src="some/image.jpg" />
    <h1>Title</h1>
  </story-card>
  <story-card>
    <video slot="media" src="some/video.mp4" loop playsinline></video>
    <h1>Whatever</h1>
    <p>I want!</p>
  </story-card>
</story-viewer>

มาเพิ่มฟีเจอร์ดังกล่าวลงในรายการฟีเจอร์กัน

รายการฟีเจอร์

  • ยอมรับชุดการ์ดในมาร์กอัป HTML

วิธีนี้จะช่วยให้ทุกคนใช้คอมโพเนนต์เรื่องราวของเราได้ง่ายๆ ด้วยการเขียน HTML ซึ่งเหมาะสำหรับทั้งโปรแกรมเมอร์และผู้ที่ไม่เชี่ยวชาญด้านโปรแกรม และใช้งานได้ทุกที่ที่ HTML ใช้งานได้ เช่น ระบบจัดการเนื้อหา เฟรมเวิร์ก ฯลฯ

ข้อกำหนดเบื้องต้น

  • Shell ที่คุณเรียกใช้ git และ npm ได้
  • เครื่องมือแก้ไขข้อความ

2. การตั้งค่า

เริ่มต้นด้วยการโคลนที่เก็บ story-viewer-starter

git clone git@github.com:PolymerLabs/story-viewer-starter.git

มีการตั้งค่าสภาพแวดล้อมด้วย lit-element และ TypeScript อยู่แล้ว เพียงติดตั้งการอ้างอิงต่อไปนี้

npm i

สําหรับผู้ใช้ VS Code ให้ติดตั้งส่วนขยาย lit-plugin เพื่อรับการเติมข้อความอัตโนมัติ การตรวจสอบประเภท และการแก้ไขเทมเพลต lit-html

เริ่มต้นสภาพแวดล้อมในการพัฒนาซอฟต์แวร์โดยการเรียกใช้:

npm run dev

คุณพร้อมเริ่มเขียนโค้ดแล้ว

3. คอมโพเนนต์ <story-card>

เมื่อสร้างคอมโพเนนต์แบบผสม บางครั้งการเริ่มต้นด้วยคอมโพเนนต์ย่อยที่ง่ายกว่าแล้วค่อยๆ เพิ่มองค์ประกอบอื่นๆ เข้าไปจะง่ายกว่า เรามาเริ่มสร้าง <story-card> กัน โดยควรแสดงวิดีโอหรือรูปภาพแบบเต็มหน้าจอได้ ผู้ใช้ควรสามารถปรับแต่งเพิ่มเติมได้ เช่น ด้วยข้อความซ้อนทับ

ขั้นตอนแรกคือกำหนดคลาสของคอมโพเนนต์ซึ่งขยายจาก LitElement ตัวแตกแต่ง customElement จะจัดการลงทะเบียนองค์ประกอบที่กําหนดเองให้เรา ตอนนี้เป็นโอกาสที่ดีในการตรวจสอบว่าได้เปิดใช้เครื่องมือตกแต่งใน tsconfig ด้วยแฟล็ก experimentalDecorators (หากคุณใช้ที่เก็บเริ่มต้น ระบบจะเปิดใช้ฟีเจอร์นี้อยู่แล้ว)

ใส่โค้ดต่อไปนี้ลงใน Story-card.ts:

import { LitElement } from 'lit';
import { customElement } from 'lit/decorators.js';

@customElement('story-card')
export class StoryCard extends LitElement {
}

ตอนนี้ <story-card> เป็นองค์ประกอบที่กำหนดเองที่ใช้ได้แล้ว แต่ยังไม่ได้แสดงอะไรเลย หากต้องการกำหนดโครงสร้างภายในขององค์ประกอบ ให้กำหนดเมธอดอินสแตนซ์ render นี่คือที่ที่เราจะให้เทมเพลตสำหรับองค์ประกอบ โดยใช้แท็ก html ของ lit-html

เทมเพลตของคอมโพเนนต์ควรใส่อะไร ผู้ใช้ควรระบุได้ 2 สิ่ง คือ องค์ประกอบสื่อ และการวางซ้อน ดังนั้นเราจะเพิ่ม <slot> 1 รายการสำหรับแต่ละเหตุการณ์

ช่องคือวิธีที่เราระบุว่าควรแสดงผลองค์ประกอบย่อยขององค์ประกอบที่กำหนดเอง หากต้องการข้อมูลเพิ่มเติม โปรดดูคำแนะนำดีๆ เกี่ยวกับการใช้สล็อต

import { html } from 'lit';

export class StoryCard extends LitElement {
  render() {
    return html`
      <div id="media">
        <slot name="media"></slot>
      </div>
      <div id="content">
        <slot></slot>
      </div>
    `;
  }
}

การแยกองค์ประกอบสื่อออกเป็นช่องของตัวเองจะช่วยให้เรากําหนดเป้าหมายองค์ประกอบนั้นสําหรับสิ่งต่างๆ เช่น การเพิ่มสไตล์แบบเต็มหน้าและวิดีโอที่เล่นอัตโนมัติ ใส่ช่องที่ 2 (ช่องสำหรับการวางซ้อนที่กำหนดเอง) ลงในเอลิเมนต์คอนเทนเนอร์เพื่อให้เราเพิ่มระยะห่างจากขอบเริ่มต้นบางส่วนได้ในภายหลัง

ตอนนี้คุณใช้คอมโพเนนต์ <story-card> แบบนี้ได้แล้ว

<story-card>
  <img slot="media" src="some/image.jpg" />
  <h1>My Title</h1>
  <p>my description</p>
</story-card>

แต่ดูแย่มาก

นักเล่าเรื่องที่ไร้สไตล์ที่แสดงภาพกาแฟ

การเพิ่มสไตล์

มาเพิ่มสไตล์กัน ซึ่งก็ใช้องค์ประกอบที่มีแสงน้อยได้โดยกำหนดพร็อพเพอร์ตี้ styles แบบคงที่และส่งสตริงเทมเพลตที่แท็ก css กลับมา CSS ที่เขียนไว้ที่นี่จะมีผลกับองค์ประกอบที่กำหนดเองของเราเท่านั้น CSS ที่มี Shadow DOM เหมาะสําหรับกรณีนี้

มาจัดรูปแบบองค์ประกอบสื่อแบบสล็อตแมชชีนให้ครอบคลุม <story-card> กัน ไหนๆ ก็มาถึงจุดนี้ เราสามารถให้การจัดรูปแบบที่ดีสำหรับองค์ประกอบในช่องที่ 2 ได้ วิธีนี้จะช่วยให้ผู้ใช้คอมโพเนนต์สามารถข้าม <h1>, <p> หรืออะไรก็ได้ และจะเห็นข้อความที่น่าสนใจโดยค่าเริ่มต้น

import { css } from 'lit';

export class StoryCard extends LitElement {
  static styles = css`
    #media {
      height: 100%;
    }
    #media ::slotted(*) {
      width: 100%;
      height: 100%;
      object-fit: cover;
    }

    /* Default styles for content */
    #content {
      position: absolute;
      top: 0;
      right: 0;
      bottom: 0;
      left: 0;
      padding: 48px;
      font-family: sans-serif;
      color: white;
      font-size: 24px;
    }
    #content > slot::slotted(*) {
      margin: 0;
    }
  `;
}

นักเล่าเรื่องที่มีสไตล์แสดงภาพกาแฟ

ตอนนี้เรามีการ์ดเรื่องราวที่มีสื่อพื้นหลัง และเราสามารถวางอะไรก็ได้ที่ต้องการไว้ด้านบน ดีมาก เราจะกลับไปที่คลาส StoryCard ในอีกสักครู่เพื่อใช้วิดีโอที่เล่นอัตโนมัติ

4. คอมโพเนนต์ <story-viewer>

องค์ประกอบ <story-viewer> ของเราเป็นพาเรนต์ของ <story-card> โมเดลนี้มีหน้าที่จัดวางการ์ดในแนวนอนและให้เราปัดการ์ดไปมา เราจะเริ่มจากวิธีเดียวกับตอนที่ StoryCard เราต้องการเพิ่มการ์ดเรื่องราวเป็นองค์ประกอบย่อยขององค์ประกอบ <story-viewer> ดังนั้นให้เพิ่มช่องสำหรับเด็กๆ เหล่านั้น

ใส่โค้ดต่อไปนี้ใน story-viewer.ts

import { LitElement, html } from 'lit';
import { customElement } from 'lit/decorators.js';

@customElement('story-viewer')
export class StoryViewer extends LitElement {
  render() {
    return html`<slot></slot>`;
  }
}

รูปแบบถัดไปคือเลย์เอาต์แนวนอน เราจัดการปัญหานี้ได้ด้วยการระบุตําแหน่งสัมบูรณ์ของ <story-card> ทั้งหมดในช่อง และแปลตามดัชนี เรากำหนดเป้าหมายตัวองค์ประกอบ <story-viewer> เองได้โดยใช้ตัวเลือก :host

static styles = css`
  :host {
    display: block;
    position: relative;
    /* Default size */
    width: 300px;
    height: 800px;
  }
  ::slotted(*) {
    position: absolute;
    width: 100%;
    height: 100%;
  }`;

ผู้ใช้สามารถควบคุมขนาดของการ์ดเรื่องราวได้ง่ายๆ เพียงลบล้างความสูงและความกว้างเริ่มต้นในโฮสต์จากภายนอก ดังนี้

story-viewer {
  width: 400px;
  max-width: 100%;
  height: 80%;
}

หากต้องการติดตามการ์ดที่ดูอยู่ในปัจจุบัน ให้เพิ่มตัวแปรอินสแตนซ์ index ลงในคลาส StoryViewer การตกแต่งองค์ประกอบด้วย @property ของ LitElement จะทำให้คอมโพเนนต์แสดงผลอีกครั้งเมื่อค่ามีการเปลี่ยนแปลง

import { property } from 'lit/decorators.js';

export class StoryViewer extends LitElement {
  @property({type: Number}) index: number = 0;
}

ต้องแปลการ์ดแต่ละใบในแนวนอน มาลองใช้การแปลเหล่านี้ในupdateเมธอดวงจรชีวิตของ lit-element เมธอดการอัปเดตจะทํางานทุกครั้งที่มีการเปลี่ยนแปลงพร็อพเพอร์ตี้ที่สังเกตได้ของคอมโพเนนต์นี้ โดยปกติแล้ว เราจะค้นหาช่องและวนผ่าน slot.assignedElements() อย่างไรก็ตาม เนื่องจากเรามีเพียงช่องที่ไม่มีชื่อช่องเดียว การดำเนินการนี้จึงเหมือนกับการใช้ this.children เราจะใช้ this.children เพื่อความสะดวก

import { PropertyValues } from 'lit';

export class StoryViewer extends LitElement {
  update(changedProperties: PropertyValues) {
    const width = this.clientWidth;
    Array.from(this.children).forEach((el: Element, i) => {
      const x = (i - this.index) * width;
      (el as HTMLElement).style.transform = `translate3d(${x}px,0,0)`;
    });
    super.update(changedProperties);
  }
}

<story-card> ทั้งหมดติดกันแล้ว องค์ประกอบนี้ยังคงใช้งานได้กับองค์ประกอบอื่นๆ ที่เป็นองค์ประกอบย่อย ตราบใดที่เราจัดสไตล์ให้เหมาะสม

<story-viewer>
  <!-- A regular story-card child... -->
  <story-card>
    <video slot="media" src="some/video.mp4"></video>
    <h1>This video</h1>
    <p>is so cool.</p>
  </story-card>
  <!-- ...and other elements work too! -->
  <img style="object-fit: cover" src="some/img.png" />
</story-viewer>

ไปที่ build/index.html แล้วยกเลิกการคอมเมนต์องค์ประกอบการ์ดเรื่องราวที่เหลือ ตอนนี้มาทำให้ไปยังส่วนต่างๆ ได้กัน

5. แถบความคืบหน้าและการนำทาง

ถัดไป เราจะเพิ่มวิธีไปยังการ์ดต่างๆ และแถบความคืบหน้า

มาเพิ่มฟังก์ชันตัวช่วยบางอย่างลงใน StoryViewer เพื่อใช้ไปยังส่วนต่างๆ ของเรื่องราวกัน เครื่องมือนี้จะตั้งค่าดัชนีให้เราขณะจำกัดค่าให้อยู่ในช่วงที่ใช้ได้

ใน Story-viewer.ts ให้เพิ่มสิ่งต่อไปนี้ในชั้นเรียน StoryViewer

/** Advance to the next story card if possible **/
next() {
  this.index = Math.max(0, Math.min(this.children.length - 1, this.index + 1));
}

/** Go back to the previous story card if possible **/
previous() {
  this.index = Math.max(0, Math.min(this.children.length - 1, this.index - 1));
}

ในการแสดงการนำทางต่อผู้ใช้ปลายทาง เราจะเพิ่มปุ่ม "ก่อนหน้า" และ "ถัดไป" ให้กับ <story-viewer> เมื่อคลิกปุ่มใดปุ่มหนึ่ง เราต้องการเรียกใช้ฟังก์ชันตัวช่วยของ next หรือ previous โดย lit-html ช่วยให้เพิ่ม Listener เหตุการณ์ลงในองค์ประกอบได้โดยง่าย เราแสดงผลปุ่มและเพิ่ม Listener การคลิกได้ในเวลาเดียวกัน

อัปเดตวิธีการ render เป็นดังนี้

export class StoryViewer extends LitElement {
  render() {
    return html`
      <slot></slot>

      <svg id="prev" viewBox="0 0 10 10" @click=${() => this.previous()}>
        <path d="M 6 2 L 4 5 L 6 8" stroke="#fff" fill="none" />
      </svg>
      <svg id="next" viewBox="0 0 10 10" @click=${() => this.next()}>
        <path d="M 4 2 L 6 5 L 4 8" stroke="#fff" fill="none" />
      </svg>
    `;
  }
}

ดูวิธีเพิ่ม Listener เหตุการณ์ในบรรทัดบนปุ่ม SVG ใหม่ของเราได้เลยในเมธอด render ซึ่งใช้ได้กับทุกกิจกรรม เพียงเพิ่มการเชื่อมโยงของแบบฟอร์ม @eventname=${handler} กับองค์ประกอบ

เพิ่มข้อมูลต่อไปนี้ลงในพร็อพเพอร์ตี้ static styles เพื่อกำหนดสไตล์ปุ่ม

svg {
  position: absolute;
  top: calc(50% - 25px);
  height: 50px;
  cursor: pointer;
}
#next {
  right: 0;
}

สำหรับแถบความคืบหน้า เราจะใช้ตารางกริด CSS ในการจัดรูปแบบกล่องเล็กๆ 1 กล่องสำหรับการ์ดเรื่องราวแต่ละใบ เราสามารถใช้พร็อพเพอร์ตี้ index เพื่อเพิ่มคลาสอย่างมีเงื่อนไขลงในช่องเพื่อระบุว่ามีการ "เห็น" หรือไม่ เราใช้นิพจน์แบบมีเงื่อนไข เช่น i <= this.index : 'watched': '' ได้ แต่จะมีรายละเอียดครบถ้วนหากเราเพิ่มคลาสอีก โชคดีที่ lit-html มีคำสั่งที่ชื่อ classMap เพื่อช่วยในเรื่องนี้ ก่อนอื่น ให้นําเข้า classMap

import { classMap } from 'lit/directives/class-map';

และเพิ่มมาร์กอัปต่อไปนี้ที่ด้านล่างของเมธอด render

<div id="progress">
  ${Array.from(this.children).map((_, i) => html`
    <div
      class=${classMap({watched: i <= this.index})}
      @click=${() => this.index = i}
    ></div>`
  )}
</div>

นอกจากนี้ เรายังเพิ่มตัวแฮนเดิลการคลิกอีกเล็กน้อยเพื่อให้ผู้ใช้ข้ามไปยังการ์ดเรื่องราวที่ต้องการได้โดยตรงหากต้องการ

นี่คือรูปแบบใหม่ที่จะเพิ่มลงใน static styles

::slotted(*) {
  position: absolute;
  width: 100%;
  /* Changed this line! */
  height: calc(100% - 20px);
}

#progress {
  position: relative;
  top: calc(100% - 20px);
  height: 20px;
  width: 50%;
  margin: 0 auto;
  display: grid;
  grid-auto-flow: column;
  grid-auto-columns: 1fr;
  grid-gap: 10px;
  align-content: center;
}
#progress > div {
  background: grey;
  height: 4px;
  transition: background 0.3s linear;
  cursor: pointer;
}
#progress > div.watched {
  background: white;
}

แถบนำทางและแถบความคืบหน้าเสร็จสมบูรณ์ มาเพิ่มลูกเล่นกัน

6. การเลื่อน

เราจะใช้ไลบรารีการควบคุมด้วยท่าทางสัมผัส Hammer.js เพื่อใช้การปัด ค้อนจะตรวจจับท่าทางสัมผัสพิเศษ เช่น การแพน และส่งเหตุการณ์ที่มีข้อมูลที่เกี่ยวข้อง (เช่น เดลต้า X) ที่เราได้ยิน

วิธีที่เราใช้ Hammer เพื่อตรวจหาการแพนและอัปเดตองค์ประกอบโดยอัตโนมัติทุกครั้งที่มีเหตุการณ์การแพนเกิดขึ้นมีดังนี้

import { state } from 'lit/decorators.js';
import 'hammerjs';

export class StoryViewer extends LitElement {
  // Data emitted by Hammer.js
  @state() _panData: {isFinal?: boolean, deltaX?: number} = {};

  constructor() {
    super();
    this.index = 0;
    new Hammer(this).on('pan', (e: HammerInput) => this._panData = e);
  }
}

เครื่องมือสร้างของคลาส LitElement เป็นอีกตำแหน่งที่ดีในการแนบ Listener เหตุการณ์กับองค์ประกอบโฮสต์ เครื่องมือสร้างค้อนจะนำองค์ประกอบเพื่อตรวจหาท่าทางสัมผัส ในกรณีของเรา นี่คือ StoryViewer เอง หรือ this จากนั้นใช้ API ของ Hammer เพื่อบอกให้ตรวจจับท่าทางสัมผัส "เลื่อน" และตั้งค่าการเลื่อนไปยังพร็อพเพอร์ตี้ _panData ใหม่

การตกแต่งพร็อพเพอร์ตี้ _panData ด้วย @state จะทำให้ LitElement สังเกตการเปลี่ยนแปลงของ _panData และทำการอัปเดต แต่จะไม่มีแอตทริบิวต์ HTML ที่เชื่อมโยงกับพร็อพเพอร์ตี้ดังกล่าว

ถัดไป มาเพิ่มตรรกะของ update เพื่อใช้ข้อมูลการเลื่อน ดังนี้

// Update is called whenever an observed property changes.
update(changedProperties: PropertyValues) {
  // deltaX is the distance of the current pan gesture.
  // isFinal is whether the pan gesture is ending.
  let { deltaX = 0, isFinal = false } = this._panData;
  // When the pan gesture finishes, navigate.
  if (!changedProperties.has('index') && isFinal) {
    deltaX > 0 ? this.previous() : this.next();
  }
  // We don't want any deltaX when releasing a pan.
  deltaX = isFinal ? 0 : deltaX;
  const width = this.clientWidth;
  Array.from(this.children).forEach((el: Element, i) => {
    // Updated this line to utilize deltaX.
    const x = (i - this.index) * width + deltaX;
    (el as HTMLElement).style.transform = `translate3d(${x}px,0,0)`;
  });

  // Don't forget to call super!
  super.update(changedProperties);
}

ตอนนี้เราลากการ์ดเรื่องราวไปมาได้แล้ว หากต้องการดำเนินการต่างๆ ให้เป็นไปอย่างราบรื่น เรากลับไปที่ static get styles แล้วเพิ่ม transition: transform 0.35s ease-out; ลงในตัวเลือก ::slotted(*) ดังนี้

::slotted(*) {
  ...
  transition: transform 0.35s ease-out;
}

ตอนนี้การปัดจะราบรื่นขึ้น

ไปยังการ์ดเรื่องราวต่างๆ ด้วยการปัดอย่างราบรื่น

7. เล่นอัตโนมัติ

ฟีเจอร์สุดท้ายที่จะเพิ่มคือการเล่นวิดีโออัตโนมัติ เมื่อการ์ดเรื่องราวเข้าสู่จุดโฟกัส เราอยากให้เล่นวิดีโอพื้นหลัง (หากมี) เมื่อการ์ดเรื่องราวออกจากจุดโฟกัส เราควรหยุดวิดีโอของการ์ดดังกล่าวชั่วคราว

เราจะใช้เหตุการณ์นี้โดยส่งเหตุการณ์ที่กําหนดเอง "เข้า" และ "ออก" ในรายการย่อยที่เหมาะสมทุกครั้งที่ดัชนีมีการเปลี่ยนแปลง ในเดือนStoryCard เราจะรับกิจกรรมดังกล่าวและเล่นหรือหยุดวิดีโอที่มีอยู่ชั่วคราว เหตุใดจึงเลือกส่งเหตุการณ์ไปยังรายการย่อยแทนการเรียกเมธอดอินสแตนซ์ "entered" และ "exited" ที่กําหนดไว้ใน StoryCard เมื่อใช้วิธีการ ผู้ใช้คอมโพเนนต์จะไม่มีทางเลือกอื่นนอกจากต้องเขียนองค์ประกอบที่กำหนดเอง หากพวกเขาต้องการเขียนการ์ดเรื่องราวของตนเองด้วยภาพเคลื่อนไหวที่กำหนดเอง เมื่อใช้เหตุการณ์ เด็กๆ จะสามารถแนบ Listener เหตุการณ์ได้

ลองเปลี่ยนโครงสร้างภายในโค้ด index ของ StoryViewer เพื่อใช้ตัวตั้งค่า ซึ่งให้เส้นทางโค้ดที่สะดวกสําหรับส่งเหตุการณ์

class StoryViewer extends LitElement {
  @state() private _index: number = 0
  get index() {
    return this._index
  }
  set index(value: number) {
    this.children[this._index].dispatchEvent(new CustomEvent('exited'));
    this.children[value].dispatchEvent(new CustomEvent('entered'));
    this._index = value;
  }
}

เราจะเพิ่ม Listeners เหตุการณ์สําหรับ "entered" และ "exited" ในคอนสตรัคเตอร์ StoryCard ที่เล่นและหยุดวิดีโอชั่วคราวเพื่อปิดท้ายฟีเจอร์เล่นอัตโนมัติ

โปรดทราบว่าผู้ใช้คอมโพเนนต์อาจให้หรือไม่ให้องค์ประกอบวิดีโอในช่องสื่อของ <story-card> ก็ได้ โดยอาจไม่ใส่องค์ประกอบในช่องสื่อเลย เราต้องระวังอย่าเรียก play ใน img หรือใน null

กลับไปที่ story-card.ts แล้วเพิ่มข้อมูลต่อไปนี้

import { query } from 'lit/decorators.js';

class StoryCard extends LitElement {
  constructor() {
    super();

    this.addEventListener("entered", () => {
      if (this._slottedMedia) {
        this._slottedMedia.currentTime = 0;
        this._slottedMedia.play();
      }
    });

    this.addEventListener("exited", () => {
      if (this._slottedMedia) {
        this._slottedMedia.pause();
      }
    });
  }

 /**
  * The element in the "media" slot, ONLY if it is an
  * HTMLMediaElement, such as <video>.
  */
 private get _slottedMedia(): HTMLMediaElement|null {
   const el = this._mediaSlot && this._mediaSlot.assignedNodes()[0];
   return el instanceof HTMLMediaElement ? el : null;
 }

  /**
   * @query(selector) is shorthand for
   * this.renderRoot.querySelector(selector)
   */
  @query("slot[name=media]")
  private _mediaSlot!: HTMLSlotElement;
}

เล่นอัตโนมัติเสร็จสมบูรณ์ ✅

8. ชั่งน้ำหนัก

เมื่อเรามีฟีเจอร์ที่จำเป็นทั้งหมดแล้ว เรามาเพิ่มอีก 1 ฟีเจอร์กัน นั่นก็คือเอฟเฟกต์การปรับขนาด กลับไปที่เมธอด update ของ StoryViewer กันอีกครั้ง คำนวณบางอย่างเพื่อหาค่าคงที่ scale ค่านี้จะเท่ากับ 1.0 สำหรับรายการย่อยที่ใช้งานอยู่ และ minScale ในกรณีอื่นๆ โดยจะประมาณค่าระหว่าง 2 ค่านี้ด้วย

เปลี่ยนลูปในเมธอด update ใน story-viewer.ts เป็น

update(changedProperties: PropertyValues) {
  // ...
  const minScale = 0.8;
  Array.from(this.children).forEach((el: Element, i) => {
    const x = (i - this.index) * width + deltaX;

    // Piecewise scale(deltaX), looks like: __/\__
    const u = deltaX / width + (i - this.index);
    const v = -Math.abs(u * (1 - minScale)) + 1;
    const scale = Math.max(v, minScale);
    // Include the scale transform
    (el as HTMLElement).style.transform = `translate3d(${x}px,0,0) scale(${scale})`;
  });
  // ...
}

ไม่มีชุดข้อความอื่นแล้วจ้า ในโพสต์นี้เราได้พูดถึงหลายเรื่อง ซึ่งรวมถึงคุณลักษณะ LitElement และ lit-html, องค์ประกอบช่อง HTML และการควบคุมด้วยท่าทางสัมผัส

ดูคอมโพเนนต์เวอร์ชันสมบูรณ์ได้ที่ https://github.com/PolymerLabs/story-viewer