इस कोडलैब में, वेब पर Instagram Stories जैसा अनुभव बनाने का तरीका बताया गया है. हम एचटीएमएल से शुरू करके, सीएसएस और फिर JavaScript के साथ कॉम्पोनेंट बनाएंगे.
इस कॉम्पोनेंट को बनाते समय किए गए बेहतरीन सुधारों के बारे में जानने के लिए, मेरी ब्लॉग पोस्ट Stories कॉम्पोनेंट बनाना देखें.
सेटअप
- प्रोजेक्ट में बदलाव करने के लिए, बदलाव करने के लिए रीमिक्स करें पर क्लिक करें.
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
चौड़ाई वाले नए कॉलम तब तक डालता रहेगा, जब तक वह आपके मार्कअप में सभी एचटीएमएल एलिमेंट नहीं डाल देता.
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
}
}
}
इसे आज़माएं
- साइट की झलक देखने के लिए, ऐप्लिकेशन देखें दबाएं. इसके बाद, फ़ुलस्क्रीन दबाएं.
नतीजा
कॉम्पोनेंट से जुड़ी मेरी ज़रूरतें पूरी हो गई हैं. इस पर काम करें, डेटा का इस्तेमाल करें, और इसे अपने हिसाब से बनाएं!