Blok 2 · rozdział 5

Układy Blade i komponenty

Menu ma być w jednym pliku, karta szczytu na liście też. Służą do tego układ (@extends) i komponent Blade (<x-karta-szczytu>). W tym rozdziale przejdziesz też składnię Blade: warunki, kilka pętli i @switch.

Po tym rozdziale powinieneś umieć

  • złożyć układ z miejscem @yield('content');
  • podpiąć własny CSS z katalogu public;
  • przełączyć listę i kartę na @extends;
  • użyć w widoku @if, @foreach, @for, @while i @switch oraz powiedzieć, czym @forelse różni się od @foreach;
  • wyjąć powtarzany HTML do komponentu anonimowego i przekazać mu dane przez @props.

1. Układ strony

Układ nazywamy korona, nie app. W bloku 4 Breeze utworzy własny layouts/app.blade.php. Osobna nazwa chroni dzisiejszą pracę przed nadpisaniem.

Plik resources/views/layouts/korona.blade.php:

<!DOCTYPE html>
<html lang="pl">
<head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <title>@yield('tytul', 'Korona Gór Polski')</title>
    <link rel="stylesheet" href="{{ asset('css/korona.css') }}">
</head>
<body>
    <header class="pasek">
        <div class="pasek-wnetrze">
            <a class="znak" href="{{ route('start') }}">Korona Gór Polski</a>
            <nav class="menu">
                <a href="{{ route('start') }}">Start</a>
                <a href="{{ route('szczyty.index') }}">Szczyty</a>
            </nav>
        </div>
    </header>
    <main class="strona">
        @yield('content')
    </main>
</body>
</html>

@yield('content') to miejsce na środek strony. Widok potomny wstawi tu swój HTML. @yield('tytul', 'Korona Gór Polski') ma drugi argument: tytuł zapasowy. Gdy potomek nie poda sekcji tytul, w karcie przeglądarki zostaje „Korona Gór Polski”.

Układ (layout) Ramka HTML wspólna dla wielu stron: nagłówek, menu, miejsce na treść. Sam układ nie zna listy szczytów. Dostaje gotowy środek od widoku, który go rozszerza.
asset() Składa adres pliku z katalogu public. Przy artisan serve asset('css/korona.css') wskazuje na http://127.0.0.1:8000/css/korona.css.

2. Plik CSS

Skopiuj materialy/korona.css z folderu kursu do C:\laragon\www\korona\public\css\korona.css (katalog css utwórz sam). To zwykły arkusz, bez Tailwind i bez Node. Node dojdzie dopiero z Breeze.

Po zapisaniu pliku odśwież dowolną stronę, która już ma układ, i w źródle HTML sprawdź adres w <link>. Ma prowadzić do http://127.0.0.1:8000/css/korona.css, a otwarty bezpośrednio ma pokazać treść arkusza, nie 404. Klasy z tego pliku, których użyjesz zaraz, to pasek, menu, strona, siatka i karta.

3. Widoki dziedziczą układ

Lista, po skróceniu, zaczyna się tak:

@extends('layouts.korona')

@section('tytul', 'Szczyty')

@section('content')
    <h1>Szczyty Korony</h1>
    <div class="siatka">
        @forelse ($szczyty as $szczyt)
            <article class="karta">
                <h2>
                    <a href="{{ route('szczyty.show', $szczyt['slug']) }}">{{ $szczyt['nazwa'] }}</a>
                </h2>
                <p class="meta">{{ $szczyt['pasmo'] }} · {{ $szczyt['wysokosc'] }} m</p>
            </article>
        @empty
            <p>Brak szczytów na liście.</p>
        @endforelse
    </div>
@endsection

@extends wybiera ramkę. Są dwie postaci sekcji. Krótka, @section('tytul', 'Szczyty'), wstawia jedną wartość i nie ma @endsection. Długa, @section('content') … @endsection, wstawia cały blok HTML w miejsce @yield('content').

Artykuł w pętli jest na razie zwykłym HTML-em. W podrozdziale 5 podmienisz go na komponent, żeby lista i ewentualna druga lista nie kopiowały tego samego znacznika.

resources/views/start.blade.php po przepięciu nie ma własnego <html>:

@extends('layouts.korona')

@section('tytul', 'Start')

@section('content')
    <h1>Korona Gór Polski</h1>
    <p class="lead">Lista szczytów, które zbierasz w tej aplikacji.</p>
    <p><a href="{{ route('szczyty.index') }}">Przejdź do szczytów</a></p>
@endsection

To samo zrób w szczyty/show.blade.php. Zostaje środek, ramkę daje układ:

@extends('layouts.korona')

@section('tytul')
{{ $szczyt['nazwa'] }}
@endsection

@section('content')
    <p><a href="{{ route('szczyty.index') }}">Wróć do listy</a></p>
    <h1>{{ $szczyt['nazwa'] }}</h1>
    <p>{{ $szczyt['pasmo'] }} · {{ $szczyt['wysokosc'] }} m n.p.m.</p>
@endsection

Odśwież /, /szczyty i /szczyty/rysy. Na każdej stronie ma być ten sam zielony pasek z napisem „Korona Gór Polski”. W źródle strony nie ma dwóch znaczników <html>.

@include a komponent @include('fragment') wkleja inny widok i widzi zmienne rodzica. Komponent ma jawne wejście: dostaje tylko to, co mu przekażesz. Przy karcie szczytu bierz komponent.

4. Składnia Blade

W rozdziale 3 zostały {{ }}, @if i @forelse. Tu przejdziesz resztę konstrukcji, które czytasz w cudzych widokach i których sam użyjesz przy formularzu. Lista Korony zostaje przy @forelse. Przykłady lądują na osobnej stronie, żeby dało się je odświeżyć i wyrzucić bez ruszania kart szczytów.

W kontrolerze dopisz metodę. Korzysta z tej samej tablicy co lista:

public function skladnia()
{
    return view('skladnia', [
        'szczyty' => $this->listaSzczytow(),
    ]);
}

W routes/web.php, obok tras szczytów:

Route::get('/skladnia', [SzczytController::class, 'skladnia'])->name('skladnia');

Utwórz resources/views/skladnia.blade.php. Każdy kolejny fragment wklejaj przed @endsection. Po każdym zapisie odśwież http://127.0.0.1:8000/skladnia. W źródle strony mają być gotowe listy i akapity. Dyrektyw @if ani @foreach tam nie ma — Laravel zamienił je na PHP, zanim HTML wyszedł z serwera.

@extends('layouts.korona')

@section('tytul', 'Składnia Blade')

@section('content')
    <h1>Składnia Blade</h1>
@endsection

@if, @elseif, @else, @unless

@if wypisuje tę gałąź, której warunek jest prawdziwy. @elseif sprawdza kolejny warunek, gdy wcześniejsze nie pasowały. @else obejmuje pozostałe przypadki. @unless działa odwrotnie niż @if: treść pokazuje się, gdy warunek nie jest spełniony.

@php
    $wysokosc = 1725;
@endphp

<h2>Warunki</h2>
@if ($wysokosc >= 2000)
    <p>{{ $wysokosc }} m — próg dwóch tysięcy.</p>
@elseif ($wysokosc >= 1000)
    <p>{{ $wysokosc }} m — od 1000 do 1999.</p>
@else
    <p>{{ $wysokosc }} m — poniżej 1000.</p>
@endif

@unless ($wysokosc >= 2000)
    <p>@unless: to nie jest próg 2000 m.</p>
@endunless

Przy 1725 widzisz zdanie „od 1000 do 1999” i zdanie z @unless. Zmień $wysokosc na 2499, odśwież: zostaje pierwsza gałąź, a akapit z @unless znika. Potem możesz wrócić do 1725.

@php w widoku @php … @endphp wstawia zwykły PHP. Tu służy tylko przykładowi, żeby nie dokładać trasy dla jednej liczby. Liczenie listy, szukanie slugu i walidacja zostają w kontrolerze. Widok Korony nie obrasta takimi blokami.

@isset i @empty

@isset($zmienna) wchodzi, gdy zmienna istnieje i nie jest null. @empty($zmienna) wchodzi, gdy jest pusta: null, "", 0 albo pusta tablica. Gdy szczyt nie ma jeszcze opisu, @isset nic nie wypisuje.

@php
    $opis = null;
@endphp

<h2>isset i empty</h2>
@isset($opis)
    <p>Opis: {{ $opis }}</p>
@else
    <p>Brak opisu — @isset nie wszedł.</p>
@endisset

@empty($opis)
    <p>@empty też widzi null.</p>
@endempty

@foreach i $loop

@foreach przechodzi po tablicy tak jak foreach w PHP. Wewnątrz pętli Blade dodaje obiekt $loop. Numeru wiersza nie liczysz sam.

<h2>@foreach</h2>
<ol>
    @foreach ($szczyty as $szczyt)
        <li>
            {{ $loop->iteration }}. {{ $szczyt['nazwa'] }}
            @if ($loop->first) — pierwszy @endif
            @if ($loop->last) — ostatni @endif
        </li>
    @endforeach
</ol>

Na stronie powinny być cztery pozycje. Przy Rysach dopisek „pierwszy”, przy Łysicy „ostatni”. $loop->iteration liczy od 1, $loop->index od 0, $loop->count to długość tablicy.

@continue przeskakuje do następnego obrotu. @break wychodzi z pętli. To te same słowa co w PHP, z małpą na początku, żeby Blade ich nie wypisał jako tekst.

<h2>Tylko od 1000 m</h2>
<ul>
    @foreach ($szczyty as $szczyt)
        @if ($szczyt['wysokosc'] < 1000)
            @continue
        @endif
        <li>{{ $szczyt['nazwa'] }}</li>
    @endforeach
</ul>

Na liście są Rysy, Babia Góra i Śnieżka. Łysica (614 m) wypada przez @continue.

@forelse a @foreach

@foreach po pustej tablicy nie wypisuje nic. Nie ma gdzie dać zdania „brak danych”. @forelse ma gałąź @empty na ten przypadek. Dlatego lista szczytów w projekcie używa @forelse, a nie gołego @foreach.

<h2>Pusta tablica</h2>
@foreach ([] as $pozycja)
    <p>Tego akapitu nie będzie.</p>
@endforeach

@forelse ([] as $pozycja)
    <p>{{ $pozycja }}</p>
@empty
    <p>@forelse: lista jest pusta, więc widzisz tę gałąź.</p>
@endforelse

@for

@for liczy sam, bez tablicy. Ma te same trzy części co pętla for w PHP: start, warunek, krok. Do wypisania szczytów i tak bierzesz @foreach. @for zostawiasz, gdy znasz liczbę obrotów, a nie kolekcję.

<h2>@for</h2>
<ul>
    @for ($nr = 1; $nr <= 3; $nr++)
        <li>Miejsce {{ $nr }}</li>
    @endfor
</ul>

Trzy pozycje: Miejsce 1, Miejsce 2, Miejsce 3.

@while

@while kręci się, dopóki warunek jest prawdziwy. Sam nie przechodzi do następnego elementu tablicy — musisz zmienić zmienną w środku. Gdy o tym zapomnisz, warunek zostaje prawdziwy i strona wisi, aż PHP przerwie skrypt limitem czasu.

<h2>@while</h2>
@php
    $zostalo = 3;
@endphp
<ul>
    @while ($zostalo > 0)
        <li>Zostaje {{ $zostalo }}</li>
        @php
            $zostalo = $zostalo - 1;
        @endphp
    @endwhile
</ul>

Widzisz „Zostaje 3”, potem 2, potem 1. Na liście szczytów tej pętli nie używasz.

@switch

@switch porównuje jedną wartość z kolejnymi @case. @break kończy dopasowany przypadek, tak jak break w PHP. Bez niego program wszedłby też w następny @case. @default obejmuje wartości, które nie pasują do żadnego przypadku.

<h2>@switch</h2>
<ul>
    @foreach ($szczyty as $szczyt)
        <li>
            {{ $szczyt['nazwa'] }} —
            @switch($szczyt['pasmo'])
                @case('Tatry')
                    najwyższe pasmo na tej liście
                    @break
                @case('Karkonosze')
                    Karkonosze
                    @break
                @default
                    inne pasmo
            @endswitch
        </li>
    @endforeach
</ul>

Rysy dostają tekst o Tatrach, Śnieżka o Karkonoszach, Babia Góra i Łysica wpadają w @default. Przy dwóch możliwościach zwykle czytelniejszy jest @if. @switch przydaje się, gdy jedno pole ma kilka stałych wartości, tak jak tutaj pasmo.

Wyrażenie, klasa, checkbox

W {{ }} może stanąć wyrażenie, nie tylko zmienna. Operator ? : wybiera jeden z dwóch napisów. @class składa atrybut class. Zwykły napis dodaje zawsze, a wpis z warunkiem tylko wtedy, gdy warunek jest prawdziwy.

<h2>Klasa i checkbox</h2>
@foreach ($szczyty as $szczyt)
    <p @class(['karta', 'wysoki' => $szczyt['wysokosc'] >= 1000])>
        {{ $szczyt['nazwa'] }} —
        {{ $szczyt['wysokosc'] >= 1000 ? 'od 1000 m' : 'poniżej 1000 m' }}
    </p>
    <label>
        <input type="checkbox" @checked($szczyt['wysokosc'] >= 2000) @disabled(true)>
        próg 2000 m (pole wyłączone, tylko do odczytu przykładu)
    </label>
@endforeach

Łysica nie dostaje klasy wysoki i ma tekst „poniżej 1000 m”. Checkbox jest zaznaczony tylko przy Rysach, bo tylko ten wiersz ma co najmniej 2000 m. @disabled(true) dopisuje atrybut disabled, więc pola nie da się kliknąć — to podgląd dyrektywy, nie element formularza z rozdziału 6. @selected działa tak samo przy <option>. Użyjesz go przy planie wyjścia.

Co zostaje w projekcie Strona /skladnia może zostać. Nie wpinasz jej w menu. Lista, karta i formularz jej nie potrzebują. Na liście Korony zostaje @forelse.

5. Komponent karty

Komponent anonimowy to sam plik Blade, bez klasy PHP. Nazwa pliku resources/views/components/karta-szczytu.blade.php daje znacznik <x-karta-szczytu>.

@props(['szczyt'])

<a class="karta" href="{{ route('szczyty.show', $szczyt['slug']) }}">
    <h2>{{ $szczyt['nazwa'] }}</h2>
    <p class="meta">
        {{ $szczyt['pasmo'] }}
        · <span class="wysokosc">{{ $szczyt['wysokosc'] }} m</span>
    </p>
</a>

Dwukropek w :szczyt="$szczyt" oznacza „przekaż wyrażenie PHP”, nie tekst. Bez dwukropka szczyt="rysy" byłoby napisem złożonym z pięciu znaków, a komponent szukałby klucza nazwa w napisie i widok by się wyłożył.

W szczyty/index.blade.php podmień artykuł w pętli na jeden znacznik:

@forelse ($szczyty as $szczyt)
    <x-karta-szczytu :szczyt="$szczyt" />
@empty
    <p>Brak szczytów na liście.</p>
@endforelse

Odśwież /szczyty. Cztery karty są linkami, pasmo i wysokość siedzą w komponencie. Nazwa pliku z myślnikiem staje się nazwą znacznika. Katalog components jest umowny: Blade szuka w nim sam, bez prefiksu w nazwie widoku.

{{ $slot }}

$slot to miejsce w komponencie na HTML, który wpiszesz między znacznikiem otwierającym a zamykającym. Tej zmiennej nie tworzysz w kontrolerze i nie przekazujesz jej dwukropkiem, tak jak :szczyt. Blade sam wkłada w {{ $slot }} to, co stoi w środku znacznika.

Dopisz jedną linię w karta-szczytu.blade.php, pod akapitem z pasmem. Cały plik wygląda wtedy tak:

@props(['szczyt'])

<a class="karta" href="{{ route('szczyty.show', $szczyt['slug']) }}">
    <h2>{{ $szczyt['nazwa'] }}</h2>
    <p class="meta">
        {{ $szczyt['pasmo'] }}
        · <span class="wysokosc">{{ $szczyt['wysokosc'] }} m</span>
    </p>
    {{ $slot }}
</a>

Nazwa, pasmo i wysokość biorą się z tablicy $szczyt. Linia {{ $slot }} jest osobnym miejscem: na dodatkową treść z widoku, który kartę wywołuje.

Na próbę otwórz znacznik na liście i wstaw w środek akapit. Zwykły ukośnik na końcu znika, bo znacznik ma teraz środek:

<x-karta-szczytu :szczyt="$szczyt">
    <p class="meta">Wpis z listy</p>
</x-karta-szczytu>

Dla Rysów przeglądarka dostaje mniej więcej taki HTML. Akapit „Wpis z listy” stoi tam, gdzie w komponencie było {{ $slot }}:

<a class="karta" href="http://127.0.0.1:8000/szczyty/rysy">
    <h2>Rysy</h2>
    <p class="meta">
        Tatry
        · <span class="wysokosc">2499 m</span>
    </p>
    <p class="meta">Wpis z listy</p>
</a>

Odśwież /szczyty. Pod wysokością każdej karty widać „Wpis z listy”. Potem na liście wróć do postaci bez środka:

<x-karta-szczytu :szczyt="$szczyt" />

Środek jest pusty, więc {{ $slot }} nic nie wypisuje. Zostają nazwa, pasmo i wysokość. Linię {{ $slot }} w pliku komponentu zostaw. Inny widok będzie mógł wstawić własne zdanie i nie skopiuje całej karty.

$slot a @props @props(['szczyt']) to dane podane atrybutem, na przykład :szczyt="$szczyt". {{ $slot }} to HTML wpisany między <x-karta-szczytu> a </x-karta-szczytu>. Przy <x-karta-szczytu :szczyt="$szczyt" /> atrybut jest, a środek jest pusty.
W bloku 3 zmieni się dostęp do pól Tablica używa $szczyt['nazwa'] i $szczyt['wysokosc']. Model użyje $szczyt->nazwa i $szczyt->wysokosc_m — w tabeli kolumna ma przyrostek _m. Poprawisz wtedy ten jeden komponent i widok karty, nie każde miejsce z osobna — o ile lista już woła komponent.

Klasowy komponent (php artisan make:component) ma osobny plik PHP. Na kursie zostajemy przy anonimowym: karta nie liczy, tylko rysuje. Livewire w bloku 5 to inny rodzaj komponentu — ten żyje na serwerze i reaguje na wpisywanie.

Commit

Gdy układ, strona /skladnia i komponent karty działają:

git add .
git status
git commit -m "Układ, składnia Blade i komponent karty"

Przed commitem git status nie pokazuje .env.