Codelab ini mengajarkan cara membangun pengalaman seperti Instagram Stories di web. Kita akan membangun komponen seiring berjalannya waktu, dimulai dengan HTML, lalu CSS, kemudian JavaScript.
Lihat postingan blog saya Membangun komponen Stories untuk mempelajari progressive enhancement yang dilakukan saat membangun komponen ini.
Penyiapan
- Klik Remix to Edit untuk membuat project dapat diedit.
- Buka
app/index.html.
HTML
Saya selalu berupaya menggunakan HTML semantik.
Karena setiap teman dapat memiliki sejumlah cerita, saya rasa akan lebih bermakna jika menggunakan
elemen <section> untuk setiap teman dan elemen <article> untuk setiap cerita.
Namun, mari kita mulai dari awal. Pertama, kita memerlukan penampung untuk komponen cerita.
Tambahkan elemen <div> ke <body>:
<div class="stories">
</div>
Tambahkan beberapa elemen <section> untuk mewakili teman:
<div class="stories">
<section class="user"></section>
<section class="user"></section>
<section class="user"></section>
<section class="user"></section>
</div>
Tambahkan beberapa elemen <article> untuk mewakili cerita:
<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>
- Kami menggunakan layanan gambar (
picsum.com) untuk membantu membuat prototipe story. - Atribut
stylepada setiap<article>adalah bagian dari teknik pemuatan placeholder, yang akan Anda pelajari lebih lanjut di bagian berikutnya.
CSS
Konten kita sudah siap untuk diberi gaya. Mari kita ubah kerangka tersebut menjadi sesuatu yang menarik untuk berinteraksi. Kita akan mengerjakan desain yang memprioritaskan perangkat seluler hari ini.
.stories
Untuk penampung <div class="stories">, kita menginginkan penampung scroll horizontal.
Kita dapat mencapainya dengan:
- Menjadikan penampung sebagai Petak
- Menyetel setiap turunan untuk mengisi jalur baris
- Membuat lebar setiap turunan sama dengan lebar area pandang perangkat seluler
Petak akan terus menempatkan kolom baru selebar 100vw di sebelah kanan kolom sebelumnya, hingga semua elemen HTML ditempatkan di markup Anda.
Tambahkan CSS berikut ke bagian bawah app/css/index.css:
.stories {
display: grid;
grid: 1fr / auto-flow 100%;
gap: 1ch;
}
Setelah kita memiliki konten yang melampaui area pandang, saatnya memberi tahu
penampung tersebut cara menanganinya. Tambahkan baris kode yang ditandai ke ruleset .stories Anda:
.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;
}
Kita ingin scroll horizontal, jadi kita akan menetapkan overflow-x ke
auto. Saat pengguna men-scroll, kita ingin komponen berhenti perlahan di story berikutnya,
jadi kita akan menggunakan scroll-snap-type: x mandatory. Baca selengkapnya tentang CSS ini di bagian CSS Scroll Snap Points dan overscroll-behavior di postingan blog saya.
Penampung induk dan turunan harus menyetujui pengepasan posisi scroll, jadi
mari kita tangani hal itu sekarang. Tambahkan kode berikut ke bagian bawah app/css/index.css:
.user {
scroll-snap-align: start;
scroll-snap-stop: always;
}
Aplikasi Anda belum berfungsi, tetapi video di bawah menunjukkan apa yang terjadi saat
scroll-snap-type diaktifkan dan dinonaktifkan. Jika diaktifkan, setiap scroll horizontal
akan beralih ke artikel berikutnya. Jika dinonaktifkan, browser akan menggunakan
perilaku scroll defaultnya.
Dengan begitu, Anda dapat men-scroll daftar teman, tetapi kita masih memiliki masalah dengan story yang harus diselesaikan.
.user
Mari kita buat tata letak di bagian .user yang mengatur elemen cerita turunan tersebut
ke tempatnya. Kita akan menggunakan trik penumpukan yang praktis untuk mengatasi masalah ini.
Pada dasarnya, kita membuat petak 1x1 dengan baris dan kolom yang memiliki alias Grid
yang sama, yaitu [story], dan setiap item petak story akan mencoba mengklaim ruang tersebut,
sehingga menghasilkan tumpukan.
Tambahkan kode yang ditandai ke ruleset .user Anda:
.user {
scroll-snap-align: start;
scroll-snap-stop: always;
display: grid;
grid: [story] 1fr / [story] 1fr;
}
Tambahkan ruleset berikut ke bagian bawah app/css/index.css:
.story {
grid-area: story;
}
Sekarang, tanpa pemosisian absolut, float, atau perintah tata letak lainnya yang mengeluarkan elemen dari alur, kita masih berada dalam alur. Selain itu, hampir tidak ada kode, lihat itu! Hal ini diuraikan dalam video dan postingan blog secara lebih mendetail.
.story
Sekarang kita hanya perlu menata gaya item cerita itu sendiri.
Sebelumnya, kami menyebutkan bahwa atribut style pada setiap elemen <article> adalah bagian dari
teknik pemuatan placeholder:
<article class="story" style="--bg: url(https://picsum.photos/480/840);"></article>
Kita akan menggunakan properti background-image CSS, yang memungkinkan kita menentukan
lebih dari satu gambar latar. Kita dapat mengurutkannya sehingga gambar pengguna berada di atas dan akan muncul secara otomatis setelah selesai dimuat. Untuk
mengaktifkannya, kita akan menempatkan URL gambar ke dalam properti kustom (--bg), dan menggunakannya
dalam CSS untuk melapisi dengan placeholder pemuatan.
Pertama, mari kita perbarui ruleset .story untuk mengganti gradien dengan gambar latar
setelah selesai dimuat. Tambahkan kode yang ditandai ke ruleset .story Anda:
.story {
grid-area: story;
background-size: cover;
background-image:
var(--bg),
linear-gradient(to top, lch(98 0 0), lch(90 0 0));
}
Menetapkan background-size ke cover akan memastikan tidak ada ruang kosong di area pandang karena gambar kita akan mengisinya. Dengan menentukan 2 gambar latar,
kita dapat menggunakan trik web CSS yang rapi, yang disebut loading tombstone:
- Gambar latar 1 (
var(--bg)) adalah URL yang kita teruskan secara inline di HTML - Gambar latar 2 (
linear-gradient(to top, lch(98 0 0), lch(90 0 0))adalah gradien yang akan ditampilkan saat URL dimuat
CSS akan otomatis mengganti gradien dengan gambar, setelah gambar selesai didownload.
Selanjutnya, kita akan menambahkan beberapa CSS untuk menghapus beberapa perilaku, sehingga browser dapat bergerak lebih cepat.
Tambahkan kode yang ditandai ke ruleset .story Anda:
.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: nonemencegah pengguna memilih teks secara tidak sengajatouch-action: manipulationmenginstruksikan browser bahwa interaksi ini harus diperlakukan sebagai peristiwa sentuh, yang membebaskan browser dari upaya memutuskan apakah Anda mengklik URL atau tidak
Terakhir, mari kita tambahkan sedikit CSS untuk menganimasikan transisi antar-story. Tambahkan kode yang ditandai ke ruleset .story Anda:
.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;
}
}
Class .seen akan ditambahkan ke story yang memerlukan exit.
Saya mendapatkan fungsi easing kustom (cubic-bezier(0.4, 0.0, 1,1))
dari panduan Easing
Desain Material (scroll ke bagian Accelerated easing).
Jika Anda jeli, Anda mungkin melihat deklarasi pointer-events: none
dan menggaruk-garuk kepala sekarang. Menurut saya, ini adalah satu-satunya
kelemahan solusi ini sejauh ini. Kita memerlukan ini karena elemen .seen.story
akan berada di atas dan akan menerima ketukan, meskipun tidak terlihat. Dengan menyetel
pointer-events ke none, kita mengubah story kaca menjadi jendela, dan tidak lagi
mencuri interaksi pengguna. Kompromi yang tidak terlalu buruk, tidak terlalu sulit untuk dikelola di sini,
dalam CSS kita saat ini. Kami tidak melakukan juggling z-index. Saya masih merasa yakin dengan hal ini.
JavaScript
Interaksi komponen Stories cukup sederhana bagi pengguna: ketuk di sebelah kanan untuk maju, ketuk di sebelah kiri untuk kembali. Hal-hal sederhana bagi pengguna cenderung menjadi pekerjaan berat bagi developer. Namun, kami akan menangani banyak hal.
Penyiapan
Untuk memulai, mari kita hitung dan simpan informasi sebanyak mungkin.
Tambahkan kode berikut ke app/js/index.js:
const stories = document.querySelector('.stories')
const median = stories.offsetLeft + (stories.clientWidth / 2)
Baris pertama JavaScript mengambil dan menyimpan referensi ke root elemen HTML utama. Baris berikutnya menghitung posisi tengah elemen, sehingga kita dapat memutuskan apakah ketukan akan mengarah ke depan atau ke belakang.
Negara Bagian
Selanjutnya, kita membuat objek kecil dengan beberapa status yang relevan dengan logika kita. Dalam hal ini, kita hanya tertarik pada cerita saat ini. Dalam markup HTML, kita dapat
mengaksesnya dengan mengambil teman pertama dan story terbarunya. Tambahkan kode yang ditandai ke app/js/index.js:
const stories = document.querySelector('.stories')
const median = stories.offsetLeft + (stories.clientWidth / 2)
const state = {
current_story: stories.firstElementChild.lastElementChild
}
Pemroses
Sekarang kita memiliki logika yang cukup untuk mulai memproses peristiwa pengguna dan mengarahkannya.
Tikus
Mari kita mulai dengan memproses peristiwa 'click' di penampung Stories.
Tambahkan kode yang ditandai ke 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')
})
Jika klik terjadi dan tidak berada di elemen <article>, kita akan keluar dan tidak melakukan apa pun.
Jika berupa artikel, kita akan mengambil posisi horizontal mouse atau jari dengan
clientX. Kita belum menerapkan navigateStories, tetapi argumen yang
diperlukan menentukan arah yang harus kita tuju. Jika posisi pengguna tersebut
lebih besar dari median, kita tahu bahwa kita perlu membuka next, jika tidak,
prev (sebelumnya).
Keyboard
Sekarang, mari kita proses penekanan keyboard. Jika Panah Bawah ditekan, kita akan membuka
next. Jika tombolnya adalah Panah Atas, kita akan membuka prev.
Tambahkan kode yang ditandai ke 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')
})
Navigasi Stories
Saatnya menangani logika bisnis unik cerita dan UX yang membuatnya terkenal. Ini terlihat rumit dan sulit, tetapi jika Anda membacanya baris demi baris, Anda akan merasa bahwa ini cukup mudah dipahami.
Di awal, kita menyimpan beberapa pemilih yang membantu kita memutuskan apakah akan men-scroll ke teman atau menampilkan/menyembunyikan story. Karena kita bekerja di HTML, kita akan membuat kueri untuk kehadiran teman (pengguna) atau cerita (cerita).
Variabel ini akan membantu kita menjawab pertanyaan seperti, "dengan cerita x, apakah "berikutnya" berarti beralih ke cerita lain dari teman yang sama atau ke teman yang berbeda?" Saya melakukannya dengan menggunakan struktur hierarki yang kami buat, yang menjangkau induk dan turunannya.
Tambahkan kode berikut ke bagian bawah 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
}
Berikut adalah sasaran logika bisnis kita, yang sedekat mungkin dengan bahasa alami:
- Menentukan cara menangani ketukan
- Jika ada cerita berikutnya/sebelumnya: tampilkan cerita tersebut
- Jika ini adalah cerita terakhir/pertama teman: tampilkan teman baru
- Jika tidak ada cerita yang dapat dibuka ke arah tersebut: tidak melakukan apa pun
- Simpan cerita saat ini yang baru ke
state
Tambahkan kode yang ditandai ke fungsi navigateStories Anda:
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
}
}
}
Cobalah
- Untuk melihat pratinjau situs, tekan View App. Kemudian, tekan
Fullscreen
.
Kesimpulan
Itulah rangkuman kebutuhan saya terkait komponen. Jangan ragu untuk mengembangkannya, mengarahkannya dengan data, dan secara umum menjadikannya milik Anda.