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

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

इस कॉम्पोनेंट को बनाते समय किए गए प्रोग्रेसिव एन्हांसमेंट के बारे में जानने के लिए, मेरी ब्लॉग पोस्ट Building a Stories component पढ़ें.

सेटअप

  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 ruleset को अपडेट करते हैं, ताकि ग्रेडिएंट को बैकग्राउंड इमेज से बदला जा सके. ऐसा तब होगा, जब इमेज लोड हो जाएगी. हाइलाइट किए गए कोड को अपने .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 की Easing गाइड से मिला है. इसके लिए, Accerlerated easing सेक्शन पर जाएं.

अगर आपकी नज़र तेज़ है, तो आपने शायद 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
    }
  }
}

इसे आज़माएं

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

नतीजा

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