1. Giriş
Hikayeler günümüzde popüler bir kullanıcı arayüzü bileşeni. Sosyal uygulamalar ve haber uygulamaları, bunları feed'lerine entegre ediyor. Bu codelab'de lit-element ve TypeScript ile bir hikaye bileşeni oluşturacağız.
Hikaye bileşeni sonunda şu şekilde görünür:
Sosyal medya veya haber "hikayesini", slayt gösterisi gibi sırayla oynatılacak bir kart koleksiyonu olarak düşünebiliriz. Aslında hikayeler kelimenin tam anlamıyla slayt gösterileridir. Kartlarda genellikle bir resim veya otomatik oynatılan video bulunur ve üstte ek metin bulunabilir. Aşağıda oluşturacağımız uygulamayı görebilirsiniz:
Özellik Listesi
- Resim veya video arka planı içeren kartlar.
- Hikayede gezinmek için ekranı sola veya sağa kaydırın.
- Otomatik oynatılan videolar
- Kartlara metin ekleme veya başka bir şekilde özelleştirme olanağı.
Bu bileşenin geliştirici deneyimine kadar, hikaye kartlarını aşağıdaki gibi düz HTML işaretlemesinde belirtmek faydalı olacaktır:
<story-viewer>
<story-card>
<img slot="media" src="some/image.jpg" />
<h1>Title</h1>
</story-card>
<story-card>
<video slot="media" src="some/video.mp4" loop playsinline></video>
<h1>Whatever</h1>
<p>I want!</p>
</story-card>
</story-viewer>
Bunu da özellik listesine ekleyelim.
Özellik Listesi
- HTML işaretlemesinde bir dizi kartı kabul edin.
Bu şekilde, herkes hikaye bileşenimizi sadece HTML yazarak kullanabilir. Bu, programcılar ve programcı olmayanlar için mükemmeldir ve HTML'in yaptığı her yerde (içerik yönetim sistemleri, çerçeveler vb.) işe yarar.
Ön koşullar
git
venpm
çalıştırabileceğiniz bir kabuk- Metin düzenleyici
2. Kurulum
Şu deposu klonlayarak başlayın: story-viewer-starter
git clone git@github.com:PolymerLabs/story-viewer-starter.git
Ortam, lit-element ve TypeScript ile zaten ayarlanmıştır. Bağımlılıkları yüklemeniz yeterlidir:
npm i
VS Code kullanıcıları, lit-html şablonlarında otomatik tamamlama, tür denetimi ve linting için lit-plugin uzantısını yüklemelidir.
Aşağıdaki komutu çalıştırarak geliştirme ortamını başlatın:
npm run dev
Kodlamaya hazırsınız.
3. <story-card> Bileşeni
Bileşik bileşenler oluştururken bazen daha basit alt bileşenlerle başlayıp bunları geliştirmek daha kolaydır. O zaman <story-card>
oluşturarak başlayalım. Tam çerçeve bir videoyu veya bir resmi gösterebilmelidir. Kullanıcılar, örneğin yer paylaşımlı metin kullanarak bunu daha fazla özelleştirebilmelidir.
İlk adım, bileşenimizin LitElement
sınıfını genişleten sınıfını tanımlamaktır. customElement
tasarımcısı, özel öğeyi kaydetme işlemini bizim için halleder. experimentalDecorators
işaretini kullanarak tsconfig dosyanızda süslemeleri etkinleştirdiğinizden emin olmanın tam zamanı (Başlangıç deposunu kullanıyorsanız süslemeler zaten etkindir).
Aşağıdaki kodu story-card.ts dosyasına ekleyin:
import { LitElement } from 'lit';
import { customElement } from 'lit/decorators.js';
@customElement('story-card')
export class StoryCard extends LitElement {
}
<story-card>
artık kullanılabilir bir özel öğedir ancak henüz gösterilecek bir şey yoktur. Öğenin iç yapısını tanımlamak için render
örneği yöntemini tanımlayın. Burada, lit-html'nin html
etiketini kullanarak öğe için şablon sağlayacağız.
Bu bileşenin şablonunda ne bulunmalıdır? Kullanıcı, iki öğe sağlamalıdır: bir medya öğesi ve bir yer paylaşımı. Bu nedenle, bunların her biri için bir <slot>
ekleyeceğiz.
Alanlar, bir özel öğenin alt öğelerinin oluşturulmasının gerektiğini belirtiriz. Daha fazla bilgi için Slot kullanımıyla ilgili adım adım açıklamalı bir kılavuzu burada bulabilirsiniz.
import { html } from 'lit';
export class StoryCard extends LitElement {
render() {
return html`
<div id="media">
<slot name="media"></slot>
</div>
<div id="content">
<slot></slot>
</div>
`;
}
}
Medya öğesini kendi slotuna ayırmak, tam sayfa stil ekleme ve videoları otomatik oynatma gibi işlemler için bu öğeyi hedeflememize yardımcı olur. Daha sonra varsayılan dolgu sağlayabilmemiz için ikinci yuvayı (özel yer paylaşımları için olan) bir kapsayıcı öğesinin içine koyun.
<story-card>
bileşeni artık şu şekilde kullanılabilir:
<story-card>
<img slot="media" src="some/image.jpg" />
<h1>My Title</h1>
<p>my description</p>
</story-card>
Ama çok kötü görünüyor:
Stil ekleniyor
Biraz stil ekleyelim. Lit-Element ile bunu statik bir styles
özelliği tanımlayıp css
ile etiketlenmiş bir şablon dizesi döndürerek yaparız. Burada yazılan her CSS yalnızca özel öğemiz için geçerlidir. Gölge DOM ile CSS bu açıdan gerçekten çok kullanışlıdır.
Yuvalı medya öğesini <story-card>
öğesini kapsayacak şekilde biçimlendirelim. Buradayken ikinci yuvada bulunan öğeler için güzel bir biçimlendirme yapabiliriz. Bu sayede, bileşen kullanıcıları bazı <h1>
veya <p>
öğeleri ekleyebilir ve varsayılan olarak güzel bir şey görebilir.
import { css } from 'lit';
export class StoryCard extends LitElement {
static styles = css`
#media {
height: 100%;
}
#media ::slotted(*) {
width: 100%;
height: 100%;
object-fit: cover;
}
/* Default styles for content */
#content {
position: absolute;
top: 0;
right: 0;
bottom: 0;
left: 0;
padding: 48px;
font-family: sans-serif;
color: white;
font-size: 24px;
}
#content > slot::slotted(*) {
margin: 0;
}
`;
}
Artık arka plan medyası içeren hikaye kartlarımız var ve istediğimiz her şeyi en üste koyabiliyoruz. Güzel! Otomatik oynatılan videoları uygulamak için birazdan StoryCard
sınıfına geri döneceğiz.
4. <story-viewer> bileşeni
<story-viewer>
öğemiz, <story-card>
öğelerinin üst öğesidir. Kartların yatay olarak düzenlenmesinden ve aralarında gezinmemize olanak tanımasından sorumludur. StoryCard
kampanyasıyla aynı şekilde başlayacağız. <story-viewer>
öğesinin alt öğesi olarak hikaye kartları eklemek istiyoruz. Bu nedenle, bu alt öğeler için bir yuva ekleyin.
Aşağıdaki kodu story-viewer.ts dosyasına ekleyin:
import { LitElement, html } from 'lit';
import { customElement } from 'lit/decorators.js';
@customElement('story-viewer')
export class StoryViewer extends LitElement {
render() {
return html`<slot></slot>`;
}
}
Sırada yatay düzen var. Bunu, yuvalı <story-card>
öğelerinin tümünün mutlak konumlarını vererek ve bunları dizinlerine göre çevirerek gerçekleştirebiliriz. :host
seçiciyi kullanarak <story-viewer>
öğesinin kendisini hedefleyebiliriz.
static styles = css`
:host {
display: block;
position: relative;
/* Default size */
width: 300px;
height: 800px;
}
::slotted(*) {
position: absolute;
width: 100%;
height: 100%;
}`;
Kullanıcı, yalnızca ana makinedeki varsayılan yüksekliği ve genişliği harici olarak geçersiz kılarak hikaye kartlarımızın boyutunu kontrol edebilir. Aşağıdaki gibi:
story-viewer {
width: 400px;
max-width: 100%;
height: 80%;
}
Şu anda görüntülenen kartı takip etmek için StoryViewer
sınıfına bir örnek değişkeni index
ekleyelim. LitElement'in @property
özelliğiyle süslemek, değeri değiştiğinde bileşenin yeniden oluşturulmasına neden olur.
import { property } from 'lit/decorators.js';
export class StoryViewer extends LitElement {
@property({type: Number}) index: number = 0;
}
Her kartın yatay olarak konuma getirilmesi gerekir. Bu çevirileri Lit-Element'ın update
yaşam döngüsü yönteminde uygulayalım. Bu bileşenin gözlemlenen bir özelliği her değiştiğinde güncelleme yöntemi çalışır. Genellikle, slot için sorgu yapar ve slot.assignedElements()
üzerinde döngü yaparız. Ancak, yalnızca bir tane adsız alanımız olduğu için bu, this.children
kullanmakla aynıdır. Kolaylık olması için this.children
değerini kullanalım.
import { PropertyValues } from 'lit';
export class StoryViewer extends LitElement {
update(changedProperties: PropertyValues) {
const width = this.clientWidth;
Array.from(this.children).forEach((el: Element, i) => {
const x = (i - this.index) * width;
(el as HTMLElement).style.transform = `translate3d(${x}px,0,0)`;
});
super.update(changedProperties);
}
}
<story-card>
'lerimiz artık aynı hizada. Uygun stile özen gösterdiğimiz sürece, alt öğe olarak diğer öğelerle birlikte çalışmaya devam ederler:
<story-viewer>
<!-- A regular story-card child... -->
<story-card>
<video slot="media" src="some/video.mp4"></video>
<h1>This video</h1>
<p>is so cool.</p>
</story-card>
<!-- ...and other elements work too! -->
<img style="object-fit: cover" src="some/img.png" />
</story-viewer>
build/index.html
sayfasına gidip diğer hikaye kartı öğelerinin yorumlarını kaldırın. Şimdi, bu sayfalara gidebileceğimiz bir yapı oluşturalım.
5. İlerleme çubuğu ve gezinme
Ardından, kartlar arasında gezinme yöntemi ve ilerleme çubuğu ekleyeceğiz.
Hikayede gezinmek için StoryViewer
işlevine bazı yardımcı işlevler ekleyelim. Dizini bizim için geçerli bir aralığa bağlarken dizini ayarlarlar.
story-viewer.ts dosyasında, StoryViewer
sınıfına şunları ekleyin:
/** Advance to the next story card if possible **/
next() {
this.index = Math.max(0, Math.min(this.children.length - 1, this.index + 1));
}
/** Go back to the previous story card if possible **/
previous() {
this.index = Math.max(0, Math.min(this.children.length - 1, this.index - 1));
}
Son kullanıcının gezinmesine olanak tanımak için <story-viewer>
öğesine "önceki" ve "sonraki" düğmelerini ekleyeceğiz. Düğmelerden biri tıklandığında, ya next
ya da previous
yardımcı işlevini çağırmak istiyoruz. lit-html, öğelere etkinlik işleyiciler eklemeyi kolaylaştırır; düğmeleri aynı anda oluşturup bir tıklama işleyici ekleyebiliriz.
render
yöntemini aşağıdaki şekilde güncelleyin:
export class StoryViewer extends LitElement {
render() {
return html`
<slot></slot>
<svg id="prev" viewBox="0 0 10 10" @click=${() => this.previous()}>
<path d="M 6 2 L 4 5 L 6 8" stroke="#fff" fill="none" />
</svg>
<svg id="next" viewBox="0 0 10 10" @click=${() => this.next()}>
<path d="M 4 2 L 6 5 L 4 8" stroke="#fff" fill="none" />
</svg>
`;
}
}
Etkinlik işleyicileri render
yönteminde, yeni svg düğmelerimize satır içi olarak nasıl ekleyebileceğimize göz atın. Bu, her etkinlik için kullanılabilir. Bir öğeye @eventname=${handler}
formunun bağlamasını eklemeniz yeterlidir.
Düğmelere stil uygulamak için static styles
özelliğine aşağıdakileri ekleyin:
svg {
position: absolute;
top: calc(50% - 25px);
height: 50px;
cursor: pointer;
}
#next {
right: 0;
}
İlerleme çubuğu için, her hikaye kartı için bir tane olmak üzere küçük kutulara stil uygulamak üzere CSS ızgarasına başvuracağız. Kutulara koşullu olarak sınıf ekleyerek "görünür olup olmadıklarını" belirtmek amacıyla index
özelliğini kullanabiliriz. i <= this.index : 'watched': ''
gibi bir koşullu ifade kullanabiliriz, ancak daha fazla sınıf eklersek ayrıntılar daha ayrıntılı hale gelebilir. Neyse ki, lit-html bu konuda yardımcı olmak için classMap
adlı bir yönerge sağlar. Önce classMap
dosyasını içe aktarın:
import { classMap } from 'lit/directives/class-map';
Ardından render
yönteminin alt kısmına aşağıdaki işaretlemeyi ekleyin:
<div id="progress">
${Array.from(this.children).map((_, i) => html`
<div
class=${classMap({watched: i <= this.index})}
@click=${() => this.index = i}
></div>`
)}
</div>
Ayrıca, kullanıcıların isterlerse doğrudan belirli bir hikaye kartına atlayabilmeleri için birkaç tıklama işleyici de ekledik.
static styles
bölümüne eklenecek yeni stiller:
::slotted(*) {
position: absolute;
width: 100%;
/* Changed this line! */
height: calc(100% - 20px);
}
#progress {
position: relative;
top: calc(100% - 20px);
height: 20px;
width: 50%;
margin: 0 auto;
display: grid;
grid-auto-flow: column;
grid-auto-columns: 1fr;
grid-gap: 10px;
align-content: center;
}
#progress > div {
background: grey;
height: 4px;
transition: background 0.3s linear;
cursor: pointer;
}
#progress > div.watched {
background: white;
}
Gezinme ve ilerleme çubuğu tamamlandı. Şimdi biraz stilde değişiklik yapalım.
6. Kaydırma
Kaydırma işlemini uygulamak için Hammer.js hareket kontrol kitaplığını kullanacağız. Çekiç, tava gibi özel hareketleri algılar ve kullanabileceğimiz alakalı bilgiler (delta X gibi) içeren etkinlikleri gönderir.
Tavaları tespit etmek için Çekiç'i şu şekilde kullanabilir ve bir kaydırma etkinliği gerçekleştiğinde öğemizi otomatik olarak güncelleyebiliriz:
import { state } from 'lit/decorators.js';
import 'hammerjs';
export class StoryViewer extends LitElement {
// Data emitted by Hammer.js
@state() _panData: {isFinal?: boolean, deltaX?: number} = {};
constructor() {
super();
this.index = 0;
new Hammer(this).on('pan', (e: HammerInput) => this._panData = e);
}
}
LitElement sınıfının kurucusu, etkinlik işleyicileri ana öğeye eklemek için mükemmel bir yerdir. Hammer kurucusu, hareketleri algılayacak bir öğe alır. Bizim örneğimizde bu, StoryViewer
veya this
olur. Ardından, Hammer'ın API'sini kullanarak "kaydırma" hareketini algılamasını ve kaydırma bilgilerini yeni bir _panData
özelliğine ayarlamasını söyleriz.
LitElement, _panData
mülkü @state
ile süsleyerek _panData
'teki değişiklikleri gözlemler ve güncelleme yapar ancak mülk için ilişkili bir HTML özelliği olmaz.
Şimdi, kaydırma verilerini kullanmak için update
mantığını geliştirelim:
// Update is called whenever an observed property changes.
update(changedProperties: PropertyValues) {
// deltaX is the distance of the current pan gesture.
// isFinal is whether the pan gesture is ending.
let { deltaX = 0, isFinal = false } = this._panData;
// When the pan gesture finishes, navigate.
if (!changedProperties.has('index') && isFinal) {
deltaX > 0 ? this.previous() : this.next();
}
// We don't want any deltaX when releasing a pan.
deltaX = isFinal ? 0 : deltaX;
const width = this.clientWidth;
Array.from(this.children).forEach((el: Element, i) => {
// Updated this line to utilize deltaX.
const x = (i - this.index) * width + deltaX;
(el as HTMLElement).style.transform = `translate3d(${x}px,0,0)`;
});
// Don't forget to call super!
super.update(changedProperties);
}
Artık hikaye kartlarımızı ileri geri sürükleyebiliriz. İşlemleri kolaylaştırmak için static get styles
'e geri dönelim ve ::slotted(*)
seçicisine transition: transform 0.35s ease-out;
ekleyelim:
::slotted(*) {
...
transition: transform 0.35s ease-out;
}
Şimdi sorunsuz kaydırma özelliğini kullanıyoruz:
7. Otomatik oynatma
Ekleyeceğimiz son özellik, videoların otomatik oynatılmasıdır. Odaklanılan hikaye kartı varsa arka plan videosunun oynatılmasını isteriz. Hikaye kartı odakta kaldığında videonun videosunu duraklatmamız gerekir.
Bunu, dizin her değiştiğinde uygun alt öğeler üzerinde "girdi" ve "çıktı" özel etkinliklerini göndererek uygularız. StoryCard
'te bu etkinlikleri alır ve mevcut videoları oynatır veya duraklatırız. StoryCard'da tanımlanan "entered" (girdi) ve "exited" (çıktı) örnek yöntemlerini çağırmak yerine neden alt öğeler üzerindeki etkinlikleri göndermeyi tercih etmelisiniz? Yöntemler kullanıldığında, bileşen kullanıcıları özel animasyonlar içeren kendi hikaye kartlarını yazmak için özel bir öğe yazmak zorunda kalıyordu. Etkinlikleri kullanarak yalnızca bir etkinlik işleyici ekleyebilirler.
StoryViewer
öğesinin index
özelliğini, etkinlikleri dağıtmak için kullanışlı bir kod yolu sağlayan bir belirleyici kullanacak şekilde yeniden düzenleyelim:
class StoryViewer extends LitElement {
@state() private _index: number = 0
get index() {
return this._index
}
set index(value: number) {
this.children[this._index].dispatchEvent(new CustomEvent('exited'));
this.children[value].dispatchEvent(new CustomEvent('entered'));
this._index = value;
}
}
Otomatik oynatma özelliğini bitirmek üzere, videoyu oynatıp duraklatan StoryCard
oluşturucuda "entered" (girdi) ve "exited" (çıktı) için etkinlik işleyiciler ekleyeceğiz.
Bileşen kullanıcısının, <story-card>
öğesine medya alanında bir video öğesi sağlayıp sağlamayacağını unutmayın. Medya alanına hiç öğe bile sağlamayabilirler. play
işlevini bir img veya null üzerinde çağırmamaya dikkat etmemiz gerekir.
Story-card.ts\'e geri dönüp aşağıdaki bilgileri ekleyin:
import { query } from 'lit/decorators.js';
class StoryCard extends LitElement {
constructor() {
super();
this.addEventListener("entered", () => {
if (this._slottedMedia) {
this._slottedMedia.currentTime = 0;
this._slottedMedia.play();
}
});
this.addEventListener("exited", () => {
if (this._slottedMedia) {
this._slottedMedia.pause();
}
});
}
/**
* The element in the "media" slot, ONLY if it is an
* HTMLMediaElement, such as <video>.
*/
private get _slottedMedia(): HTMLMediaElement|null {
const el = this._mediaSlot && this._mediaSlot.assignedNodes()[0];
return el instanceof HTMLMediaElement ? el : null;
}
/**
* @query(selector) is shorthand for
* this.renderRoot.querySelector(selector)
*/
@query("slot[name=media]")
private _mediaSlot!: HTMLSlotElement;
}
Otomatik oynatma tamamlandı. ✅
8. Terazileri Oynayın
Artık temel özelliklerin tümüne sahip olduğumuza göre, bir ekleme daha yapalım: hoş bir ölçeklendirme efekti. StoryViewer
olan update
yöntemine bir kez daha geri dönelim. scale
sabitindeki değeri almak için bazı matematik işlemleri yapılır. Bu değer, etkin çocuk için 1.0
'e, aksi takdirde minScale
'a eşit olur ve bu iki değer arasında da ara değer bulunur.
Story-viewer.ts'deki update
yöntemindeki döngüyü şu şekilde değiştirin:
update(changedProperties: PropertyValues) {
// ...
const minScale = 0.8;
Array.from(this.children).forEach((el: Element, i) => {
const x = (i - this.index) * width + deltaX;
// Piecewise scale(deltaX), looks like: __/\__
const u = deltaX / width + (i - this.index);
const v = -Math.abs(u * (1 - minScale)) + 1;
const scale = Math.max(v, minScale);
// Include the scale transform
(el as HTMLElement).style.transform = `translate3d(${x}px,0,0) scale(${scale})`;
});
// ...
}
Hepsi bu kadar! Bu gönderide LitElement ve lit-html özellikleri, HTML alanı öğeleri ve hareket kontrolü gibi pek çok konuyu ele aldık.
Bu bileşenin tamamlanmış sürümünü görmek için https://github.com/PolymerLabs/story-viewer adresini ziyaret edin.