1. Giriş
Hikayeler, günümüzde popüler bir kullanıcı arayüzü bileşenidir. Sosyal medya 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 şöyle görünür:

Sosyal medya veya haber "hikayelerini" slayt gösterisi gibi, sırayla oynatılacak bir kart koleksiyonu olarak düşünebiliriz. Aslında hikayeler, tam anlamıyla slayt gösterileridir. Kartlarda genellikle bir resim veya otomatik oynatılan video bulunur ve üstte ek metinler yer alabilir. Bu etkinlikte oluşturacağımız öğeler:
Özellik Listesi
- Resim veya video arka planlı kartlar.
- Hikayede gezinmek için ekranı sola veya sağa kaydırın.
- Videoları otomatik oynatma
- Kartlara metin ekleme veya kartları başka şekillerde özelleştirme
Bu bileşenin geliştirici deneyimi açısından, hikaye kartlarını şu şekilde düz HTML işaretlemesiyle belirtmek iyi olur:
<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>
Bu nedenle, bunu da özellik listesine ekleyelim.
Özellik Listesi
- HTML biçimlendirmesinde bir dizi kartı kabul etme
Bu sayede herkes yalnızca HTML yazarak hikaye bileşenimizi kullanabilir. Bu özellik, hem programcılar hem de programcı olmayanlar için idealdir ve HTML'nin kullanıldığı her yerde (içerik yönetim sistemleri, çerçeveler vb.) çalışır.
Ön koşullar
gitvenpmkomutlarını çalıştırabileceğiniz bir kabuk- Metin düzenleyici
2. Kurulum
story-viewer-starter deposunu klonlayarak başlayın.
git clone git@github.com:PolymerLabs/story-viewer-starter.git
Ortamda lit-element ve TypeScript zaten ayarlanmış olmalıdır. Bağımlılıkları yükleyin:
npm i
VS Code kullanıcıları, lit-html şablonlarında otomatik tamamlama, tür kontrolü ve linting özelliklerini kullanmak için lit-plugin uzantısını yükleyebilir.
Aşağıdaki komutu çalıştırarak geliştirme ortamını başlatın:
npm run dev
Kodlamaya başlayabilirsiniz.
3. <story-card> bileşeni
Bileşik bileşenler oluştururken bazen daha basit alt bileşenlerle başlayıp bunları birleştirmek daha kolaydır. Bu nedenle, <story-card> oluşturarak başlayalım. Tam taşan video veya resim gösterebilmelidir. Kullanıcılar, örneğin yer paylaşımı metniyle daha fazla özelleştirme yapabilmelidir.
İlk adım, LitElement öğesini genişleten bileşenimizin sınıfını tanımlamaktır. customElement dekoratörü, özel öğeyi bizim için kaydeder. Şimdi experimentalDecorators işaretini kullanarak tsconfig dosyanızda dekoratörleri etkinleştirdiğinizden emin olmanın tam zamanı (başlangıç deposunu kullanıyorsanız bu özellik zaten etkindir).
Aşağıdaki kodu story-card.ts dosyasına yerleştirin:
import { LitElement } from 'lit';
import { customElement } from 'lit/decorators.js';
@customElement('story-card')
export class StoryCard extends LitElement {
}
Artık <story-card> kullanılabilir bir özel öğe ancak henüz görüntülenecek bir şey yok. Öğenin iç yapısını tanımlamak için render örnek yöntemini tanımlayın. Burada, lit-html'nin html etiketini kullanarak öğenin şablonunu sağlayacağız.
Bu bileşenin şablonunda ne olmalıdır? Kullanıcı iki öğe sağlamalıdır: bir medya öğesi ve bir yer paylaşımı. Bu nedenle, her biri için bir <slot> ekleyeceğiz.
Yuvalar, özel bir öğenin alt öğelerinin nasıl oluşturulacağını belirtmek için kullanılır. Daha fazla bilgi için yuvaları kullanmayla ilgili bu ayrıntılı açıklamayı inceleyin.
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 alanına ayırmak, bu öğeyi tam taşma stili ekleme ve videoları otomatik oynatma gibi işlemler için hedeflememize yardımcı olur. Daha sonra varsayılan dolgu sağlayabilmemiz için ikinci yuvanın (özel yer paylaşımları için olan) bir kapsayıcı öğenin içine yerleştirilmesi gerekir.
<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>
Ancak bu durum korkunç görünüyor:

Stil ekleme
Tarzınızı yansıtın. 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 CSS yalnızca özel öğemiz için geçerlidir. Gölge DOM'lu CSS bu açıdan gerçekten çok iyi.
Yuvalı medya öğesini <story-card>'yı kaplayacak şekilde stilize edelim. Bu sırada, ikinci yuvadaki öğeler için güzel bir biçimlendirme sağlayabiliriz. Bu sayede, bileşen kullanıcıları bazı <h1>'ları, <p>'ları veya başka öğeleri bırakıp 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 üzerine istediğimiz her şeyi ekleyebiliyoruz. Güzel! Otomatik oynatılan videoları uygulamak için birazdan StoryCard sınıfına döneceğiz.
4. <story-viewer> bileşeni
<story-viewer> öğemiz, <story-card> öğelerinin üst öğesidir. Kartları yatay olarak yerleştirmek ve aralarında geçiş yapmamızı sağlamak bu görünümün sorumluluğundadır. StoryCard için yaptığımız gibi başlayacağız. Hikaye kartlarını <story-viewer> öğesinin alt öğeleri olarak eklemek istiyoruz. Bu nedenle, bu alt öğeler için bir yuva ekleyin.
Aşağıdaki kodu story-viewer.ts dosyasına yerleştirin:
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, yerleştirilmiş tüm <story-card>'lere mutlak konumlandırma vererek ve bunları dizinlerine göre çevirerek yapabiliriz. :host seçiciyi kullanarak <story-viewer> öğesini hedefleyebiliriz.
static styles = css`
:host {
display: block;
position: relative;
/* Default size */
width: 300px;
height: 800px;
}
::slotted(*) {
position: absolute;
width: 100%;
height: 100%;
}`;
Kullanıcı, 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şken index ekleyelim. LitElement'in @property ile süslenmesi, değeri her 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 yerine çevrilmesi gerekir. Bu çevirileri lit-element'in update yaşam döngüsü yönteminde uygulayalım. Güncelleme yöntemi, bu bileşenin gözlemlenen bir özelliği her değiştiğinde çalışır. Genellikle, slot.assignedElements() üzerinden yuvayı sorgular ve döngü oluştururuz. Ancak yalnızca bir tane adsız yuva olduğundan bu, this.children kullanmakla aynıdır. Kolaylık sağlaması için this.children simgesini 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>larımız artık yan yana. Uygun şekilde stil verdiğimiz sürece diğer öğelerle birlikte alt öğe olarak kullanılabilir:
<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 bölümüne gidin ve hikaye kartı öğelerinin geri kalanını yorum satırı dışına alın. Şimdi de bu öğelere gidebilmemizi sağlayalı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 öğesine bazı yardımcı işlevler ekleyelim. Geçerli bir aralığa sabitleyerek bizim için dizin oluşturur.
story-viewer.ts dosyasındaki StoryViewer sınıfına şunu 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));
}
Gezinme özelliğini son kullanıcıya sunmak için <story-viewer>'ya "önceki" ve "sonraki" düğmelerini ekleyeceğiz. Düğmelerden biri tıklandığında next veya previous yardımcı işlevini çağırmak istiyoruz. lit-html, öğelere etkinlik işleyicileri eklemeyi kolaylaştırır. Düğmeleri oluşturabilir ve aynı anda 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>
`;
}
}
Yeni SVG düğmelerimize render yönteminde satır içi etkinlik dinleyicileri nasıl ekleyebileceğimize göz atın. Bu yöntem, tüm etkinlikler için geçerlidir. Yalnızca @eventname=${handler} biçiminde bir bağlama ekleyin.
Düğmeleri stilize etmek 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 kutuları stilize etmek amacıyla CSS ızgarası kullanacağız. Kutulara "görüldü" veya "görülmedi" durumunu belirten sınıfları koşullu olarak eklemek için index özelliğini kullanabiliriz. i <= this.index : 'watched': '' gibi bir koşullu ifade kullanabiliriz ancak daha fazla sınıf eklersek kod çok ayrıntılı hale gelebilir. Neyse ki lit-html, bu konuda yardımcı olacak classMap adlı bir yönerge sunuyor. Öncelikle classMap içe aktarın:
import { classMap } from 'lit/directives/class-map';
Ayrıca, render yönteminin en altına aşağıdaki biçimlendirmeyi 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 istedikleri hikaye kartına doğrudan gitmelerini sağlamak için birkaç tıklama işleyici daha ekledik.
static styles'ya eklenecek yeni stiller şunlardır:
::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 renk katalım.
6. Kaydırma
Kaydırma özelliğini uygulamak için Hammer.js hareket kontrolü kitaplığını kullanacağız. Hammer, kaydırma gibi özel hareketleri algılar ve kullanabileceğimiz alakalı bilgiler (ör. delta X) içeren etkinlikler gönderir.
Kaydırmaları algılamak ve bir kaydırma etkinliği gerçekleştiğinde öğemizi otomatik olarak güncellemek için Hammer'ı nasıl kullanabileceğimiz aşağıda açıklanmıştır:
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);
}
}
Bir LitElement sınıfının oluşturucusu, etkinlik işleyicileri doğrudan ana öğeye eklemek için de kullanılabilir. Hammer oluşturucusu, hareketlerin algılanacağı bir öğe alır. Bu örnekte, StoryViewer veya this. 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öylüyoruz.
_panData özelliğini @state ile süsleyerek LitElement, _panData özelliğindeki değişiklikleri gözlemler ve güncelleme yapar ancak özellik için ilişkili bir HTML özelliği olmaz.
Ardından, update mantığını, pan verilerini kullanacak şekilde artıralım:
// 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. İşleri kolaylaştırmak için static get styles'ya geri dönüp ::slotted(*) seçicisine transition: transform 0.35s ease-out; ekleyelim:
::slotted(*) {
...
transition: transform 0.35s ease-out;
}
Artık sorunsuz kaydırma yapabilirsiniz:

7. Otomatik oynatma
Ekleyeceğimiz son özellik, videoları otomatik oynatma olacak. Bir hikaye kartı odaklandığında, varsa arka plan videosunun oynatılmasını istiyoruz. Bir hikaye kartı odak dışına çıktığında videosu duraklatılmalıdır.
Dizin her değiştiğinde uygun alt öğelerde "entered" ve "exited" özel etkinliklerini göndererek bu işlevi uygularız. StoryCard uygulamasında bu etkinlikleri alır ve mevcut videoları oynatır veya duraklatırız. Neden StoryCard'da tanımlanan "entered" ve "exited" örnek yöntemlerini çağırmak yerine çocuklarda etkinlik göndermeyi tercih etmelisiniz? Yöntemler kullanıldığında, bileşen kullanıcıları özel animasyonlar içeren kendi hikaye kartlarını yazmak isteseler bile özel bir öğe yazmak zorunda kalırdı. Etkinliklerle, yalnızca bir etkinlik dinleyicisi ekleyebilirler.
Etkinlikleri göndermek için uygun bir kod yolu sağlayan bir belirleyici kullanmak üzere StoryViewer özelliğini yeniden düzenleyelim:index
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 tamamlamak için StoryCard oluşturucusuna, videoyu oynatan ve duraklatan "entered" ve "exited" etkinlik dinleyicilerini ekleyeceğiz.
Bileşen kullanıcısının, medya yuvasında <story-card> öğesine video öğesi verip vermeyeceğini unutmayın. Hatta medya alanında hiç öğe sağlamayabilirler. Bir img üzerinde veya null üzerinde play çağırmamaya dikkat etmemiz gerekir.
story-card.ts dosyasına geri dönüp aşağıdakileri 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. Tip the Scales
Artık tüm temel özelliklere sahibiz. Şimdi bir özellik daha ekleyelim: tatlı bir ölçeklendirme efekti. StoryViewer update yöntemine bir kez daha dönelim. scale sabiti için değer elde etmek üzere bazı matematiksel işlemler yapılır. Etkin çocuk için 1.0, aksi takdirde minScale değerine eşit olur ve bu iki değer arasında da enterpolasyon yapılır.
story-viewer.ts dosyasındaki update yönteminde 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 yayında, LitElement ve lit-html özelliklerinin yanı sıra HTML yuva öğeleri ve hareket kontrolü gibi birçok konuyu ele aldık.
Bu bileşenin tamamlanmış sürümü için https://github.com/PolymerLabs/story-viewer adresini ziyaret edin.