লিট-এলিমেন্ট সহ একটি গল্পের উপাদান তৈরি করুন

১. ভূমিকা

আজকাল স্টোরি একটি জনপ্রিয় UI কম্পোনেন্ট। সোশ্যাল ও নিউজ অ্যাপগুলো তাদের ফিডে এটি যুক্ত করছে। এই কোডল্যাবে আমরা lit-element এবং TypeScript ব্যবহার করে একটি স্টোরি কম্পোনেন্ট তৈরি করব।

শেষে গল্পের অংশটি দেখতে এইরকম হবে:

একটি সম্পূর্ণ স্টোরি-ভিউয়ার কম্পোনেন্ট, যেখানে কফির তিনটি ছবি প্রদর্শিত হচ্ছে।

আমরা একটি সোশ্যাল মিডিয়া বা সংবাদ 'স্টোরি'-কে ক্রমানুসারে খেলার জন্য কার্ডের একটি সংগ্রহ হিসাবে ভাবতে পারি, অনেকটা স্লাইডশোর মতো। আসলে, স্টোরিগুলো আক্ষরিক অর্থেই স্লাইডশো। কার্ডগুলোতে সাধারণত একটি ছবি বা স্বয়ংক্রিয়ভাবে চলতে থাকা ভিডিও প্রাধান্য পায় এবং এর উপরে অতিরিক্ত লেখা থাকতে পারে। আমরা যা তৈরি করব তা নিচে দেওয়া হলো:

বৈশিষ্ট্য তালিকা

  • ছবি বা ভিডিও ব্যাকগ্রাউন্ড সহ কার্ড।
  • গল্পে এগিয়ে যেতে বামে বা ডানে সোয়াইপ করুন।
  • স্বয়ংক্রিয়ভাবে চলতে থাকা ভিডিও।
  • কার্ডে টেক্সট যোগ করার বা অন্যভাবে কাস্টমাইজ করার সুবিধা।

এই কম্পোনেন্টটির ডেভেলপার অভিজ্ঞতার কথা বলতে গেলে, স্টোরি কার্ডগুলোকে সাধারণ 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 যেখানে যেখানে কাজ করে, এটিও সেখানে সেখানে কাজ করে: কন্টেন্ট ম্যানেজমেন্ট সিস্টেম, ফ্রেমওয়ার্ক ইত্যাদি।

পূর্বশর্ত

  • একটি শেল যেখানে আপনি git এবং npm চালাতে পারবেন
  • একটি টেক্সট এডিটর

২. স্থাপন করা

প্রথমে এই রিপোটি ক্লোন করুন: story-viewer-starter

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

পরিবেশটি ইতিমধ্যে লিট-এলিমেন্ট এবং টাইপস্ক্রিপ্ট দিয়ে সেট আপ করা আছে। শুধু ডিপেন্ডেন্সিগুলো ইনস্টল করুন:

npm i

ভিএস কোড ব্যবহারকারীরা, লিট-এইচটিএমএল টেমপ্লেটের অটোকমপ্লিশন, টাইপ-চেকিং এবং লিন্টিং সুবিধা পেতে লিট-প্লাগইন এক্সটেনশনটি ইনস্টল করুন।

নিম্নলিখিত কমান্ডটি চালিয়ে ডেভেলপমেন্ট এনভায়রনমেন্ট শুরু করুন:

npm run dev

আপনি কোডিং শুরু করার জন্য প্রস্তুত!

৩. <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 ইনস্ট্যান্স মেথডটি সংজ্ঞায়িত করুন। এখানেই আমরা lit-html-এর html ট্যাগ ব্যবহার করে এলিমেন্টটির জন্য টেমপ্লেট সরবরাহ করব।

এই কম্পোনেন্টের টেমপ্লেটে কী থাকা উচিত? ব্যবহারকারী দুটি জিনিস যোগ করতে পারবেন: একটি মিডিয়া এলিমেন্ট এবং একটি ওভারলে। তাই, আমরা এগুলোর প্রতিটির জন্য একটি করে <slot> যোগ করব।

স্লটের মাধ্যমেই আমরা নির্দিষ্ট করি যে একটি কাস্টম এলিমেন্টের চাইল্ড এলিমেন্টগুলো কীভাবে রেন্ডার করা হবে। আরও তথ্যের জন্য, এখানে স্লট ব্যবহারের একটি চমৎকার ওয়াকথ্রু রয়েছে।

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>
    `;
  }
}

মিডিয়া এলিমেন্টকে তার নিজস্ব স্লটে আলাদা করলে তা আমাদের ফুল-ব্লিড স্টাইলিং যোগ করা এবং ভিডিও অটো-প্লে করার মতো কাজের জন্য ওই এলিমেন্টটিকে নির্দিষ্ট করতে সাহায্য করবে। দ্বিতীয় স্লটটি (কাস্টম ওভারলে-র জন্য) একটি কন্টেইনার এলিমেন্টের ভেতরে রাখুন, যাতে আমরা পরে কিছু ডিফল্ট প্যাডিং দিতে পারি।

<story-card> কম্পোনেন্টটি এখন এইভাবে ব্যবহার করা যাবে:

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

কিন্তু, এটা দেখতে ভয়ানক:

একটি স্টাইলবিহীন স্টোরি-ভিউয়ারে কফির একটি ছবি প্রদর্শিত হচ্ছে।

শৈলী যোগ করা

চলুন কিছু স্টাইল যোগ করা যাক। lit-element ব্যবহার করে আমরা একটি স্ট্যাটিক ' styles প্রপার্টি নির্ধারণ করি এবং css ট্যাগযুক্ত একটি টেমপ্লেট স্ট্রিং রিটার্ন করি। এখানে যা কিছু CSS লেখা হয়, তা শুধুমাত্র আমাদের কাস্টম এলিমেন্টের উপরেই প্রযোজ্য হয়! এই দিক থেকে শ্যাডো ডোমের সাথে CSS বেশ চমৎকার।

চলুন, <story-card> টিকে ঢেকে ফেলার জন্য স্লটেড মিডিয়া এলিমেন্টটিকে স্টাইল করি। এই সুযোগে, আমরা দ্বিতীয় স্লটের এলিমেন্টগুলোর জন্য কিছু সুন্দর ফরম্যাটিংও দিয়ে দিতে পারি। এর ফলে, কম্পোনেন্ট ব্যবহারকারীরা কিছু <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 ক্লাসে ফিরে আসব।

৪. <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> এলিমেন্টকে অ্যাবসোলিউট পজিশনিং দিতে পারি এবং তাদের ইন্ডেক্স অনুযায়ী ট্রান্সলেট করতে পারি। আমরা :host সিলেক্টর ব্যবহার করে সরাসরি <story-viewer> এলিমেন্টটিকে টার্গেট করতে পারি।

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%;
}

বর্তমানে দেখা কার্ডটির হিসাব রাখার জন্য, StoryViewer ক্লাসে index নামে একটি ইনস্ট্যান্স index যোগ করা যাক। এটিকে LitElement-এর @property দিয়ে ডেকোরেট করলে, এর মান পরিবর্তন হলেই কম্পোনেন্টটি পুনরায় রেন্ডার হবে।

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

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

প্রতিটি কার্ডকে আনুভূমিকভাবে সঠিক অবস্থানে সরাতে হবে। চলুন, এই স্থানান্তরগুলো lit-element-এর update লাইফসাইকেল মেথডে প্রয়োগ করি। এই কম্পোনেন্টের কোনো অবজার্ভড প্রপার্টি পরিবর্তিত হলেই update মেথডটি রান করবে। সাধারণত, আমরা স্লটটি কোয়েরি করে 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 এ যান এবং বাকি story-card এলিমেন্টগুলোর কমেন্ট তুলে দিন । এবার, চলুন সেগুলোতে নেভিগেট করার ব্যবস্থা করি!

৫. অগ্রগতি বার এবং নেভিগেশন

এরপরে, আমরা কার্ডগুলোর মধ্যে যাতায়াতের একটি উপায় এবং একটি অগ্রগতি বার যুক্ত করব।

চলুন, স্টোরিতে নেভিগেট করার জন্য 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> এ 'previous' এবং 'next' বাটন যোগ করব। যেকোনো একটি বাটন ক্লিক করা হলে, আমরা next অথবা previous হেল্পার ফাংশনটি কল করতে চাই। lit-html এলিমেন্টগুলিতে ইভেন্ট লিসেনার যোগ করা সহজ করে তোলে; আমরা একই সাথে বাটনগুলো রেন্ডার করতে এবং একটি ক্লিক লিসেনার যোগ করতে পারি।

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>
    `;
  }
}

দেখে নিন কিভাবে আমরা আমাদের নতুন SVG বাটনগুলিতে, সরাসরি render মেথডের মধ্যেই ইনলাইন ইভেন্ট লিসেনার যোগ করতে পারি। এটি যেকোনো ইভেন্টের জন্যই কাজ করে। শুধু একটি এলিমেন্টে @eventname=${handler} ফর্মের একটি বাইন্ডিং যোগ করুন।

বাটনগুলোকে স্টাইল করার জন্য static styles প্রপার্টিতে নিম্নলিখিতটি যোগ করুন:

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

প্রোগ্রেস বারের জন্য, আমরা CSS গ্রিড ব্যবহার করে ছোট ছোট বক্স স্টাইল করব, প্রতিটি স্টোরি কার্ডের জন্য একটি করে। বক্সগুলো "দেখা" হয়েছে কি না, তা বোঝানোর জন্য আমরা শর্তসাপেক্ষে ক্লাস যোগ করতে 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;
}

নেভিগেশন এবং প্রোগ্রেস বার তৈরি। এবার কিছু আকর্ষণীয় বিষয় যোগ করা যাক!

৬. সোয়াইপ করা

সোয়াইপিং বাস্তবায়নের জন্য, আমরা Hammer.js জেসচার কন্ট্রোল লাইব্রেরিটি ব্যবহার করব। Hammer প্যানের মতো বিশেষ জেসচার শনাক্ত করে এবং প্রাসঙ্গিক তথ্য (যেমন ডেল্টা এক্স) সহ ইভেন্ট প্রেরণ করে, যা আমরা ব্যবহার করতে পারি।

যেভাবে আমরা 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 ক্লাসের কনস্ট্রাক্টর হলো হোস্ট এলিমেন্টটিতেই ইভেন্ট লিসেনার যুক্ত করার আরেকটি চমৎকার জায়গা। Hammer কনস্ট্রাক্টরটি জেসচার শনাক্ত করার জন্য একটি এলিমেন্ট গ্রহণ করে। আমাদের ক্ষেত্রে, এটি হলো StoryViewer নিজেই, অথবা this । তারপর, Hammer-এর API ব্যবহার করে, আমরা এটিকে 'pan' জেসচার শনাক্ত করতে বলি এবং প্যানের তথ্য একটি নতুন _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 এ ফিরে যাই এবং ::slotted(*) সিলেক্টরে transition: transform 0.35s ease-out; যোগ করি:

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

এখন আমরা সাবলীলভাবে সোয়াইপ করতে পারি:

সাবলীল সোয়াইপের মাধ্যমে স্টোরি-কার্ডগুলোর মধ্যে চলাচল

৭. অটোপ্লে

আমরা সর্বশেষ যে ফিচারটি যোগ করব তা হলো স্বয়ংক্রিয়ভাবে ভিডিও চালু হওয়া। যখন কোনো স্টোরি কার্ড ফোকাসে আসবে, আমরা চাই যে ব্যাকগ্রাউন্ড ভিডিওটি (যদি থাকে) চালু হোক। আর যখন কোনো স্টোরি কার্ড ফোকাস থেকে সরে যাবে, তখন তার ভিডিওটি পজ হয়ে যাবে।

যখনই ইনডেক্স পরিবর্তিত হবে, আমরা উপযুক্ত চাইল্ড কম্পোনেন্টগুলোতে 'entered' এবং 'exited' কাস্টম ইভেন্ট ডিসপ্যাচ করার মাধ্যমে এটি বাস্তবায়ন করব। StoryCard এ, আমরা সেই ইভেন্টগুলো গ্রহণ করে বিদ্যমান যেকোনো ভিডিও প্লে বা পজ করব। স্টোরিকার্ড-এ সংজ্ঞায়িত 'entered' এবং 'exited' ইনস্ট্যান্স মেথড কল করার পরিবর্তে চাইল্ড কম্পোনেন্টগুলোতে ইভেন্ট ডিসপ্যাচ করার কারণ কী? মেথড ব্যবহার করলে, কম্পোনেন্ট ব্যবহারকারীরা যদি কাস্টম অ্যানিমেশনসহ নিজেদের স্টোরিকার্ড তৈরি করতে চাইতেন, তবে তাদের একটি কাস্টম এলিমেন্ট লেখা ছাড়া আর কোনো উপায় থাকত না। কিন্তু ইভেন্ট ব্যবহার করলে, তারা কেবল একটি ইভেন্ট লিসেনার যুক্ত করতে পারেন!

চলুন StoryViewer এর index প্রপার্টিটিকে একটি সেটার ব্যবহার করার জন্য রিফ্যাক্টর করি, যা ইভেন্টগুলো ডিসপ্যাচ করার জন্য একটি সুবিধাজনক কোড পাথ প্রদান করে:

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;
  }
}

অটোপ্লে ফিচারটি সম্পূর্ণ করতে, আমরা StoryCard কনস্ট্রাক্টরে 'entered' এবং 'exited' এর জন্য ইভেন্ট লিসেনার যোগ করব, যা ভিডিওটি প্লে ও পজ করবে।

মনে রাখবেন যে কম্পোনেন্ট ব্যবহারকারী <story-card> এর মিডিয়া স্লটে একটি ভিডিও এলিমেন্ট দিতেও পারেন, আবার নাও দিতে পারেন। এমনকি তারা মিডিয়া স্লটে কোনো এলিমেন্ট নাও দিতে পারেন। আমাদের সতর্ক থাকতে হবে যেন কোনো img বা null-এর উপর play কল না করা হয়।

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;
}

স্বয়ংক্রিয় প্লে সম্পন্ন। ✅

৮. ভারসাম্য পরিবর্তন করুন

এখন যেহেতু আমাদের কাছে সমস্ত প্রয়োজনীয় বৈশিষ্ট্য রয়েছে, চলুন আরও একটি যোগ করি: একটি চমৎকার স্কেলিং এফেক্ট। চলুন আরেকবার StoryViewer এর update মেথডে ফিরে যাই। scale কনস্ট্যান্টের মানটি পাওয়ার জন্য কিছু গাণিতিক হিসাব করা হয়। এটি active child-এর জন্য 1.0 এবং অন্যথায় minScale সমান হবে, এবং এই দুটি মানের মধ্যে ইন্টারপোলেটও করবে।

story-viewer.ts-এর update মেথডের লুপটি পরিবর্তন করে নিম্নরূপ করুন:

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