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

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

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

सेटअप

  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)) मिला है (स्क्रोल करके एक्रेटेड ईज़िंग सेक्शन पर जाएं).

ध्यान से देखने पर, शायद आपने pointer-events: none एलान देखा हो और इस समय आपका सिर खुजा रहा हो. मैं यह कह सकती हूं कि इस समस्या को हल करने में सिर्फ़ यही समस्या थी. हमें इसकी ज़रूरत इसलिए है, क्योंकि .seen.story एलिमेंट सबसे ऊपर होगा और न दिखने पर भी उसे टैप किया जाएगा. pointer-events को none पर सेट करके, हम कांच की कहानी को एक विंडो में बदल देते हैं और उपयोगकर्ताओं से इंटरैक्शन करने की कोई ज़रूरत नहीं पड़ती. अभी हमारे CSS में इसे मैनेज करना बहुत मुश्किल नहीं है और न ही इसे आपके लिए मैनेज करना बहुत मुश्किल है. हम 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
    }
  }
}

इसे आज़माएं

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

नतीजा

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