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
- Aby umożliwić edytowanie projektu, kliknij Zremiksuj do edycji.
- 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.
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 .
Podsumowanie
To podsumowanie naszych potrzeb związanych z tym komponentem. Możesz na niej korzystać, wykorzystywać dane i w ogóle tworzyć własne.