Z tego kursu dowiesz się, jak tworzyć w internecie treści podobne do Instagram Stories. Będziemy tworzyć komponent na bieżąco, zaczynając od HTML, potem CSS, a na końcu JavaScript.
Informacje o postępujących ulepszeniach wprowadzanych podczas tworzenia tego komponentu znajdziesz w poście na blogu o tworzeniu komponentu Relacje.
Konfiguracja
- Aby udostępnić projekt do edycji, kliknij Remiksuj, aby edytować.
- Otwórz pokój
app/index.html
.
HTML
Zawsze staram się używać semantycznych znaczników HTML.
Ponieważ każdy znajomy może mieć dowolną liczbę historii, uznałem, że warto użyć elementu <section>
dla każdego znajomego i elementu <article>
dla każdej historii.
Zacznijmy jednak od początku. Najpierw potrzebujemy kontenera dla komponentu relacji.
Dodaj element <div>
do elementu <body>
:
<div class="stories">
</div>
Dodaj elementy <section>
, które będą 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>
, aby reprezentować historie:
<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>
- Do tworzenia prototypów historii używamy usługi dotyczącej obrazów (
picsum.com
). - Atrybut
style
w każdym elemencie<article>
jest częścią metody wczytywania zastępczego, o której dowiesz się więcej w następnej sekcji.
CSS
Nasze treści są zawsze w dobrym stylu. Przekształcmy te kości w coś, z czym użytkownicy będą chcieli wchodzić w interakcje. Już dziś skupimy się na urządzeniach mobilnych.
.stories
W przypadku kontenera <div class="stories">
chcemy użyć poziomego kontenera z przewijaniem.
Możemy to osiągnąć, wykonując te czynności:
- Utworzenie kontenera jako siatki
- Ustawienie każdego dziecka w celu wypełnienia ścieżki wiersza
- Ustaw szerokość każdego elementu potomnego na szerokość obszaru widoku urządzenia mobilnego.
Użyta siatka będzie umieszczać nowe kolumny o szerokości 100vw
po prawej stronie poprzedniej, aż do umieszczenia wszystkich elementów HTML w sprawie.
Dodaj ten kod CSS na dole pliku app/css/index.css
:
.stories {
display: grid;
grid: 1fr / auto-flow 100%;
gap: 1ch;
}
Teraz, gdy mamy już zawartość wykraczającą poza widoczny obszar, musimy poinformować kontener, jak ma sobie z tym poradzić. Dodaj wyróżnione wiersze kodu do 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 przewijać w poziomie, więc ustawiamy wartość overflow-x
na auto
. Gdy użytkownik przewija stronę, chcemy, aby komponent delikatnie przylegał do następnego artykułu, dlatego użyjemy właściwości scroll-snap-type: x mandatory
. Więcej informacji na ten temat znajdziesz w postach na blogu w sekcjach Punkty zaczepienia przewijania CSS i overscroll-behavior.
Zarówno kontener nadrzędny, jak i elementy podrzędne muszą wyrazić zgodę na przyciąganie przewijania, więc zajmiemy się tym teraz. Dodaj ten kod na dole pliku app/css/index.css
:
.user {
scroll-snap-align: start;
scroll-snap-stop: always;
}
Twoja aplikacja jeszcze nie działa, ale z filmu poniżej dowiesz się, co się dzieje, gdy włączysz i wyłączysz funkcję scroll-snap-type
. Gdy ta opcja jest włączona, każde przewinięcie
w poziomie powoduje przejście do następnego artykułu. Gdy ta opcja jest wyłączona, przeglądarka używa domyślnego zachowania przewijania.
Możesz przeglądać listę znajomych, ale wciąż mamy problem z historią do rozwiązania.
.user
Utwórzmy w sekcji .user
układ, który umieści te podrzędne elementy osi czasu. Aby to zrobić, użyjemy przydatnego triku.
Tworzymy w istocie siatkę 1 x 1, w której wiersz i kolumna mają ten sam alias siatki [story]
, a każdy element siatki w historii będzie próbował zająć tę przestrzeń, co spowoduje utworzenie stosu.
Dodaj wyróżniony kod do reguł .user
:
.user {
scroll-snap-align: start;
scroll-snap-stop: always;
display: grid;
grid: [story] 1fr / [story] 1fr;
}
Dodaj te reguły na dole pliku app/css/index.css
:
.story {
grid-area: story;
}
Bez pozycjonowania bezwzględnego, elementów pływających i innych dyrektyw układu, które wyciągają element poza przepływ, nadal jesteśmy w przepływie. Poza tym prawie nie ma kodu. Więcej informacji znajdziesz w filmie i poście na blogu.
.story
Teraz musimy nadać styl samemu elementowi historii.
Wspominaliśmy już, że atrybut style
w każdym elemencie <article>
jest częścią techniki ładowania zastępników:
<article class="story" style="--bg: url(https://picsum.photos/480/840);"></article>
Użyjemy właściwości background-image
w CSS, która pozwala nam określić więcej niż 1 obraz tła. Możemy je ułożyć w takiej kolejności, by obraz użytkownika
znajdował się na górze, a po wczytaniu pojawiał się automatycznie. Aby to umożliwić, umieścimy adres URL obrazu w właściwości niestandardowej (--bg
) i użyjemy go w plikach CSS, aby nałożyć go na element zastępczy ładowania.
Najpierw zaktualizuj reguły .story
, aby zastąpić gradient obrazem tła. Zrobisz to po zakończeniu wczytywania. Dodaj wyróżniony kod do 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
sprawia, że w widoku nie ma pustego miejsca, ponieważ obraz wypełnia cały obszar. Zdefiniowanie 2 obrazów tła umożliwia nam użycie fajnego triku CSS o nazwie loading tombstone (ładowanie nagrobka):
- Obraz tła 1 (
var(--bg)
) to adres URL podany w kodzie HTML. - Obraz tła 2 (
linear-gradient(to top, lch(98 0 0), lch(90 0 0))
to gradient) do wyświetlania podczas wczytywania adresu URL
Po zakończeniu pobierania obrazu CSS automatycznie zastąpi gradient obrazem.
Dodamy kod CSS, by usunąć pewne zachowania i zwiększyć szybkość działania przeglądarki.
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;
}
user-select: none
uniemożliwia użytkownikom przypadkowe zaznaczanie tekstutouch-action: manipulation
informuje przeglądarkę, że te interakcje powinny być traktowane jako zdarzenia dotykowe, co uwalnia przeglądarkę od próby określenia, czy klikasz adres URL.
Na koniec dodamy trochę kodu CSS, aby animować przejście między opowieściami. Dodaj wyróżniony 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 historii, która wymaga zakończenia.
Funkcję wygładzania niestandardowego (cubic-bezier(0.4, 0.0, 1,1)
) uzyskałem z instrukcji wygładzania w Material Design (przewiń do sekcji Wygładzanie przyspieszone).
Jeśli masz bystry wzrok, prawdopodobnie zauważysz deklarację pointer-events: none
i zaczniesz się zastanawiać, co to oznacza. To chyba jedyna wada tego rozwiązania. Potrzebujemy tego, ponieważ element .seen.story
będzie widoczny na górze i będzie otrzymywać kliknięcia, mimo że jest niewidoczny. Ustawiając pointer-events
na none
, zmieniamy szklankę w okno i nie powodujemy już kradzieży interakcji z użytkownikiem. To nie jest zbyt duży kompromis, a zarządzanie w naszej usłudze porównywania cen nie jest obecnie zbyt trudne. Nie żonglujemy urządzeniem z-index
. Nadal jestem zadowolony z tego.
JavaScript
Interakcje z komponentem Stories są dla użytkownika bardzo proste: do przesuwania do przodu należy dotykać prawej strony, a do cofnięcia – lewej. Proste rzeczy dla użytkowników często wymagają od deweloperów sporo pracy. My zajmiemy się większością z nich.
Konfiguracja
Na początek zapiszmy i przechowujemy 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 odwołanie do głównego elementu HTML. W następnej linii znajduje się środek elementu, dzięki czemu możemy zdecydować, czy kliknięcie ma przejść do przodu czy do tyłu.
Stan
Następnie tworzymy mały obiekt z niektórymi stanami odpowiednimi dla naszej logiki. W tym przypadku interesuje nas tylko bieżąca historia. W naszym znaczniku HTML możemy uzyskać dostęp do pierwszego znajomego i jego najnowszej historii, Dodaj wyróżniony kod do pliku 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 już wystarczającą logikę, aby zacząć nasłuchiwać zdarzeń użytkownika i je kierować.
Mysz
Zacznijmy od nasłuchiwania zdarzenia 'click'
w kontenerze historii.
Dodaj wyróżniony kod do pliku 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 nie dotyczy elementu <article>
, nie wykonujemy żadnych działań.
Jeśli jest to artykuł, clientX
przechwytuje pozycję poziomą myszy lub palca. Nie wdrożyliśmy jeszcze funkcji navigateStories
, ale jej argument określa kierunek, w którym powinniśmy iść. Jeśli pozycja użytkownika jest wyższa niż mediana, wiemy, że musimy przejść do next
. W przeciwnym razie prev
(poprzednio).
Klawiatura
Teraz posłuchajmy, jak działają klawisze. Po naciśnięciu strzałki w dół przechodzimy do next
. Jeśli jest to strzałka w górę, przechodzimy do prev
.
Dodaj wyróżniony kod do pliku 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 w relacjach
Czas zająć się wyjątkową logiką biznesową historii i UX, z którego są sławne. Wydaje się to ciężkie i skomplikowane, ale myślę, że jeśli po kolei pisze się kolejne linijki, uda Ci się zwięźle.
Na początku wczytujemy selektory, które pomagają nam zdecydować, czy przewinąć do znajomego, czy pokazać lub ukryć historię. Pracujemy nad kodem HTML, więc zapytamy go o obecność znajomych (użytkowników) lub artykułów (artykułów).
Te zmienne pomogą nam uzyskać odpowiedzi na pytania w rodzaju „Czy w przypadku danej relacji x polecenie „dalej” oznacza przejście do innej relacji tego samego znajomego czy do relacji innego znajomego?”. Zrobiliśmy to, korzystając z naszej hierarchicznej struktury, która obejmuje rodziców i ich dzieci.
Dodaj ten kod na dole pliku 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 związany z logiką biznesową, maksymalnie zbliżony do języka naturalnego:
- Zdecyduj, jak obsłużyć kliknięcie.
- Jeśli jest następny lub poprzedni artykuł: pokaż go.
- Jeśli jest to ostatnia/pierwsza historia znajomego: pokaż nowego znajomego
- Jeśli nie ma żadnej historii, która mogłaby być wykorzystana w tym kierunku: nie rób nic.
- Ukryj nową bieżącą relację w
state
Dodaj wyróżniony 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ę. Następnie kliknij Pełny ekran.
Podsumowanie
To już nasze ogólne potrzeby dotyczące tego komponentu. Możesz go rozwijać, uzupełniać o dane i ogółem wykorzystywać do własnych celów.