Codelab: Membuat komponen Stories

Codelab ini mengajarkan cara membangun pengalaman seperti Instagram Stories di web. Kita akan membangun komponen sambil berjalan, dimulai dengan HTML, kemudian CSS, kemudian JavaScript.

Lihat postingan blog saya Membuat komponen Stories untuk mempelajari tentang peningkatan progresif yang dilakukan saat membangun komponen ini.

Penyiapan

  1. Klik Remix to Edit agar project dapat diedit.
  2. Buka app/index.html.

HTML

Saya selalu bertujuan menggunakan HTML semantik. Karena setiap teman dapat memiliki sejumlah cerita, saya pikir akan sangat bermanfaat untuk menggunakan Elemen <section> untuk setiap teman dan elemen <article> untuk setiap cerita. Mari kita mulai dari awal. Pertama, kita membutuhkan kontainer untuk komponen cerita.

Tambahkan elemen <div> ke <body> Anda:

<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 cerita.
  • Atribut style di setiap <article> adalah bagian dari pemuatan placeholder teknik ini, yang akan Anda pelajari lebih lanjut di bagian selanjutnya.

CSS

Konten kita sudah siap untuk gaya. Mari kita ubah tulang itu menjadi sesuatu yang akan dilihat banyak orang mereka gunakan. Hari ini kita akan menggunakan {i>mobile-first<i}.

.stories

Untuk penampung <div class="stories">, kita ingin container scroll horizontal. Kita dapat mencapainya dengan:

  • Membuat container menjadi Petak
  • Menyetel setiap turunan untuk mengisi baris baris
  • Membuat lebar setiap turunan selebar area pandang perangkat seluler

Petak akan terus menempatkan kolom baru selebar 100vw di sebelah kanan kolom sebelumnya satu, hingga semua elemen HTML ditempatkan di markup Anda.

Chrome dan DevTools terbuka dengan visual petak yang menampilkan tata letak lebar penuh
Chrome DevTools menampilkan overflow kolom petak, sehingga membuat scroller horizontal.

Tambahkan CSS berikut ke bagian bawah app/css/index.css:

.stories {
  display: grid;
  grid: 1fr / auto-flow 100%;
  gap: 1ch;
}

Sekarang setelah kita memiliki konten yang melampaui area pandang, saatnya untuk memberi tahu container terkait cara menanganinya. Tambahkan baris kode yang ditandai ke kumpulan aturan .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, sehingga kita akan menetapkan overflow-x ke auto. Saat pengguna men-scroll, kita ingin komponen beristirahat dengan lembut di cerita berikutnya, jadi kita akan menggunakan scroll-snap-type: x mandatory. Baca selengkapnya tentang hal ini CSS di Snap Points Scroll CSS dan perilaku-overscroll bagian postingan blog saya.

Penampung induk dan turunan perlu setuju untuk mengesampingkan scroll, jadi mari kita tangani 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 kolom horizontal scroll akan langsung menuju ke cerita berikutnya. Jika dinonaktifkan, browser akan menggunakan perilaku scroll default.

Anda akan menelusuri teman-teman Anda, tetapi kami masih mengalami masalah dengan cerita yang harus dipecahkan.

.user

Mari kita buat tata letak di bagian .user yang menangani cerita turunan tersebut elemen data pada tempatnya. Kita akan menggunakan trik menumpuk yang praktis untuk menyelesaikan ini. Pada dasarnya kita membuat kisi 1x1 di mana baris dan kolom memiliki {i>Grid<i} yang sama alias dari [story], dan setiap item petak story akan mencoba dan mengklaim ruang tersebut, yang menghasilkan stack.

Tambahkan kode yang ditandai ke kumpulan aturan .user Anda:

.user {
  scroll-snap-align: start;
  scroll-snap-stop: always;
  display: grid;
  grid: [story] 1fr / [story] 1fr;
}

Tambahkan kumpulan aturan berikut ke bagian bawah app/css/index.css:

.story {
  grid-area: story;
}

Sekarang, tanpa posisi mutlak, {i>float<i}, atau perintah tata letak lain yang membutuhkan elemen keluar dari {i>flow<i} (alur), kita masih berada di aliran. Selain itu, ini tidak seperti kode apa pun, lihat itu! Informasi ini diuraikan dalam video dan postingan blog secara lebih mendetail.

.story

Sekarang kita hanya perlu memberi gaya pada item cerita itu sendiri.

Sebelumnya, kita 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 bisa memasukkannya dalam urutan sehingga pengguna gambar ada di atas dan akan muncul secara otomatis setelah pemuatan selesai. Kepada mengaktifkan ini, kita akan menempatkan URL gambar ke dalam properti khusus (--bg), dan menggunakannya dalam CSS kita untuk dilapisi dengan placeholder pemuatan.

Pertama, mari kita perbarui kumpulan aturan .story untuk mengganti gradien dengan gambar latar setelah pemuatan selesai. Tambahkan kode yang ditandai ke kumpulan aturan .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));
}

Menyetel background-size ke cover akan memastikan tidak ada ruang kosong di area pandang karena gambar kita akan memenuhinya. Menentukan 2 gambar latar memungkinkan kita untuk menarik trik web CSS yang rapi yang disebut pemuatan tombstone:

  • Gambar latar 1 (var(--bg)) adalah URL yang kami teruskan sejajar dalam HTML
  • Gambar latar 2 (linear-gradient(to top, lch(98 0 0), lch(90 0 0)) adalah gradien yang ditampilkan saat URL sedang dimuat

CSS akan otomatis mengganti gradien dengan gambar, setelah gambar selesai didownload.

Berikutnya, kita akan menambahkan beberapa CSS untuk menghapus beberapa perilaku, sehingga browser dapat bergerak lebih cepat. Tambahkan kode yang ditandai ke kumpulan aturan .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: none mencegah pengguna memilih teks secara tidak sengaja
  • touch-action: manipulation menginstruksikan browser bahwa interaksi ini harus diperlakukan sebagai peristiwa sentuh, yang membebaskan browser dari mencoba memutuskan apakah Anda akan mengklik URL atau tidak

Terakhir, mari kita tambahkan sedikit CSS untuk menganimasikan transisi antar-cerita. Tambahkan kode yang ditandai ke kumpulan aturan .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 cerita yang memerlukan jalan keluar. Saya mendapatkan fungsi easing kustom (cubic-bezier(0.4, 0.0, 1,1)) dari Easing Desain Material panduan (scroll ke bagian Easing yang dipercepat).

Jika Anda memiliki mata yang tajam, Anda mungkin telah memperhatikan pointer-events: none dan sedang menggaruk-garuk kepala Anda sekarang. Saya akan mengatakan ini adalah satu-satunya kekurangan dari solusi sejauh ini. Kita membutuhkannya karena elemen .seen.story akan berada di atas dan akan menerima ketukan, meskipun tidak terlihat. Dengan menetapkan pointer-events untuk none, kita mengubah kisah kaca menjadi jendela, dan tidak mencuri lebih banyak interaksi pengguna. Keuntungannya tidak terlalu buruk, tidak terlalu sulit untuk dikelola di sini di CSS kita sekarang. Kami tidak berpindah-pindah z-index. Saya senang dengan hal ini diam.

JavaScript

Interaksi komponen Stories cukup sederhana bagi pengguna: ketuk kanan untuk maju, ketuk di sebelah kiri untuk kembali. Hal yang sederhana bagi pengguna cenderung menjadi kerja keras bagi pengembang. Meskipun begitu, kita akan berurusan dengan banyak hal.

Penyiapan

Untuk memulainya, mari kita hitung dan simpan informasi sebanyak yang kita bisa. Tambahkan kode berikut ke app/js/index.js:

const stories = document.querySelector('.stories')
const median = stories.offsetLeft + (stories.clientWidth / 2)

Baris pertama JavaScript kita mengambil dan menyimpan referensi ke HTML utama root elemen. Baris berikutnya menghitung di tengah elemen kita, jadi kita dapat memutuskan apakah ketukan akan maju atau mundur.

Negara Bagian

Selanjutnya, kita membuat objek kecil dengan beberapa status yang relevan dengan logika kita. Di sini kita hanya tertarik pada cerita saat ini. Pada markup HTML, kita bisa mengaksesnya dengan memilih teman pertama dan kisah terbaru mereka. Menambahkan kode yang ditandai ke app/js/index.js Anda:

const stories = document.querySelector('.stories')
const median = stories.offsetLeft + (stories.clientWidth / 2)

const state = {
  current_story: stories.firstElementChild.lastElementChild
}

Pemroses

Kita memiliki logika yang cukup untuk mulai memproses peristiwa pengguna dan mengarahkannya.

Tikus

Mari kita mulai dengan mendengarkan peristiwa 'click' di penampung story kita. 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 bukan pada elemen <article>, kami menjamin dan tidak melakukan apa pun. Jika itu adalah artikel, kita ambil posisi horizontal {i>mouse<i} atau jari dengan clientX. Kita belum mengimplementasikan navigateStories, tetapi argumen yang yang dibutuhkan untuk menentukan arah apa yang kita harus tuju. Jika posisi pengguna tersebut lebih besar dari median, kita tahu kita harus membuka next, jika tidak prev (sebelumnya).

Keyboard

Sekarang, mari kita dengarkan penekanan {i>keyboard<i}. Jika Panah Bawah ditekan, kita akan membuka ke next. Jika sasarannya 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 story

Saatnya menangani logika bisnis unik dari cerita dan UX yang telah menjadi objeknya yang terkenal. Ini terlihat tebal dan rumit, tetapi saya pikir jika Anda menjelaskannya , Anda akan merasa bahwa entri tersebut cukup mudah dicerna.

Di depan, kita menyimpan beberapa pemilih yang membantu kita memutuskan apakah akan menggulir ke teman atau tampilkan/sembunyikan sebuah cerita. Karena HTML adalah tempat kita bekerja, kita akan dengan kueri untuk mengetahui keberadaan teman (pengguna) atau cerita (cerita).

Variabel-variabel ini akan membantu kita menjawab pertanyaan seperti, "mengenai cerita x, apakah "berikutnya" pindah ke cerita lain dari teman yang sama atau kepada teman yang berbeda?" Saya melakukannya dengan menggunakan yang kita bangun, menjangkau orang tua dan anak-anak mereka.

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 tujuan logika bisnis kami, yang semirip mungkin dengan natural language:

  • Tentukan cara menangani ketukan
    • Jika ada cerita berikutnya/sebelumnya: tunjukkan cerita tersebut
    • Jika cerita terakhir/pertama tentang teman: tunjukkan teman baru
    • Jika tidak ada cerita untuk disampaikan ke arah itu: jangan lakukan apa pun
  • Simpan cerita baru saat ini 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 Lihat Aplikasi. Lalu tekan Layar penuh layar penuh.

Kesimpulan

Itu rangkuman untuk kebutuhan saya dengan komponen tersebut. Jangan ragu untuk membangun itu, mengemudikannya dengan data, dan secara umum membuatnya menjadi milik Anda!