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

इस कोडलैब में, वेब पर 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
    }
  }
}

इसे आज़माएं

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

नतीजा

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