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.
Aby dowiedzieć się więcej o ulepszeniach wprowadzonych podczas tworzenia tego komponentu, przeczytaj ten post na blogu.
Konfiguracja
- Kliknij Remixuj do edycji, aby umożliwić edycję projektu.
- 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 obiektu <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>
- Korzystamy z usługi dotyczącej obrazów (
picsum.com
), aby tworzyć prototypy historii. - Atrybut
style
w każdym elemencie<article>
jest częścią techniki wczytywania za pomocą placeholderów, o której dowiesz się więcej w następnej sekcji.
CSS
Treści są gotowe do stylizacji. Przekształcmy te kości w coś, z czym użytkownicy będą chcieli wchodzić w interakcje. Dzisiaj będziemy pracować nad optymalizacją pod kątem urządzeń 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 przewijania poziomego, więc ustawimy overflow-x
na
auto
. Gdy użytkownik przewija ekran, chcemy, aby komponent delikatnie przesuwał się na następną historię, dlatego użyjemy 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.
Aby włączyć przewijanie z przyspieszeniem, musisz wyrazić zgodę zarówno w kontenerze nadrzędnym, jak i w kontenerach podrzędnych. 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 na filmie poniżej możesz zobaczyć, co się dzieje, gdy włączysz i wyłączysz funkcję scroll-snap-type
. Gdy ta opcja jest włączona, przewijanie poziome powoduje przejście do następnej historii. Gdy ta opcja jest wyłączona, przeglądarka używa domyślnego zachowania przewijania.
Dzięki temu będziesz mieć dostęp do listy znajomych, ale nadal mamy problem z historią.
.user
Utwórzmy układ w sekcji .user
, 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 z przepływu, 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 ustawić w takim porządku, aby zdjęcie użytkownika było na górze i po załadowaniu wyświetlało 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. 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):
- Background image 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.
Następnie dodamy trochę kodu CSS, aby usunąć niektóre zachowania, co pozwoli przeglądarce działać szybciej.
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));
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 opowiadaniami. 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 relacji, 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ę na 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 rzucamy się z poziomu na poziom 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 obliczmy i przechowujmy 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. Następny wiersz oblicza środek elementu, dzięki czemu możemy zdecydować, czy kliknięcie ma przesunąć element 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 tagach 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ąco dużo logiki, 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. Funkcji navigateStories
jeszcze nie wdrożyliśmy, ale jej argument określa, w jakim kierunku mamy się poruszać. Jeśli pozycja użytkownika jest większa niż mediana, wiemy, że musimy przejść do next
, w przeciwnym razie do prev
(poprzedni).
Klawiatura
Teraz posłuchajmy, jak działają klawisze. Jeśli naciśniesz strzałkę w dół, przejdziesz 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 na zajęcie się unikalną logiką biznesową relacji i UX, z których słyną. Wygląda to na skomplikowane, ale jeśli przeczytasz to zdanie po zdaniu, powinno być zrozumiałe.
Na początku wczytujemy selektory, które pomagają nam zdecydować, czy przewinąć do znajomego, czy pokazać lub ukryć historię. Ponieważ pracujemy w języku HTML, będziemy wysyłać zapytania o obecność znajomych (użytkowników) lub relacji (relacji).
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 biznesowy, sformułowany w jak najbardziej zbliżony do języka naturalnego sposób:
- Zdecyduj, jak obsłużyć kliknięcie.
- Jeśli jest kolejna/poprzednia relacja: wyświetl tę relację.
- 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 wszystko, co chciałam powiedzieć na temat tego komponentu. Możesz go rozwijać, wykorzystywać w nim dane i ogółem robić z nim, co chcesz.