Web Components – artykuł konkursowy


14 sierpnia 2014 / Comandeer


Web ComponentsWakacje! Słońce, wysoka temperatura, półnagie kobiety na plaży… Meh. To najlepszy czas, żeby zaszyć się w swojej jaskinii programisty i wyhodować jakiegoś kodowego potworka. BUHAHAHAHA!

A ci z nas, którzy nie cierpią na ostre postacie agorafobii, z chęcią lubią prezentować innym efekty swej syzyfowej pracy. Ja również należę do tych ekshibicjonistów, dlatego też postanowiłem pochwalić się ideą (ok, istnieje coś w wersji mocno prealpha, aczkolwiek używalnej ;)) rodzącą się w bólach w mym umyśle. Ale od początku!

Na początku było Słowo…

…a kilkaset milionów lat później inteligentny zlepek komórek stworzył coś, co buńczucznie nazwał HTML-em. Dzięki temu wynalazkowi dzisiaj możemy oglądać słodkie kotki! Jest on także sponsorem dzisiejszego artykułu.

Któż nie zna tego wręcz prymitywnego w swej budowie języka znaczników? Wszyscy mamy z nim do czynienia na co dzień – wszak to język, w którym do nas przemawia Sieć. Lecz czy kiedykolwiek zastanawialiście się jaka to magia kryje się za tymi niepozornymi <tagami>. Na pewno nie! No bo cóż w tym takiego magicznego?

A jeśli powiem, że w tym prostym języku tkwi niesamowity potencjał, który może odmienić oblicze Sieci, naszej Sieci? „Oszalał, chce powrotu XHTML 2!” – zakrzyknie co bardziej rozgarnięty czytelnik. Sam bym za takie myśli dał sobie w pysk jakieś 2 lata temu… Ale o tamtego czasu zmieniło się bardzo dużo, by nie powiedzieć: wszystko!

Declaratives, declaratives everywhere!

Declaratives, Declaratives Everywhere

Nie oszukujmy się: HTML nie powstał jako język dla zaawansowanych aplikacji. To język dla dokumentów – header nie ma nic wspólnego z titlebarem aplikacji a [data-action] nie są w stanie zastąpić sensownego data binding. Nic zatem dziwnego, że powstały całe potężne frameworki JS mające te braki naprawić: poczynając od standardowego, tradycyjnego i lekko trącego dziś myszką jQuery UI, przechodząc obok potężnego i nielubianego przeze mnie, przerośniętego ext.js na najnowszym dziecku Google, Angularze ze swoim sytemem modułów i nie do końca walidującym się HTML-em, kończąc. Żeby zrobić proste okienko z komunikatem (ok, w HTML 5 mamy dialog, który działa aż w Chrome 37+) ładujemy tonę JS-a, zamiast… umieścić odpowiedni kod HTML na stronie. Powiedzcie sami – czy nie ładniej zamiast

użyć po prostu:

Nie jest to jakiś nowy pomysł – w HTML de facto od zarania dziejów (tzn od powstania JS ;)) takie rzeczy istnieją. Najlepszym przykładem tego jest new Image, który równocześnie jest konstruktorem dla elementu img (a raczej – każdy tag img na stronie jest równocześnie obiektem Image w JS) czy też new Audio i tag audio. Oto mamy mały i poręczny element HTML, pod którym kryje się potężne JS-owe API. Jak bardzo jest to myślenie naturalne, wskazują dzieje Event Source API (aka Server Sent Events): jego pierwszą wersją był… tag eventsource, który nawet doczekał się implementacji w jednej z wersji Opery.

Ci, którzy znają XML, są całkowicie przyzwyczajeni do własnych tagów. W HTML jednak przez długie lata taka rzecz była niedostępna. Owszem, na upartego można było stworzyć sobie nowy tag, typu comandeertoboss, ale na tym nasze możliwości się kończyły. Nie było żadnego sensownego sposobu na stworzenie zależności, jak w parze new Imageimg. Co więcej, niektóre przeglądarki (tak, na ciebie patrzę, IE; na ciebie również, stary lisku) miały dziwne problemy ze stylowaniem tych elementów (IE potrzebował czegoś na kształt HTML5 shiv a lisek… serwowania jako XHTML). O problemach w DOM już nawet nie wspominam. Nie dało się i już. „Semantyczny” kod (tzn. taki, który po prostu dałoby się czytać) trzeba było osiągać na inne sposoby – tutaj ważne role pełniły (i wciąż pełnią!) mikroformaty oraz architektura BEM. XML-owy raj (gosh…) własnych znaczników był poza zasięgiem webdeveloperów. Do czasu…

Web Components are coming!

Choć aplikacje internetowe różnią się diametralnie od siebie (FB nijak się ma do Muro), pod maską tak naprawdę niewiele się różnią. Wszystkie stosują potężne frameworki JS do tworzenia interfejsów użytkownika. Takie rozwiązanie musi:

  • Udostępniać sensowny sposób tworzenia nowych elementów GUI
  • Posiadać system szablonów
  • Umożliwiać całkowitą enkapsulację (interfejs ma brać dane i tyle – co leży pod spodem, powinno być ukryte)
  • Być reużywalne (czyli można je zaimportować na każdej stronie WWW, która tego potrzebuje)
  • Posiadać system 2-way data binding lub posiadać sensowny sposób informowania o zmianach (system eventów/pub-sub)

Tylko patrząc na tą listę, każdy logicznie myślący człowiek widzi, że problemy te można rozwiązać na wiele różnych sposobów (stąd tak niesamowita liczba dostępnych frameworków JS). A jeśli powiem, że obecnie istnieje natywny mechanizm, który rozwiązuje te wszystkie problemy?

Uśmiechasz się ironicznie, prawda? Nie wierzysz mi a to już działa (tzn nie do końca, ale o tym później) i nazywa się „Web Components”. Jak sama nazwa wskazuje, składa się to z kilku elementów:

  • Custom elements – czyli sensowny sposób tworzenia nowych elementów GUI
  • Tag template – czyli sensowny system szablonów
  • Shadow DOM – czyli całkowita enkapsulacja
  • link[rel=import] – czyli całkowita reużywalność
  • Custom events – czyli sensowny sposób informowania o zmianach
  • Object.observe/DOM Mutation Observer – czyli 2-way data binding

Brzmi tak pięknie, że aż musi być haczyk, prawda? Otóż… jest, taki jak zawsze w naszym dobrym, starym webdevelopingu: kompatybilność, którą na bieżąco można sprawdzić na stronie Are We Componentized Yet?. Ale jak to zwykle bywa, istnieją polyfille. Najpopularniejsze to Polymer (którego używam i który jest o wiele bardziej rozbudowany i oprócz wspomnianych wyżej rzeczy dostajemy w prezencie Pointer Events; „minusem” jest brak wsparcia dla IE9-, ale patrząc na ranking.pl to wcale nie jest minus ;)) i X-Tags (mniej rozbudowany, skupiający się przede wszystkim na custom elements, ale za to dostajemy obsługę dla IE9+). Natywnie wszystkie te rzeczy, jak widać, działają sensownie jedynie w Chrome 36+ (no i w Operze ;)), natomiast Firefox zostaje ciut z tyłu; Safari i IE przemilczę.

Skoro już wiemy co i gdzie (nie) działa, przyjrzyjmy się bliżej wszystkim tym elementom po kolei (nie liczcie na duży tutorial – nie chce mi się ;) poza tym napisano już doskonałe – oczywiście po angielsku – do których linki podam na końcu; ten artykuł potraktujcie jako dobry overview całej technologii).

Custom elements

To najczęściej wykorzystywany element (pun intented) Web Components i ich znak rozpoznawczy. Bez tego elementu (pun not intented) cała reszta nie ma żadnego sensu. Zatem w skrócie: tak, możesz tworzyć własne elementy HTML!

I zanim polecisz tworzyć comandeertoboss, wiadro zimnej wody na ochłodę: to nie jest aż tak proste! Custom element wygląda bowiem ciut inaczej niż zwykły element:

Jak widać, nazwa składa się z dwóch części, przedzielonych myślnikiem. Ten myślnik jest konieczny. Dlaczego? Gdyż przeglądarka inaczej traktuje elementy bez niego i z nim. Bez myślnika przeglądarka po prostu uznaje to za nieznany, niepoprawny element… i na tym się kończy. Natomiast gdy w nazwie myślnik występuje, przeglądarka zaznacza sobie ten element jako „prawdopodobnie custom element”.

„Do rzeczy – my chcemy pełnoprawnego custom elementa!” – zakrzykną webdeveloperzy, nielegalnie czytający ten artykuł w pracy. A więc – żeby przeglądarka rozpoznawała nasz tag jako… nasz tag ;) należy użyć funkcji document.registerElement:

Pozwoliłem sobie zapisać wynik funkcji do globalnego konstruktora TagName (dzięki temu mamy swój własny new Image – yay!). Jak widać, document.registerElement bierze jako 2. paremetr obiekt opcji, a w nim – prototyp naszego elementu. Tutaj ważne jest to, żeby obiekt TagNamePrototype dziedziczył po HTMLElement (albo jego pochodnych). Drugą opcją, obok prototype, jest extends – dzięki temu można rozszerzać już istniejące elementy; używa się ich jednak ciut inaczej:

Bardziej doświadczeni czytelnicy na pewno od razu zapytają czemu jest bezsensowny myślnik, skoro można to było rozwiązać za pomocą XML namespaces? Otóż: XML jest martwy ;) XHTML się nigdy nie przyjął, zatem naturalnym ruchem było wykorzystanie o wiele prostszego sposobu i to kompatybilnego ze starym HTML (tzw. bożek Backwards Compatibility).

W Sieci można znaleźć pełno custom elements, od bezsensownych po całkiem użyteczne.

template

Każda większa aplikacja webowa korzysta z systemu szablonów – nic w tym dziwnego. Jeśli kilka różnych części GUI korzysta z tego samego rozwiązania, to można to wyrzucić do szablonu i jedynie podmieniać odpowiednie dane. Dotąd, z powodu ograniczeń JS, przeważająca część systemów szablonów client-side operowała na stringach. template przenosi szablony na poziom DOM. To tak naprawdę nic innego, jak osobne drzewko DOM na naszej stronie. Co więcej – całkowicie nieaktywne. Nie musimy się martwić o to, że obrazki doczytają się bez potrzeby – zawartość  template uaktywnia się dopiero po umieszczeniu wewnątrz strony. Do tego czasu wszystko w nim pozostaje nienaruszone. A jak szablonów używać?

Mogłoby być prostsze, ale i tak nie jest źle. A mamy za to natywny system szablonów!

Shadow DOM

Kto wie jak wygląda drzewko DOM dla elementu input? Pewnie większość z Was popuka się w czoło i stwierdzi, że zwariowałem – jakie drzewko? A to już Wam pokazuję jakie (wyciągnięte z Chrome):

Zszokowani? I właśnie o to chodzi! My, jako userzy, widzimy tylko efekt końcowy – pole formularza. Cała reszta siedzi pod spodem i jest obsługiwana przez przeglądarkę. Polecam spojrzeć jak bardzo złożony pod spodem jest np tag video (na pewno ma więcej kodu niż aplikacje niejednego z nas ;)). I to jest właśnie Shadow DOM – magiczny worek, ukrywający całą implementację pod ładnie wyglądającym tagiem. Do niedawna taką moc mieli jedynie twórcy przeglądarek. Jednak Shadow DOM zostało ustandaryzowane (prawie – to najczęściej zmieniająca się specyfikacja W3C ;)) i obecnie każdy z nas może tworzyć rzeczy podobne do pola input. Jak?

Tym sposobem stworzyliśmy akapit z niewidzialnym tekstem (sympatyczny kod? ;)) – nie ma go w źródle strony a inspektor elementów też niekoniecznie go pokazuje (dopóki nie zaznaczym odpowiedniej opcji w dev-tools). Oczywiście nic nie stoi na przeszkodzie, żeby włożyć tam coś bardziej skomplikowanego (Shadow DOM bardzo lubi tag template ;)) czy nawet… kolejne cieniste drzewko (because we always must be able to go deeper…).

Zalety? Całkowita enkapsulacja! Co więcej – Shadow DOM można podpiąć pod accessibility API (czyli czytniki ekranowe to widzą) i wykorzystywać na nim ARIA. Niemniej jest to sposób na rozdzielenie prezentacji naszego custom elementu od jego treści – treść podajemy normalnie w tagu (jak zawsze), natomiast cały sposób prezentacji zamykamy w Shadow DOM (a obsługa treści w Shadow DOM jest banalna, bo wystarczy w odpowiednim miejscu wstawić tag content). Proste i skuteczne.

Największa przewaga GUI w JS nad GUI w HTML? Możliwość importu tylko tych rzeczy, których potrzebujemy, w prosty, asynchroniczny i przyjemny sposób (patrz: AMD, CJS, UMD, ES6 Modules). W HTML takiej możliwości nie było… do niedawna. Od niedawna „moduły HTML” są jeszcze prostsze do wykorzystania niźli moduły JS (i mówi Wam to wielki fanatyk JS-a!). Wyobraźmy sobie, że stworzyliśmy super wypasiony custom element, z Shadow DOM, szablonami i całym potężnym API… I mamy problem: jak go załączać na wszystkich naszych stronach? 1. myśl – inline’ujemy – odpada w przedbiegach: 1000 linijek HTML-a to jednak nie to, co tygryski lubią najbardziej. 2. myśl – AJAX i wkładanie odpowiednich zasobów w odpowiednie miejsca. Działa a my mamy asynchroniczny lazy loading… Niemniej wymaga to ciut pracy i nie zawsze musi działać.

Zatem jesteśmy pokonani? Oczywiście, że nie! Z pomocą nadchodzi link[rel=import]! Pozwala on wydzielić z naszego kodu „moduł HTML” (zatem custom element oraz to wszystko, czego potrzebuje do działania) i dołączać go do wszystkich innych dokumentów (skoro nasze API przepisaliśmy na tag, to czemu nie mamy załączać go dzięki tagowi?). Wyobraźmy sobie kod naszego wypasionego custom elementu:

Po pewnym czasie może się to bardzo rozrosnąć… Dlatego wystarczy cały ten kod przerzucić do osobnego pliku .html i importować:

I tyle! Do całej zawartości zaimportowanego pliku dostaniemy się przez własność import owego link[rel=import] (tak, jak w przypadku template, tutaj również otrzymamy drzewko DOM). Tym prostym sposobem stworzyliśmy nasz 1. „moduł HTML”. Czekam z niecierpliwością kiedy Facebook i inne portale będą tak udostępniać swoje widgety.

Custom events

Każdy szanujący się custom element powinien rzucać jakieś zdarzenia. A czy jest jakiś lepszy sposób od użycia tutaj custom events?

Jak widać, można nadać zdarzeniu dowolną nazwę. Jedynym ograniczeniem jest to, że wszystkie dodatkowe dane zdarzenia są przekazywane jako event.detail. Niemniej jest to potężny DOM-owy element, pozwalający kontrolować działanie elementu (no przecież każdy z nas doskonale to wie! :D).

Object.observe/DOM Mutation Observer

Data binding w JS? Object.observe – służy do obserwowania zmian zachodzących w danym obiekcie JS i w chwili jakiejkolwiek zmiany, wykonuje daną czynność.

Coś pięknego prawda? Jeśli chodzi o same DOM, tutaj tę rolę pełni Mutation Observer, którego używanie już tak przyjemne nie jest, dlatego odeślę zainteresowanych do MDN ;)

Co prawda same te mechanizmy nie tworzą nam 2-way data binding out of box, ale wystarczy kilka minut, żeby takie rozwiązanie na ich podstawie przygotować. Jeśli chodzi o ES5 – tutaj można się ładnie pobawić przy pomocy getterów/setterów i eventów, by uzyskać podobne rozwiązanie.

Co dalej?

Jaki obecnie jest najlepszy sposób na tworzenie własnych custom elements? Użycie custom elementu polymer-element!

Chyba żartujesz?

Wszystkich zainteresowanych tym rozwiązaniem zapraszam do zapoznania się z projektem untitled-element, który jest bardzo ciekawym wrapperem dla polymer-element, generującym automatycznie dokumentację dla naszego custom elementu oraz używającym Bowera jako systemu rozprowadzania (Polymer stworzył niebotyczny ekosystem i de facto „czyste” custom elements są w zdecydowanej mniejszości).

Osobiście sam używam untitled-element, który jest podstawą mojego projektu dGUI (declarative GUI) – na razie są dwa nieskończone elementy, ale na dysku już mam zalążki kolejnych custom elementów, które w ostateczności złożą się na rozbudowane GUI aplikacji a’la desktopowej i nie tylko (node-webkit, yay!).

Większość devów oszalała na punkcie Web Components i nic dziwnego. Przepisano już połowę Sieci na nie: routery, asynchroniczne formularze, przycisk FB a nawet… console.log. Czy Web Components się przyjmą? Już to zrobiły. Czy ludzie je polubią? Już z nimi sypiają! Czy to dobrze? Tak i nie… Możliwości są bardzo duże, a wraz z nimi przyszła wielka odpowiedzialność (ale o niej to może innym razem!). Niemniej dobrze użyte WebComponents są przyszłością aplikacji webowych!

Linki i inspiracja:

Image courtesy of twobee at FreeDigitalPhotos.net


Tagi:


5 odpowiedzi na “Web Components – artykuł konkursowy”

  1. Piotr Nalepa pisze:

    No, no Comandeer :) Ciekawy sposób pisania o tym.
    Mimo wszystko, dalej nie jestem przekonany do web components. Może kiedyś.

    • Comandeer pisze:

      Dzięki :D

      >Może kiedyś
      Miałem to samo podejście… aż przez przypadek wsiąkłem ;) Na samym początku wydaje się to całkowicie bzdurną ideą, lecz po bliższym przyjrzeniu jednak to przekonuje.
      No i najważniejsze: to działa. Istnieją już nawet bardzo sensowne projekty oparte na tym, np http://onsenui.io/

  2. Chyba miało być overview, a nie overwiev :P

    Mam poważne wątpliwości czy ktoś takiego systemu szablonów by używał, w poważnym projekcie. Największą zaletą szablonów jest prosty podział obowiązków. Backend backendem, a frontend sobie, wystarczy tylko dostarczyć info o renderowanych do widoku zmiennych. IMO szablony frontendowe to trochę przerost formy nad treścią. Dodatkowe zapytanie o naszwypasionycustomelement.html, w praktyce daleko temu do $view->render(’naszwypasionycustomelement.html’) – a zawsze i argumenty będzie można podać, albo jakiś {include file=’naszwypasionycustomelement.html’} ze Smarty czy innego Twiga.

    Przyznacie sami, że dużo przyjemniejsze będzie np: data-ng-bind-html=tplFactory(’tpl-name’, {arg1: 'foo’, arg2: 'bar’}), które znamy z AngularJS. (przykład wymyślony na potrzebę komentarza)

    Co do przyszłości aplikacji sieciowych to raczej wypatrywał bym jej gdzieś w warstwie OSI, bo patrząc prawdzie w oczy użytkownik ma gdzieś jak ten element został wstawiony, tag video nigdy nie robił na nim żadnego wrażenia, bo użytkownik od dawna miał już konto na YT… Web Components to fajna idea, ale tak na prawdę nie wnosi specjalnie nic nowego dla użytkownika. Możemy się zachwycić Polymerem, pewnie, ale to raczej wisienka do tortu niż „przyszłość”. Wszystko co można osiągnąć za pomocą Web Components można zrobić na dziś dzień używając HTMl5 i JS. Skutecznie Web Components będą blokować smartfony i tablety z Androidem, które często mają przeglądarkę na starszej o kilka wersji Chromium, a producent aktualizacji nie przewiduje.

    • Comandeer pisze:

      >Gratuluję wygranej.
      Dziękuję ;)

      >Chyba miało być overview, a nie overwiev :P
      Miało, miało.

      >Mam poważne wątpliwości czy ktoś takiego systemu szablonów by używał, w poważnym projekcie.
      Ale przecież to już jest używane ;) I to przez samych twórców HTML5 – https://github.com/ThePacielloGroup/html5-h

      >Największą zaletą szablonów jest prosty podział obowiązków. Backend backendem, a frontend sobie, wystarczy tylko dostarczyć info o renderowanych do widoku zmiennych.
      Obecnie słowo backend oznacza tylko i wyłącznie REST API dla większości aplikacji. Cały frontend Twittera opiera się na takiej zasadzie (Twitter.com jest klientem własnego API OAuth). To samo widać na FB (React.js i stworzenie abstrakcyjnego DOM). Co więcej, WebComponents przecież nie wprowadzają żadnego nowego pomysłu tutaj (aka mustache czy hogan) – serwer jedynie dostarcza danych a całe składanie odbywa się przecież na kliencie (Angular i Backbone działają identycznie). Jedyną różnicą między template a starodawnymi systemami szablonów jest fakt, że template operuje na DOM, reszta na stringach (lub abstrakcjach, jak React).

      >Dodatkowe zapytanie o naszwypasionycustomelement.html, w praktyce daleko temu do $view->render(’naszwypasionycustomelement.html’) – a zawsze i argumenty będzie można podać, albo jakiś {include file=’naszwypasionycustomelement.html’} ze Smarty czy innego Twiga.
      Twig jest przeportowany do JS i można go używać także po client side ;) Lekka rozbudowa tagu template (jak np ta, zaimplementowana w Polymer) pozwala także na taki binding w template. Jeśli serwer służy tylko do udostępniania danych dla aplikacji, to szablony leżą u klienta. W przypadku obecnej Sieci, gdzie coraz większy nacisk kładzie się na techniki offline first i no backend, jest to naturalne. I znów: mustache robi tak od wieków.

      >Przyznacie sami, że dużo przyjemniejsze będzie np: data-ng-bind-html=”tplFactory(’tpl-name’, {arg1: 'foo’, arg2: 'bar’})”, które znamy z AngularJS.
      Angular to akurat bardzo zły przykład, bo Angular 2.0 będzie opierał się na Polymer i wszystkie jego komponenty będą Web Componentami ;) http://www.2ality.com/2014/07/angularjs-vs-polymer.html (no bo po co wymyślać koło od nowa, skoro istnieje standard od tego?)
      Zresztą to zależy od preferencji programisty. Mnie łatwiej się jednak pracuje z szablonami deklaratywnymi.

      Mam wrażenie, że ograniczasz Web Components tylko i wyłącznie do systemu szablonów a to tylko wierzchołek góry lodowej. Prawdziwą potęgą jest ich modularność i reużywalność, połączona praktycznie z maksymalną hermetyzacją. To jest natywny mechanizm, implementujący eventowy model aplikacji, zaprezentowany przez Zakasa: http://www.slideshare.net/nzakas/scalable-javascript-application-architecture każdy element jest niezależnym modułem, który z resztą komunikuje się eventami/systemem pub-sub.

      >Web Components to fajna idea, ale tak naprawdę nie wnosi specjalnie nic nowego dla użytkownika.
      Super… ale od kiedy technologie internetowe związane z architekturą Sieci były przeznaczone dla użytkowników? ;) One są przeznaczone dla developerów, żeby mogli w łatwiejszy dla nich sposób dostarczać lepszy UX userom. I tylko to dla końcowego usera się liczy – a nie sposób przygotowania dania. Natomiast developerzy bardzo pozytywnie odbierają Web Components. Jedyne obawy z nimi związane to tak naprawdę semantyka i dostępność.

      >Wszystko co można osiągnąć za pomocą Web Components można zrobić na dziś dzień używając HTMl5 i JS – mniej elegancko, ale nadal
      Zapominasz o bardzo ważnych dwóch kwestiach: o natywności rozwiązania i jego standaryzacji. Mamy jeden, zunifikowany sposób robienia pewnych rzeczy. Na podstawie takiego standardu można zbudować rozwiązanie, które będzie działać wszędzie (a przy pomocy polyfillów de facto już działa). No i oczywiście, że można ;) Web Components powstały na bazie najpopularniejszych praktyk – to jeden z niewielu *realnych* standardów od W3C.

      >Skutecznie Web Components będą blokować smartfony i tablety z Androidem, które często mają przeglądarkę na starszej o kilka wersji Chromium, a producent aktualizacji nie przewiduje.
      Polyfill (wspomniane w artykule Polymer i X-Tags to najpopularniejsze) i już. Poza tym – na większości fonów z nowszym andkiem i tak raczej jest zainstalowany Google Chrome a on już aktualizacje dostaje ;) Cała reszta dostanie bardzo porządny polyfill, pozwalający używać tej funkcjonalności wszędzie (no hej, przecież cały czas tak robimy i działa!). Jedyną rzeczą, jaka od zawsze powstrzymywała HTML5 przed ogólną adaptacją, jest wciąż dychające IE9-. I tutaj upatrywałbym głównego zmartwienia.

  3. […] traktuje o technologi Web Components, która – co tu dużo mówić – fascynuje mnie od niemal początków prac nad nią, nie mogłem przejść obok niej obojętnie. Czy opłacało się wydać te kilkadziesiąt […]

Skomentuj Comandeer Anuluj pisanie odpowiedzi

Twój adres e-mail nie zostanie opublikowany.