Kliensoldali webprogramozás

Web komponensek

Horváth Győző
Egyetemi docens
1117 Budapest, Pázmány Péter sétány 1/c., 2.408-as szoba
Tel: (1) 372-2500/8469
horvath.gyozo@inf.elte.hu

Ismétlés

Weboldalak progresszív fejlesztése

  • Elv
    • JS nélkül is működjön
    • JS-sel kényelmesebb, szebb
  • Felépítés
    1. szerkezet (szemantikus HTML)
    2. megjelenés, stílus (CSS)
    3. viselkedés (JavaScript)

Függvénykönyvtárak alkalmazása

<!-- index.html -->
<script src="https://cdn.jsdelivr.net/npm/lodash@4.17.15/lodash.min.js"></script>

Hasznos böngésző API-k

  • Méretek
  • Scroll
  • IntersectionObserver, MutationObserver

Példa

Password mező

<form>
  <input type="password" name="password1" value="secret1">
  <input type="password" name="password2" value="secret2">
</form>

Progresszív fejlesztés

<style>
  /* ... */
</style>
<form>
  <div class="show-password">
    <input type="password" name="password1" value="secret1">
    <button type="button"></button>
  </div>
  <div class="show-password">
    <input type="password" name="password2" value="secret2">
    <button type="button"></button>
  </div>
</form>
<script>
  // ...
</script>

Egységbe zárás

<form>
  <!-- SHOW-PASSWORD BEGINS -->
    <style> /* ... */ </style>
    <div class="show-password">
      <input type="password" name="password1" value="secret1">
      <button type="button"></button>
    </div>
    <script> /* ... */ </script>
  <!-- SHOW-PASSWORD ENDS -->

  <!-- SHOW-PASSWORD BEGINS -->
    <style> /* ... */ </style>
    <div class="show-password">
      <input type="password" name="password2" value="secret2">
      <button type="button"></button>
    </div>
    <script> /* ... */ </script>
  <!-- SHOW-PASSWORD ENDS -->
</form>

Egységbe zárás

<form>
  <show-password name="password1" value="secret1"></show-password>
  <show-password name="password2" value="secret2"></show-password>
</form>

Web komponensek

Cél

  • Felület felosztása funkcionális egységekre
  • Egységbe zárása
    • HTML és/vagy CSS és/vagy JS
  • Moduláris kód
  • Újrafelhasználhatóság
  • Izoláltság

Építőkövei

  • Custom elements
  • Shadow DOM
  • Templates

Templates

<template>

  • Felhasználó által meghatározott sablon
  • Nem jelenik meg rendereléskor
  • Újrahasznosítható
  • Hatékony (egyszer építik fel)
  • Bármilyen HTML-t tartalmazhat
<template>
  <!-- ... -->
</template>

Példa

<div id="movies"></div>
<template id="movie-details">
  <details>
    <summary>Film</summary>
    <img src="">
  </details>
</template>
(async function () {
  const search = 'Hobbit'
  const response = await fetch(`http://www.omdbapi.com/?s=${search}&apikey=2dd0dbee`)
  const json = await response.json()
  const results = json.Search

  const movies = document.querySelector('#movies')
  const template = document.querySelector('#movie-details')
  results.forEach(movie => {
    const instance = template.content.cloneNode(true)
    instance.querySelector('summary').innerHTML = movie.Title
    instance.querySelector('img').src = movie.Poster
    movies.appendChild(instance)
  });
}());

Példa

<template>

<template id="movie-details">
  <details>
    <summary>Film</summary>
    <img src="">
  </details>
</template>
const title = 'Hobbit'
const url = 'http://imguri'
const template = document.querySelector('#movie-details')
const instance = template.content.cloneNode(true)
instance.querySelector('summary').innerHTML = title
instance.querySelector('img').src = url
movies.appendChild(instance)

template string

const title = 'Hobbit'
const url = 'http://imguri'
const s = `
  <details>
    <summary>${title}</summary>
    <img src="${url}">
  </details>
`
movies.innerHTML += s

Dinamikus létrehozás

const template = document.createElement('template')
template.innerHTML = `
  <details>
    <summary>Film</summary>
    <img src="">
  </details>
`

const template = document.querySelector('#movie-details')
const instance = template.content.cloneNode(true)
// ...

Tartalma

Bármi lehet

<template>
  <style>
    /*  */
  </style>
  <div>
    <!--  -->
  </div>
  <script>
    // 
  </script>
</template>

~ show-password (???)

Custom elements

Cél

  • Saját HTML elemek definiálása
  • Meglévő elemek funkcionalitásának bővítése
  • Újrahasznosítható komponensek írása HTML/CSS/JS segítségével
  • Modularitás
<show-password name="password1" value="secret1"></show-password>

Kategóriák

  • Autonomous custom elements
    • önálló
    • extends HTMLElement
  • Customized built-in elements
    • meglévő kiterjesztése
    • örökli tulajdonságait

Létrehozás

class ShowPassword extends HTMLElement {
  connectedCallback() {
    this.innerHTML = `Hello component!`;
  }
}
    
customElements.define('show-password', ShowPassword);
<show-password></show-password>

Kötőjelet kell tartalmaznia a névnek!

Létrehozás

<show-password>
  <p>Content</p>
</show-password>
class ShowPassword extends HTMLElement {
  connectedCallback() {
    console.log(this.innerHTML) // '<p>Content</p>'
    this.appendChild(document.createElement('hr'))
    this.innerHTML = `Hello component!`
  }
}
customElements.define('show-password', ShowPassword);

Életciklus események

class ShowPassword extends HTMLElement {
  constructor() {
    super(); // always call super() first in the constructor.
    // ...
  }
  connectedCallback() {
    // Called every time the element is inserted into the DOM. 
    // Useful for running setup code, such as fetching resources or rendering. 
    // Generally, you should try to delay work until this time.
  }
  disconnectedCallback() {
    // Called every time the element is removed from the DOM. 
    // Useful for running clean up code.
  }
  attributeChangedCallback(attrName, oldVal, newVal) {
    // Called when an observed attribute has been added, removed, updated, or replaced. 
  }
  adoptedCallback() {
    // The custom element has been moved into a new document
  }
}

Attribútumok

  • setAttribute(name, value)
  • getAttribute(name)
  • removeAttribute(name)
  • hasAttribute(name)
<show-password name="password1" value="secret1"></show-password>

Tulajdonságok

Komponens JavaScript API-ja

const sp = document.querySelector('show-password[name=password1]')
sp.name
sp.name = 'password0'
sp.show()
class ShowPassword extends HTMLElement {
  constructor() {
    super()
    this._name = ''
  }
  get name() {
    return this._name
  }
  set name(val) {
    this._name = val
  }
  show() {
    // ...
  }
}

Attribútumok és tulajdonságok szinkronban tartása

Pl. emiatt: show-password[disabled] { opacity: 0.4; }

class ShowPassword extends HTMLElement {
  // ...
  get name() {
    if (this.hasAttribute('name')) {
      return this.getAttribute('name')
    }
  }
  set name(val) {
    if (val) {
      this.setAttribute('name', val);
    } else {
      this.removeAttribute('name');
    }
  }
}

Reflecting properties to attributes

Változások az attribútumban

Statikus observedAttributes tömb

class ShowPassword extends HTMLElement {
  static get observedAttributes() {
    return ['name', 'value'];
  }
  attributeChangedCallback(name, oldValue, newValue) {
    console.log(name, oldValue, newValue)
  }
}
const sp = document.querySelector('show-password[name=password1]')
sp.setAttribute('name', 'password0')

Változások az attribútumban

  • Figyelem! Végtelen ciklus!
  • Megoldás: ebben az esetben nem kell külön figyelni
class ShowPassword extends HTMLElement {
  static get observedAttributes() {
    return ['name', 'value'];
  }
  attributeChangedCallback(name, oldValue, newValue) {
    if (name === 'name') {
      this.name = newValue
    }
    else if (name === 'value') {
      this.value = newValue
    }
  }
  ✒>get name() {
    if (this.hasAttribute('name')) {
      return this.getAttribute('name')
    }
  }
  set name(val) {
    if (val) {
      this.setAttribute('name', val);
    } else {
      this.removeAttribute('name');
    }
  }<✒
}

Stílusok

Mivel az oldal DOM-jába renderelődik ki, ezért a globális stílusok alkalmazódnak.

<style>
  p {
    color: red;
  }
  show-password {
    border: 3px solid orange;
  }
</style>
<show-password>
  <p>Content</p>
</show-password>

Egyedi komponensek kiterjesztése

class ExtendedShowPassword extends ShowPassword {
  constructor() {
    super()
    // ....
  }
  show() {
    // super.show()
  }
  newMethod() {
    // ...
  }
}

customElements.define('extended-show-password', ExtendedShowPassword);

Beépített elemek testreszabása

class LoggerButton extends HTMLButtonElement {
  constructor() {
    super();
  }
  connectedCallback() {
    this.addEventListener('click', e => console.log('click'));
  }
}

customElements.define('logger-button', LoggerButton, {extends: 'button'});
<button is="logger-button">Click me to log!</button>
<button is="logger-button" disabled>Click me to log!</button>

Támogatottsága egyelőre kicsi

Shadow DOM

Shadow DOM

  • Rejtett DOM fa kapcsolása egy elemhez
  • egységbe zárás
  • izolált DOM és CSS
  • Nem új, eddig is volt, pl. <video> tag

Előnyei

  • Izolált DOM
    • komponens DOM-ját nem látja az oldal DOM-ja
  • Izolált CSS
    • nem hat az oldal CSS-e a komponens CSS-ére
    • belülről sincs kihatás
  • Kompozíció
    • deklaratív, markup alapú API
  • Egyszerűbb CSS
  • Hatékonyság
    • részekben gondolkozni az egész oldal helyett

Shadow DOM

Létrehozás

  • attachShadow({ mode: 'open' })
  • Nem mindenhez lehet (van saját vagy nincs értelme)
<style>
  p { color: red; }
</style>
<p>Other paragraph</p>
<div>
  <p>Original content in div</p>
</div>
const elem = document.querySelector('div')
const shadowRoot = elem.attachShadow({ mode: 'open' })
shadowRoot.innerHTML = `
  <style>
    p { color: blue; }
  </style>
  <p>Shadow content</p>
`

Tartalom és kompozíció

<slot> elem, light, shadow, flattened DOM

<style>p { color: red; }</style>
<div>
  <!-- Light DOM -->
  <span slot="content">Span from outside</span>
  <p>Original content</p>
</div>
const elem = document.querySelector('div')
const shadowRoot = elem.attachShadow({ mode: 'open' })
shadowRoot.innerHTML = `
  <!-- Shadow DOM -->
  <style>p { color: blue; }</style>
  <p>
    Shadow content and
    <slot name="content">Default content</slot>
  </p>
  <slot>Default value</slot>
`

Stílusok

  • Light DOM: külső CSS
  • Shadow DOM: belső CSS
    • :host, :host(sel), :host-context(sel): root stílusa
    • ::slotted: beemelt elemek belső stílusának
  • CSS custom properties
    • --my-variable: red;
    • color: var(--my-variable, blue);
  • Bővebben
  • Constructible stylesheets
    • CSSOM

<show-password>

  • Web komponensek
  • Progresszív fejlesztés
  • Kódevolúció

Alap oldal

<form action="" method="get">
  <input type="password" name="password1" value="secret1">
  <button>Submit</button>
</form>

Progresszív fejlesztés

<form action="" method="get">
  <input type="password" name="password1" value="secret1" data-show-password>
  <button>Submit</button>
</form>
<style>/* ... */</style>
const input = document.querySelector('[data-show-password]')
const parent = input.parentNode
const div = document.createElement('div')
div.classList.add('show-password')
const button = document.createElement('button')
button.type = 'button'
parent.replaceChild(div, input)
div.appendChild(input)
div.appendChild(button)

let visible = false;

button.addEventListener('click', function (e) {
  visible = !visible
  if (visible) {
    div.setAttribute('data-visible', '')
    input.type = 'text'
  } else {
    div.removeAttribute('data-visible')
    input.type = 'password'
  }
})

Stílusok

<style>
  div.show-password {
    white-space: nowrap;
    display: inline-flex;
  }

  div.show-password input {
    line-height: 2;
    border: 1px solid gray;
    border-radius: 0.25em;
    flex: 1;
  }

  div.show-password button {
    border: 1px solid gray;
    border-radius: 0.25em;
    background-color: hsl(200, 50%, 90%);
  }

  div.show-password button::after {
    content: '⚪';
  }

  div.show-password[data-visible] button::after {
    content: '⚫';
  }

  div.show-password input:not(:last-child) {
    border-top-right-radius: 0;
    border-bottom-right-radius: 0;
    border-right: 0;
  }

  div.show-password button:last-child {
    border-top-left-radius: 0;
    border-bottom-left-radius: 0;
  }
</style>

Egységbe zárás

<form action="" method="get">
  <input type="password" name="password1" value="secret1" data-show-password>
  <input type="password" name="password2" value="secret2" data-show-password>
  <button>Submit</button>
</form>
<style></style>
document.querySelectorAll('[data-show-password]').forEach(
  el => new ShowPassword(el))

class ShowPassword {
  constructor(input) {
    if (!input.matches('input[type=password]')) {
      return
    }

    const parent = input.parentNode
    const div = document.createElement('div')
    div.classList.add('show-password')
    const button = document.createElement('button')
    button.type = 'button'
    parent.replaceChild(div, input)
    div.appendChild(input)
    div.appendChild(button)
    
    let visible = false;
    
    button.addEventListener('click', function (e) {
      visible = !visible
      if (visible) {
        div.setAttribute('data-visible', '')
        input.type = 'text'
      } else {
        div.removeAttribute('data-visible')
        input.type = 'password'
      }
    })
  }
}

Saját komponens

Shadow DOM nélkül

<form action="" method="get">
  <show-password name="password1" value="secret"></show-password>
  <input type="password" name="password2" value="secret" data-show-password>
</form>
<style>
  /* ... */
</style>
<template id="show-password">
  <div class="show-password">
    <input type="password">
    <button type="button"></button>
  </div>
</template>

Saját komponens

class ShowPassword extends HTMLElement {
  constructor() {
    super()
  }
  connectedCallback() {
    const name = this.getAttribute('name')
    const value = this.getAttribute('value')
    
    const template = document.querySelector('#show-password')
    const content = template.content.cloneNode(true)
    this.appendChild(content)

    const div = this.querySelector('div')
    const button = this.querySelector('button')
    const input = this.querySelector('input')
    this.input = input
    input.name = name
    input.value = value

    let visible = false;
    
    button.addEventListener('click', function (e) {
      visible = !visible
      if (visible) {
        div.setAttribute('data-visible', '')
        input.type = 'text'
      } else {
        div.removeAttribute('data-visible')
        input.type = 'password'
      }
    })
  }

  get name() {
    if (this.hasAttribute('name')) {
      return this.getAttribute('name')
    }
  }

  set name(val) {
    if (val) {
      this.setAttribute('name', val);
    } else {
      this.removeAttribute('name');
    }
  }

  get value() {
    return this.input.value
  }

  set value(val) {
    this.input.value = val
  }
}

customElements.define('show-password', ShowPassword)

Saját komponens

Shadow DOM
Probléma: form nem küldi el az input mezőt

class ShowPassword extends HTMLElement {
  constructor() {
    super()
  }
  connectedCallback() {}
    const name = this.getAttribute('name')
    const value = this.getAttribute('value')
    
    ✒>const template = document.querySelector('#show-password')
    const content = template.content.cloneNode(true)
    const shadowRoot = this.attachShadow({ mode: 'open' })
    shadowRoot.appendChild(content)<✒

    ✒>const parent = this.parentNode
    const hidden = document.createElement('input')
    hidden.type = 'hidden'
    hidden.name = name
    parent.insertBefore(hidden, this)<✒

    const div = ✒>shadowRoot<✒.querySelector('div')
    const button = shadowRoot.querySelector('button')
    const input = shadowRoot.querySelector('input')
    this.input = input
    input.name = name
    input.value = value

    let visible = false;
    
    button.addEventListener('click', function (e) {
      visible = !visible
      if (visible) {
        div.setAttribute('data-visible', '')
        input.type = 'text'
      } else {
        div.removeAttribute('data-visible')
        input.type = 'password'
      }
    })

    ✒>input.addEventListener('input', function (e) {
      hidden.value = this.value
    })<✒
  }

  get name() {
    if (this.hasAttribute('name')) {
      return this.getAttribute('name')
    }
  }

  set name(val) {
    if (val) {
      this.setAttribute('name', val);
    } else {
      this.removeAttribute('name');
    }
  }

  get value() {
    return this.input.value
  }

  set value(val) {
    this.input.value = val
  }
}

customElements.define('show-password', ShowPassword)

Stílusok

<template id="show-password">
  <style>
    div {
      white-space: nowrap;
      display: inline-flex;
    }
  
    input {
      line-height: 2;
      border: 1px solid gray;
      border-radius: 0.25em;
      flex: 1;
    }
  
    button {
      border: 1px solid gray;
      border-radius: 0.25em;
      background-color: hsl(200, 50%, 90%);
    }
  
    button::after {
      content: '⚪';
    }
  
    div[data-visible] button::after {
      content: '⚫';
    }
  
    input:not(:last-child) {
      border-top-right-radius: 0;
      border-bottom-right-radius: 0;
      border-right: 0;
    }
  
    button:last-child {
      border-top-left-radius: 0;
      border-bottom-left-radius: 0;
    }
  </style>
  <div>
    <input type="password">
    <button type="button"></button>
  </div>
</template>

Progresszív fejlesztés komponenssel

<form action="" method="get">
  <show-password>
    <input type="password" name="password1" value="secret1">
  </show-password>
</form>
<template id="show-password">
  <style>
    div {
      white-space: nowrap;
      display: inline-flex;
    }
  
    ✒>::slotted(input) {
      line-height: 2;
      border: 1px solid gray;
      border-radius: 0.25em;
      flex: 1;
    }<✒
  
    button {
      border: 1px solid gray;
      border-radius: 0.25em;
      background-color: hsl(200, 50%, 90%);
    }
  
    button::after {
      content: '⚪';
    }
  
    div[data-visible] button::after {
      content: '⚫';
    }
  
    input:not(:last-child) {
      border-top-right-radius: 0;
      border-bottom-right-radius: 0;
      border-right: 0;
    }
  
    button:last-child {
      border-top-left-radius: 0;
      border-bottom-left-radius: 0;
    }
  </style>
  <div>
    ✒><slot></slot><✒
    <button type="button"></button>
  </div>
</template>

Progresszív fejlesztés komponenssel

class ShowPassword extends HTMLElement {

  constructor() {
    super()
  }
  connectedCallback() {
    const template = document.querySelector('#show-password')
    const content = template.content.cloneNode(true)
    const shadowRoot = this.attachShadow({ mode: 'open' })
    shadowRoot.appendChild(content)

    const div = shadowRoot.querySelector('div')
    const button = shadowRoot.querySelector('button')
    const input = this.querySelector('input')

    let visible = false;
    
    button.addEventListener('click', function (e) {
      visible = !visible
      if (visible) {
        div.setAttribute('data-visible', '')
        input.type = 'text'
      } else {
        div.removeAttribute('data-visible')
        input.type = 'password'
      }
    })
  }
}

customElements.define('show-password', ShowPassword)

Beépített elem

<form action="" method="get">
  <input type="password" name="password1" value="secret" is="show-password">
  <input type="password" name="password2" value="secret" data-show-password>
</form>
<style>/* Again global styles without template */</style>
class ShowPassword extends HTMLInputElement {
  constructor() {
    super()
  }
  connectedCallback() {
    window.onload = () => {
      // Nem működik Shadow DOM-mal
      const input = this

      const button = document.createElement('button')
      button.type = 'button'

      const div = document.createElement('div')
      div.classList.add('show-password')

      const parent = this.parentElement
      parent.insertBefore(div, this)

      div.appendChild(this)
      div.appendChild(button)

      let visible = false;

      button.addEventListener('click', function (e) {
        visible = !visible
        if (visible) {
          div.setAttribute('data-visible', '')
          input.type = 'text'
        } else {
          div.removeAttribute('data-visible')
          input.type = 'password'
        }
      })
    }
  }
}

customElements.define('show-password', ShowPassword, { extends: 'input' })

Linkek

Végszó

  • Web komponensek
    • Custom elements
    • Templates
    • Shadow DOM
  • Progresszív fejlesztésre használható
<show-password>
  <input type="password" name="password1" value="secret">
</show-password>

<input type="password" name="password1" value="secret" is="show-password">