Ćwiczenia z programowania: tworzenie komponentu Relacje

Dzięki temu ćwiczeniu w Codelabs dowiesz się, jak stworzyć w internecie coś takiego jak relacje na Instagramie. Tworzymy komponent na bieżąco. Zacznijmy od HTML, potem CSS, a potem JavaScript.

Przeczytaj posta Building a Stories (Tworzenie komponentu historii), aby dowiedzieć się więcej o progresywnych ulepszeniach wprowadzonych podczas tworzenia tego komponentu.

Konfiguracja

  1. Aby umożliwić edytowanie projektu, kliknij Zremiksuj do edycji.
  2. Otwórz pokój app/index.html.

HTML

Zawsze staram się używać semantycznego kodu HTML. Każdy znajomy może mieć dowolną liczbę historii, dlatego uznałem, że warto użyć elementu <section> w przypadku każdego znajomego i elementu <article> w każdej relacji. Zacznijmy jednak od początku. Po pierwsze, potrzebny jest kontener na komponent artykułów.

Dodaj element <div> do <body>:

<div class="stories">

</div>

Dodaj elementy <section>, aby reprezentować znajomych:

<div class="stories">
  <section class="user"></section>
  <section class="user"></section>
  <section class="user"></section>
  <section class="user"></section>
</div>

Dodaj elementy <article> reprezentujące artykuły:

<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>
  • Używamy usługi obrazów (picsum.com), aby tworzyć prototypy artykułów.
  • Atrybut style w każdym elemencie <article> jest częścią metody wczytywania obiektów zastępczych, o której dowiesz się więcej w następnej sekcji.

CSS

Nasze treści są gotowe w doskonałym stylu. Zamień te kości w coś, z czym widzowie będą chcieli wchodzić w interakcję. Dziś będziemy pracować głównie na urządzeniach mobilnych.

.stories

W przypadku naszego kontenera <div class="stories"> potrzebujemy kontenera z przewijaniem poziomym. Możemy to osiągnąć poprzez:

  • Tworzenie siatki z kontenera
  • Ustawianie wypełniania ścieżki wiersza przez każdy element podrzędny
  • Ustawianie szerokości każdego dziecka na poziomie widocznego obszaru na urządzeniu mobilnym

W siatce nowe kolumny o szerokości 100vw będą nadal umieszczane po prawej stronie poprzedniej, dopóki nie umieścisz w znacznikach wszystkich elementów HTML.

Otwierają się Chrome i Narzędzia deweloperskie, z widoczną siatką pokazującą układ o pełnej szerokości
Narzędzia deweloperskie w Chrome pokazujące nadmiar kolumn siatki w poziomie powodują przewijanie w poziomie.

Na dole strony app/css/index.css dodaj ten kod CSS:

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

Teraz gdy zawartość wykracza poza widoczny obszar, czas zastanowić się, jak kontener ma ją obsługiwać. Dodaj zaznaczone wiersze kodu do zestawu reguł .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;
}

Chcemy używać przewijania w poziomie, więc ustawimy overflow-x na auto. Gdy użytkownik przewija stronę, chcemy, by komponent delikatnie spoczywał na następnej relacji, więc użyjemy scroll-snap-type: x mandatory. Więcej informacji o tym CSS znajdziesz w postach na moim blogu w sekcjach CSS Scroll Snap Points (Punkty przyciągania przewijania w CSS) i Zachowanie podczas przewijania w moim poście na blogu.

Wymagamy, aby kontener nadrzędny i elementy podrzędne zgodziły się na przewijanie, więc zajmijmy się tym teraz. Dodaj ten kod na końcu strony app/css/index.css:

.user {
  scroll-snap-align: start;
  scroll-snap-stop: always;
}

Twoja aplikacja jeszcze nie działa, ale na filmie poniżej pokazujemy, co się dzieje, gdy usługa scroll-snap-type jest włączona lub wyłączona. Po włączeniu każde przewijanie w poziomie przyciąga następny artykuł. Gdy ta opcja jest wyłączona, przeglądarka korzysta z domyślnego sposobu przewijania.

Pozwoli Ci to przeglądać listę znajomych, ale wciąż mamy problem z historiami do rozwiązania.

.user

Utwórzmy w sekcji .user układ, który uporządkuje elementy bajek podrzędnych. Skorzystamy z praktycznej sztuczki, aby rozwiązać ten problem. Tworzymy siatkę 1 x 1, w której wiersz i kolumna mają taki sam alias siatki, jaki ma [story], a każdy element siatki historii będzie próbował dostać to miejsce, co spowoduje utworzenie stosu.

Dodaj zaznaczony kod do zestawu reguł .user:

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

Dodaj ten zestaw reguł na dole tabeli app/css/index.css:

.story {
  grid-area: story;
}

Teraz bez pozycjonowania bezwzględnego, elementów swobodnych i innych dyrektyw układu, które wychodzą z elementu, idziemy dalej. No i spójrz na nie prawie żadnego kodu. Szczegółowo opisaliśmy to w filmie i poście na blogu.

.story

Teraz wystarczy, że określisz styl samej historii.

Wcześniej wspomnieliśmy, że atrybut style w każdym elemencie <article> jest częścią zastępczej techniki wczytywania:

<article class="story" style="--bg: url(https://picsum.photos/480/840);"></article>

Użyjemy właściwości CSS background-image, która umożliwia określenie więcej niż jednego obrazu tła. Możemy je ułożyć w taki sposób, aby obraz użytkownika był widoczny na górze i pojawiał się automatycznie po zakończeniu ładowania. Aby to włączyć, umieść adres URL obrazu w właściwości niestandardowej (--bg) i użyjemy go w naszym CSS, aby nałożyć go na obiekt zastępczy wczytywania.

Najpierw zaktualizujmy zestaw reguł .story, aby po zakończeniu ładowania gradient zastąpić obrazem tła. Dodaj zaznaczony kod do zestawu reguł .story:

.story {
  grid-area: story;

  background-size: cover;
  background-image:
    var(--bg),
    linear-gradient(to top, lch(98 0 0), lch(90 0 0));
}

Ustawienie wartości background-size na cover zapewnia, że w widocznym obszarze nie ma pustego miejsca, ponieważ zapełnia je obraz. Zdefiniowanie 2 obrazów tła pozwala nam zastosować sztuczkę internetową CSS nazywaną wczytywaniem elementów tombstone:

  • Obraz tła 1 (var(--bg)) to adres URL przekazany w kodzie HTML
  • Obraz tła 2 (linear-gradient(to top, lch(98 0 0), lch(90 0 0)) to gradient wyświetlany podczas wczytywania adresu URL)

Po zakończeniu pobierania obrazu CSS automatycznie zastąpi gradient.

Następnie dodamy kod CSS, aby usunąć pewne zachowania, aby przeglądarka mogła działać szybciej. Dodaj zaznaczony kod do zestawu reguł .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;
}
  • Funkcja user-select: none zapobiega przypadkowemu zaznaczaniu tekstu przez użytkowników
  • touch-action: manipulation informuje przeglądarkę, że te interakcje powinny być traktowane jako zdarzenia dotknięcia, dzięki czemu przeglądarka nie będzie mogła decydować o kliknięciu adresu URL

Na koniec dodajmy trochę CSS, by animować przejścia między artykułami. Dodaj zaznaczony kod do zestawu reguł .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;
  }
}

Klasa .seen zostanie dodana do artykułu, który wymaga zamknięcia. Funkcja wygładzania niestandardowego (cubic-bezier(0.4, 0.0, 1,1)) pochodzi z przewodnika Wygładzanie w interfejsie Material Design (przewiń do sekcji Wygładzanie wspomagane).

Jeśli masz skrupulatne oko, pewnie widzisz deklarację pointer-events: none i trafisz w głowę. To jest jak dotąd jedyna wada tego rozwiązania. Wymagamy tego, ponieważ element .seen.story będzie się znajdować na górze i będzie otrzymywać kliknięcia, mimo że jest niewidoczny. Gdy ustawisz właściwość pointer-events na none, szklana opowieść zmieni się w okno i nie przechwytuje więcej interakcji użytkowników. Mamy dla Ciebie kompromis. Na razie nie jest to trudne do zarządzania w naszej usłudze porównywania cen. Nie żonglujemy czasem z-index. Wciąż czuję się dobrze.

JavaScript

Interakcje z elementami relacji są dla użytkownika bardzo proste: kliknij po prawej, aby przejść dalej, lub po lewej, aby się cofnąć. Proste rzeczy dla użytkowników bywają ciężkie dla programistów. Zajmiemy się jednak wieloma sprawami.

Konfiguracja

Na początek oblicz i zapisujmy jak najwięcej informacji. Dodaj do pliku app/js/index.js ten kod:

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

Pierwszy wiersz kodu JavaScript pobiera i przechowuje odniesienie do głównego elementu HTML. Następna linia określa, gdzie znajduje się środek elementu, więc możemy zdecydować, czy kliknięcie ma przejść do przodu czy do tyłu.

Stan

Następnie tworzymy mały obiekt o stanie odpowiadającym naszej logice. W tym przypadku interesuje nas aktualna historia. W znacznikach HTML możemy uzyskać do nich dostęp, przechwytując pierwszego znajomego i jego najnowszą historię. Dodaj zaznaczony kod do elementu app/js/index.js:

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

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

Detektory

Mamy teraz wystarczającą ilość logiki, aby zacząć nasłuchiwać zdarzeń użytkowników i nimi kierować.

Mysz

Zacznijmy od wysłuchania wydarzenia 'click' w kontenerze relacji. Dodaj zaznaczony kod do aplikacji 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')
})

Jeśli kliknięcie zdarzy się, a nie jest związane z elementem <article>, pobierzemy kaucję i nie podejmiemy żadnych działań. W przypadku artykułu chwytamy za pomocą clientX pozycję myszy lub palca w poziomie. Nie zaimplementowaliśmy jeszcze funkcji navigateStories, ale argument, który bierze pod uwagę, wskazuje kierunek, w jakim powinniśmy iść. Jeśli pozycja użytkownika jest większa niż mediana, wiemy, że trzeba przejść do next. W przeciwnym razie prev (poprzednia)

Klawiatura

Teraz posłuchajmy naciśnięć klawiszy. Po naciśnięciu strzałki w dół przejdź do: next. Jeśli to strzałka w górę, wybierzemy prev.

Dodaj zaznaczony kod do aplikacji 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')
})

Nawigacja po relacjach

Czas zająć się unikalną logiką biznesową historii i wrażeniami użytkownika, z których się słyną. Może się to wydawać chaotyczne i trudne, ale jeśli porozmawiasz o nich po linijce, przekonasz się, że tekst jest przyswajalny.

Na początku zapisujemy selektory, które pomagają nam zdecydować, czy przewinąć do znajomego, czy pokazać/ukryć artykuł. Ponieważ pracujemy w języku HTML, w którym pracujemy, będziemy wysyłać do niego zapytania o znajomych (użytkowników) lub historie (artykuł).

Pomagają nam one odpowiedzieć na pytania w rodzaju: „Dane x, czy „Dalej” oznacza przejście do innej historii tego samego znajomego czy od znajomego?”. Skorzystałam z wbudowanej przez nas struktury drzewa, czyli kontaktując się z rodzicami i dziećmi.

Dodaj ten kod na końcu strony 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
}

Oto nasz cel logiki biznesowej, który jest jak najbardziej zbliżony do języka naturalnego:

  • Zdecyduj, jak chcesz obsługiwać dotknięcie.
    • Jeśli jest już kolejna/poprzednia historia: pokaż ją
    • Jeśli to ostatnia lub pierwsza historia znajomego, pokaż nowemu znajomemu.
    • Jeśli nie ma tematu, którym można by się kierować, nie rób nic
  • Przenieś nową bieżącą historię do: state

Dodaj zaznaczony kod do funkcji 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
    }
  }
}

Wypróbuj

  • Aby wyświetlić podgląd strony, kliknij Wyświetl aplikację, a potem Pełny ekran pełny ekran.

Podsumowanie

To podsumowanie naszych potrzeb związanych z tym komponentem. Możesz na niej korzystać, wykorzystywać dane i w ogóle tworzyć własne.