कोडलैब: स्टोरीज़ से जुड़ा कॉम्पोनेंट बनाना

इस कोडलैब में, वेब पर Instagram Stories जैसा अनुभव बनाने का तरीका बताया गया है. आगे बढ़ने के साथ-साथ, हम एचटीएमएल, फिर सीएसएस, और फिर JavaScript से शुरू करते हुए कॉम्पोनेंट बनाएंगे.

इस कॉम्पोनेंट को बनाते समय किए गए बेहतरीन सुधारों के बारे में जानने के लिए, मेरी ब्लॉग पोस्ट Stories कॉम्पोनेंट बनाना देखें.

सेटअप

  1. प्रोजेक्ट में बदलाव करने के लिए, बदलाव करने के लिए रीमिक्स करें पर क्लिक करें.
  2. app/index.html खोलें.

एचटीएमएल

मैं हमेशा सिमेंटिक एचटीएमएल इस्तेमाल करना चाहता/चाहती हूं. हर दोस्त के पास, जितनी चाहे उतनी स्टोरी हो सकती हैं. इसलिए, मैंने यह फ़ैसला लिया कि हर दोस्त के लिए <section> एलिमेंट और हर स्टोरी के लिए <article> एलिमेंट का इस्तेमाल किया जाए. चलिए, फिर से शुरू करते हैं. सबसे पहले, हमें अपने स्टोरीज़ कॉम्पोनेंट के लिए एक कंटेनर चाहिए.

अपने <body> में <div> एलिमेंट जोड़ें:

<div class="stories">

</div>

दोस्तों की जानकारी देने के लिए, कुछ <section> एलिमेंट जोड़ें:

<div class="stories">
  <section class="user"></section>
  <section class="user"></section>
  <section class="user"></section>
  <section class="user"></section>
</div>

स्टोरीज़ दिखाने के लिए, कुछ <article> एलिमेंट जोड़ें:

<div class="stories">
  <section class="user">
    <article class="story" style="--bg: url(https://picsum.photos/480/840);"></article>
    <article class="story" style="--bg: url(https://picsum.photos/480/841);"></article>
  </section>
  <section class="user">
    <article class="story" style="--bg: url(https://picsum.photos/481/840);"></article>
  </section>
  <section class="user">
    <article class="story" style="--bg: url(https://picsum.photos/481/841);"></article>
  </section>
  <section class="user">
    <article class="story" style="--bg: url(https://picsum.photos/482/840);"></article>
    <article class="story" style="--bg: url(https://picsum.photos/482/843);"></article>
    <article class="story" style="--bg: url(https://picsum.photos/482/844);"></article>
  </section>
</div>
  • हम स्टोरीज़ के प्रोटोटाइप बनाने के लिए, इमेज सेवा (picsum.com) का इस्तेमाल कर रहे हैं.
  • हर <article> पर मौजूद style एट्रिब्यूट, प्लेसहोल्डर लोड करने की टेक्नोलॉजी का हिस्सा है. इस बारे में अगले सेक्शन में ज़्यादा जानकारी दी गई है.

सीएसएस

हमारा कॉन्टेंट स्टाइल के लिए तैयार है. आइए, उन डेटा को ऐसी चीज़ों में बदलें जिनसे लोग इंटरैक्ट करना चाहें. हम आज मोबाइल-फ़र्स्ट पर काम करेंगे.

.stories

हमें अपने <div class="stories"> कंटेनर के लिए, हॉरिज़ॉन्टल स्क्रोलिंग वाला कंटेनर चाहिए. ऐसा करने के लिए, हम ये काम कर सकते हैं:

  • कंटेनर को ग्रिड बनाना
  • पंक्ति ट्रैक को भरने के लिए हर चाइल्ड को सेट करना
  • हर चाइल्ड की चौड़ाई को मोबाइल डिवाइस के व्यूपोर्ट की चौड़ाई के बराबर करना

ग्रिड तब तक पिछले कॉलम की दाईं ओर, 100vw चौड़ा नए कॉलम दिखाता रहेगा, जब तक कि इसमें आपके मार्कअप में सभी एचटीएमएल एलिमेंट शामिल नहीं कर दिए जाते.

Chrome और DevTools, पूरी चौड़ाई वाले लेआउट को दिखाने वाले ग्रिड विज़ुअल के साथ खुलते हैं
Chrome DevTools में ग्रिड कॉलम ओवरफ़्लो दिख रहा है. साथ ही, एक हॉरिज़ॉन्टल स्क्रोलर भी दिख रहा है.

app/css/index.css के सबसे नीचे, यह सीएसएस जोड़ें:

.stories {
  display: grid;
  grid: 1fr / auto-flow 100%;
  gap: 1ch;
}

अब हमारे पास व्यूपोर्ट से ज़्यादा कॉन्टेंट है. अब यह बताने का समय आ गया है कि उस कॉन्टेंट को कैसे मैनेज किया जाए. अपने .stories नियमों-सेट में कोड की हाइलाइट की गई पंक्तियां जोड़ें:

.stories {
  display: grid;
  grid: 1fr / auto-flow 100%;
  gap: 1ch;
  overflow-x: auto;
  scroll-snap-type: x mandatory;
  overscroll-behavior: contain;
  touch-action: pan-x;
}

हमें हॉरिज़ॉन्टल स्क्रोलिंग चाहिए, इसलिए हम overflow-x को auto पर सेट कर देंगे. जब लोग स्क्रोल करते हैं, तब हम चाहते हैं कि कॉम्पोनेंट हमें अगली स्टोरी पर धीरे से स्क्रोल करे. इसलिए, हम scroll-snap-type: x mandatory का इस्तेमाल करेंगे. इस सीएसएस के बारे में ज़्यादा जानने के लिए, मेरी ब्लॉग पोस्ट के सीएसएस स्क्रोल स्नैप पॉइंट और overscroll-behavior सेक्शन पढ़ें.

स्क्रॉल स्नैपिंग के लिए, पैरंट कंटेनर और चाइल्ड कंटेनर, दोनों की सहमति ज़रूरी है. इसलिए, अब हम इस बारे में बताते हैं. app/css/index.css के नीचे यह कोड जोड़ें:

.user {
  scroll-snap-align: start;
  scroll-snap-stop: always;
}

आपका ऐप्लिकेशन अभी काम नहीं करता. हालांकि, नीचे दिए गए वीडियो में दिखाया गया है कि scroll-snap-type को चालू और बंद करने पर क्या होता है. इस सुविधा के चालू होने पर, हर हॉरिज़ॉन्टल स्क्रोल, अगली खबर पर स्नैप हो जाता है. बंद होने पर, ब्राउज़र अपने डिफ़ॉल्ट स्क्रोलिंग तरीके का इस्तेमाल करता है.

ऐसा करने पर, आपको अपने दोस्तों के स्टोरीज़ दिखेंगे. हालांकि, हमें स्टोरीज़ से जुड़ी एक समस्या को ठीक करना है.

.user

चलिए, .user सेक्शन में एक लेआउट बनाते हैं, जो उन चाइल्ड स्टोरी एलिमेंट को सही जगह पर दिखाएगा. हम इस समस्या को हल करने के लिए, स्टैक करने की एक आसान तरकीब का इस्तेमाल करेंगे. हम 1x1 ग्रिड बना रहे हैं, जहां पंक्ति और कॉलम का ग्रिड एलियास [story] का है. साथ ही, हर स्टोरी ग्रिड आइटम में उस स्पेस पर दावा करने की कोशिश की जाएगी, जिसकी वजह से स्टैक बन जाएगा.

हाइलाइट किए गए कोड को अपने .user नियमों के सेट में जोड़ें:

.user {
  scroll-snap-align: start;
  scroll-snap-stop: always;
  display: grid;
  grid: [story] 1fr / [story] 1fr;
}

app/css/index.css में सबसे नीचे, यह नियम सेट जोड़ें:

.story {
  grid-area: story;
}

अब, एलिमेंट को फ़्लो से बाहर ले जाने वाले एब्सोलूट पोज़िशनिंग, फ़्लोट या अन्य लेआउट डायरेक्टिव के बिना भी, हम फ़्लो में ही हैं. साथ ही, इसमें बहुत कम कोड है, इस पर नज़र डालें! इस बारे में ज़्यादा जानकारी, वीडियो और ब्लॉग पोस्ट में दी गई है.

.story

अब हमें सिर्फ़ स्टोरी आइटम को स्टाइल करना है.

पहले हमने बताया था कि हर <article> एलिमेंट पर मौजूद style एट्रिब्यूट, प्लेसहोल्डर लोड करने की तकनीक का हिस्सा है:

<article class="story" style="--bg: url(https://picsum.photos/480/840);"></article>

हम सीएसएस की background-image प्रॉपर्टी का इस्तेमाल करेंगे. इसकी मदद से, एक से ज़्यादा बैकग्राउंड इमेज सेट की जा सकती हैं. हम उन्हें क्रम में लगा सकते हैं, ताकि उपयोगकर्ता की फ़ोटो सबसे ऊपर दिखे और लोड होने के बाद अपने-आप दिखे. इसे चालू करने के लिए, हम अपनी इमेज के यूआरएल को कस्टम प्रॉपर्टी (--bg) में डालेंगे. साथ ही, लोडिंग प्लेसहोल्डर के साथ लेयर बनाने के लिए, अपनी सीएसएस में इसका इस्तेमाल करेंगे.

सबसे पहले, .story नियमों का सेट अपडेट करें, ताकि ग्रेडिएंट लोड होने के बाद उसे बैकग्राउंड इमेज से बदला जा सके. हाइलाइट किए गए कोड को अपने .story नियमसेट में जोड़ें:

.story {
  grid-area: story;

  background-size: cover;
  background-image:
    var(--bg),
    linear-gradient(to top, lch(98 0 0), lch(90 0 0));
}

background-size को cover पर सेट करने से यह पक्का होता है कि व्यूपोर्ट में कोई खाली जगह न हो, क्योंकि हमारी इमेज उसे भर देगी. दो बैकग्राउंड इमेज तय करने पर, हम लोडिंग टॉम्बस्टोन नाम की एक बेहतरीन सीएसएस वेब ट्रिक का इस्तेमाल कर सकते हैं:

  • बैकग्राउंड इमेज 1 (var(--bg)) वह यूआरएल है जिसे हमने एचटीएमएल में इनलाइन किया है
  • बैकग्राउंड इमेज 2 (linear-gradient(to top, lch(98 0 0), lch(90 0 0)) एक ग्रेडिएंट है, जो यूआरएल लोड होने के दौरान दिखता है

इमेज डाउनलोड होने के बाद, सीएसएस ग्रेडिएंट को अपने-आप इमेज से बदल देगा.

इसके बाद, हम कुछ सीएसएस जोड़ेंगे, ताकि कुछ व्यवहार हटाया जा सके. इससे ब्राउज़र तेज़ी से काम कर पाएगा. हाइलाइट किए गए कोड को अपने .story नियमसेट में जोड़ें:

.story {
  grid-area: story;

  background-size: cover;
  background-image:
    var(--bg),
    linear-gradient(to top, lch(98 0 0), lch(90 0 0));

  user-select: none;
  touch-action: manipulation;
}
  • user-select: none, उपयोगकर्ताओं को गलती से टेक्स्ट चुनने से रोकता है
  • touch-action: manipulation, ब्राउज़र को निर्देश देता है कि इन इंटरैक्शन को टच इवेंट के तौर पर माना जाना चाहिए. इससे ब्राउज़र को यह तय करने की ज़रूरत नहीं पड़ती कि यूआरएल पर क्लिक किया जा रहा है या नहीं

आखिर में, एक से दूसरी स्टोरी पर स्विच करने के दौरान ऐनिमेशन जोड़ने के लिए, थोड़ी सी सीएसएस जोड़ें. हाइलाइट किए गए कोड को अपने .story नियमसेट में जोड़ें:

.story {
  grid-area: story;

  background-size: cover;
  background-image:
    var(--bg),
    linear-gradient(to top, lch(98 0 0), lch(90 0 0));

  user-select: none;
  touch-action: manipulation;

  transition: opacity .3s cubic-bezier(0.4, 0.0, 1, 1);

  &.seen {
    opacity: 0;
    pointer-events: none;
  }
}

.seen क्लास, उस स्टोरी में जोड़ी जाएगी जिसमें किसी एलिमेंट को हटाना है. मुझे कस्टम ईज़िंग फ़ंक्शन (cubic-bezier(0.4, 0.0, 1,1)) का पता, Material Design की ईज़िंग गाइड से चला. इसके लिए, तेज़ी से ईज़िंग सेक्शन पर स्क्रोल करें.

अगर आपने ध्यान से देखा है, तो आपको pointer-events: none एलान दिख गया होगा और शायद आप अब परेशान हो रहे हों. मेरा कहना है कि अब तक, यह इस समाधान का एकमात्र नुकसान है. हमें इसकी ज़रूरत है, क्योंकि .seen.story एलिमेंट सबसे ऊपर होगा और उस पर टैप मिलेंगे, भले ही वह न दिख रहा हो. pointer-events को none पर सेट करके, हम ग्लास स्टोरी को विंडो में बदल देते हैं. साथ ही, उपयोगकर्ता के इंटरैक्शन को भी नहीं चुराते. फ़िलहाल, हमारी सीएसएस में जाकर, इसे मैनेज करना ज़्यादा मुश्किल नहीं है. हम z-index को एक साथ नहीं ला रहे हैं. मुझे अब भी इस बारे में अच्छा लग रहा है.

JavaScript

स्टोरीज़ कॉम्पोनेंट के इंटरैक्शन, उपयोगकर्ता के लिए काफ़ी आसान होते हैं: आगे बढ़ने के लिए दाईं ओर टैप करें और पीछे जाने के लिए बाईं ओर टैप करें. उपयोगकर्ताओं के लिए आसान चीज़ें डेवलपर के लिए मेहनत की तरह होती हैं. हालांकि, हम इसकी ज़्यादातर जानकारी अपने पास सेव कर लेंगे.

सेटअप

शुरू करने के लिए, ज़्यादा से ज़्यादा जानकारी कैलकुलेट और सेव करें. app/js/index.js में यह कोड जोड़ें:

const stories = document.querySelector('.stories')
const median = stories.offsetLeft + (stories.clientWidth / 2)

JavaScript की हमारी पहली लाइन, हमारे मुख्य एचटीएमएल एलिमेंट रूट का रेफ़रंस लेती है और उसे सेव करती है. अगली लाइन यह हिसाब लगाती है कि हमारे एलिमेंट का बीच में कौनसा बिंदु है, ताकि हम यह तय कर सकें कि टैप करने पर, स्क्रीन पर आगे या पीछे जाना है.

स्थिति

इसके बाद, हम अपने लॉजिक के हिसाब से कुछ स्टेटस के साथ एक छोटा ऑब्जेक्ट बनाते हैं. इस मामले में, हमारी दिलचस्पी सिर्फ़ मौजूदा खबर में है. अपने एचटीएमएल मार्कअप में, हम इसे ऐक्सेस करने के लिए पहले दोस्त और उसकी हाल ही की कहानी की जानकारी हासिल कर सकते हैं. हाइलाइट किए गए कोड को अपने app/js/index.js में जोड़ें:

const stories = document.querySelector('.stories')
const median = stories.offsetLeft + (stories.clientWidth / 2)

const state = {
  current_story: stories.firstElementChild.lastElementChild
}

लिसनर

अब हमारे पास उपयोगकर्ता इवेंट को सुनने और उन्हें निर्देश देने के लिए ज़रूरी लॉजिक है.

चूहा

आइए, अपने स्टोरीज़ कंटेनर पर 'click' इवेंट के बारे में सुनकर शुरुआत करते हैं. हाइलाइट किए गए कोड को app/js/index.js में जोड़ें:

const stories = document.querySelector('.stories')
const median = stories.offsetLeft + (stories.clientWidth / 2)

const state = {
  current_story: stories.firstElementChild.lastElementChild
}

stories.addEventListener('click', e => {
  if (e.target.nodeName !== 'ARTICLE')
    return

  navigateStories(
    e.clientX > median
      ? 'next'
      : 'prev')
})

अगर कोई क्लिक होता है और वह <article> एलिमेंट पर नहीं होता है, तो हम बाहर निकल जाते हैं और कुछ नहीं करते. अगर यह कोई लेख है, तो हम clientX से माउस या उंगली की हॉरिज़ॉन्टल पोज़िशन लेते हैं. हमने अब तक navigateStories को लागू नहीं किया है. हालांकि, इसमें इस्तेमाल होने वाले आर्ग्युमेंट से पता चलता है कि हमें किस दिशा में जाना है. अगर उपयोगकर्ता की पोज़िशन, मीडियन से ज़्यादा है, तो हमें next पर जाना होगा. अगर नहीं, तो prev (पिछला) पर जाना होगा.

कीबोर्ड

अब, कीबोर्ड के बटन दबाने पर होने वाली आवाज़ को सुनने की सुविधा को चालू करते हैं. डाउन ऐरो दबाने पर, हम next पर जाएं. अगर यह अप ऐरो है, तो हम prev पर जाते हैं.

हाइलाइट किए गए कोड को app/js/index.js में जोड़ें:

const stories = document.querySelector('.stories')
const median = stories.offsetLeft + (stories.clientWidth / 2)

const state = {
  current_story: stories.firstElementChild.lastElementChild
}

stories.addEventListener('click', e => {
  if (e.target.nodeName !== 'ARTICLE')
    return

  navigateStories(
    e.clientX > median
      ? 'next'
      : 'prev')
})

document.addEventListener('keydown', ({key}) => {
  if (key !== 'ArrowDown' || key !== 'ArrowUp')
    navigateStories(
      key === 'ArrowDown'
        ? 'next'
        : 'prev')
})

स्टोरीज़ में नेविगेट करना

अब स्टोरीज़ के यूनीक बिज़नेस लॉजिक और यूज़र एक्सपीरियंस को समझने का समय आ गया है. यह अजीब और पेचीदा लग रहा है, लेकिन मुझे लगता है कि अगर आप एक-एक करके समीक्षा करें, तो आपको लगेगा कि यह आसानी से समझ आने वाला होता है.

सबसे पहले, हम कुछ सिलेक्टर को छिपा देते हैं, जिससे हमें यह तय करने में मदद मिलती है कि स्टोरी को स्क्रोल करना है या उसे दिखाना/छिपाना है. हम एचटीएमएल में काम करते हैं. इसलिए, हम दोस्तों (उपयोगकर्ताओं) या कहानियों (स्टोरी) की मौजूदगी के बारे में क्वेरी करेंगे.

इन वैरिएबल की मदद से, हमें इस तरह के सवालों के जवाब मिलेंगे, "x वाली स्टोरी के लिए, "आगे बढ़ें" का मतलब क्या है? क्या इसका मतलब है कि उसी दोस्त की किसी दूसरी स्टोरी पर जाना है या किसी दूसरे दोस्त की स्टोरी पर जाना है?" मैंने ऐसा, अपने बनाए गए ट्री स्ट्रक्चर का इस्तेमाल करके किया. इससे माता-पिता और उनके बच्चों तक पहुंचा जा सका.

app/js/index.js के नीचे यह कोड जोड़ें:

const navigateStories = direction => {
  const story = state.current_story
  const lastItemInUserStory = story.parentNode.firstElementChild
  const firstItemInUserStory = story.parentNode.lastElementChild
  const hasNextUserStory = story.parentElement.nextElementSibling
  const hasPrevUserStory = story.parentElement.previousElementSibling
}

यहां हमारे कारोबारी नियम का लक्ष्य बताया गया है, जो सामान्य भाषा के ज़्यादा से ज़्यादा करीब है:

  • टैप को मैनेज करने का तरीका तय करना
    • अगर कोई अगली/पिछली कहानी है: वह कहानी दिखाएँ:
    • अगर यह दोस्त की आखिरी/पहली कहानी है, तो नया दोस्त दिखाएं
    • अगर उस दिशा में कोई कहानी नहीं है, तो कुछ न करें
  • मौजूदा स्टोरी को state में स्टैश करना

अपने navigateStories फ़ंक्शन में हाइलाइट किया गया कोड जोड़ें:

const navigateStories = direction => {
  const story = state.current_story
  const lastItemInUserStory = story.parentNode.firstElementChild
  const firstItemInUserStory = story.parentNode.lastElementChild
  const hasNextUserStory = story.parentElement.nextElementSibling
  const hasPrevUserStory = story.parentElement.previousElementSibling

  if (direction === 'next') {
    if (lastItemInUserStory === story && !hasNextUserStory)
      return
    else if (lastItemInUserStory === story && hasNextUserStory) {
      state.current_story = story.parentElement.nextElementSibling.lastElementChild
      story.parentElement.nextElementSibling.scrollIntoView({
        behavior: 'smooth'
      })
    }
    else {
      story.classList.add('seen')
      state.current_story = story.previousElementSibling
    }
  }
  else if(direction === 'prev') {
    if (firstItemInUserStory === story && !hasPrevUserStory)
      return
    else if (firstItemInUserStory === story && hasPrevUserStory) {
      state.current_story = story.parentElement.previousElementSibling.firstElementChild
      story.parentElement.previousElementSibling.scrollIntoView({
        behavior: 'smooth'
      })
    }
    else {
      story.nextElementSibling.classList.remove('seen')
      state.current_story = story.nextElementSibling
    }
  }
}

इसे आज़माएं

  • साइट की झलक देखने के लिए, ऐप्लिकेशन देखें दबाएं. इसके बाद, फ़ुलस्क्रीनफ़ुलस्क्रीन दबाएं.

नतीजा

यह कॉम्पोनेंट से जुड़ी मेरी ज़रूरतों को पूरा करने वाला है. इस पर काम करें, डेटा का इस्तेमाल करें, और इसे अपने हिसाब से बनाएं!