PolymerElement is dood, leve LitElement!

Hallo vrienden! We hebben eindelijk het hoofdonderwerp van deze serie bereikt:LitElement .

Een nieuwe klasse om webcomponenten te regeren

Om te begrijpen wat LitElement is en wat het voor ons doet, komen we terug op waar we in de vorige post gebleven waren. Laten we onthouden wat we de vorige keer deden, we gebruikten lit-html om een ​​webcomponent te maken die opnieuw kan worden weergegeven wanneer de waarde van de eigenschap van een component verandert.

Om dat te bereiken, moesten we een klasse maken met een constructor die verantwoordelijk was voor het maken van de schaduw-DOM en het reflecteren van de waarden van de HTML-attributen naar de componenteigenschappen. Om vervolgens de component opnieuw te laten renderen telkens wanneer een eigenschap verandert, moesten we een setter voor elk van hen schrijven en de lit-html aanroepen render functie binnen die setter.
En last but not least moesten we code schrijven om de HTML-eigenschappen en -attributen gesynchroniseerd te houden.

Dit alles resulteert in repetitieve code die toeneemt met het aantal eigenschappen dat de component heeft. Om een ​​betere ontwikkelaarservaring te creëren, is de Polymer team dacht dat het goed zou zijn om een ​​klas te hebben die al deze last voor ons oplost. Evenals ze maakten PolymerElement , maar deze keer moesten ze evolueren en profiteren van hun prachtige lit-html , dus het zou geen nieuwe versie zijn van PolymerElement , het moest helemaal nieuw zijn en daarom creëerden ze LitElement .

LitElement bevrijdt ons niet alleen van repetitieve code, het maakt ook lit-html nog efficiënter renderen door het asynchroon te laten gebeuren.

Dus, LitElement is een lichtgewicht klasse om webcomponenten te maken. Het verwerkt voor ons alle repetitieve code die nodig is om:

  • schaduw DOM gebruiken
  • HTML-attributen en componenteigenschappen synchroon houden
  • efficiënt renderen (met behulp van lit-html ) de component telkens wanneer een eigenschap verandert

Laten we eens kijken naar de minimale code die een LitElement behoeften:

// Import the LitElement base class and html helper function
import { LitElement, html } from 'lit-element';
// Import other element if it needed
import 'package-name/other-element.js';

// Extend the LitElement base class
class MyElement extends LitElement {

  /**
   * Implement `render` to define a template for your element.
   *
   * You must provide an implementation of `render` for any element
   * that uses LitElement as a base class.
   */
  render() {
    /**
     * `render` must return a lit-html `TemplateResult`.
     *
     * To create a `TemplateResult`, tag a JavaScript template literal
     * with the `html` helper function:
     */
    return html`
      <!-- template content -->
      <p>A paragraph</p>
      <other-element></other-element>
    `;
  }
}

// Register the new element with the browser.
customElements.define('my-element', MyElement);

Zoals je hebt gezien, is de enige vereiste om de render . te implementeren functie die de gebruikersinterface van het onderdeel tekent. Deze functie moet een TemplateResult . teruggeven en dat is omdat het de render . zal aanroepen functie geleverd door lit-html (merk op dat we ook de html importeren functie). Dit is erg belangrijk omdat alles wat we hebben gezien in eerdere berichten van lit-html is van toepassing op LitElement .

We kunnen LitElement samenvatten:in een zeer simplistische formule:

De renderfunctie van LitElement

De render functie is van groot belang in LitElement omdat het bepaalt hoe het onderdeel eruit zal zien. Wanneer u de gedefinieerde sjabloon ziet, moet u begrijpen hoe het onderdeel in elke situatie zal worden geverfd. Er is geen andere plaats waar de gebruikersinterface van het onderdeel kan worden gewijzigd of bijgewerkt. En wat meer is, wanneer een eigenschap verandert (de status van het onderdeel verandert) LitElement zal de render . aanroepen functie om de componentweergave bij te werken. Het blijkt dus dat de gebruikersinterface wordt uitgedrukt als functie van de toestand van het onderdeel.

Volgens deze functionele benadering moet de sjabloon worden geschreven als een zuivere functie van de eigenschappen van de component, zodat:

  • Het verandert niets aan de status van het onderdeel
  • Het heeft geen bijwerkingen
  • Het hangt alleen af ​​van de eigenschappen van het onderdeel
  • Het geeft altijd dezelfde waarde als de eigenschappen niet zijn gewijzigd

Dit resulteert in een geweldige ontwikkelaarservaring, omdat u zich geen zorgen hoeft te maken over het bijwerken van het onderdeel wanneer er iets is veranderd. Het wordt opnieuw weergegeven volgens de nieuwe status en de prestatie-impact van de weergave-actie heeft geen waarde vanwege de efficiëntie van lit-html plus de asynchrone optimalisatie toegevoegd door LitElement .

Zonder deze oplossing zouden we de component één keer (de eerste keer) hebben weergegeven en extra code hebben geschreven om een ​​deel van de component bij te werken voor elke mogelijke statuswijziging en in het algemeen zou deze code veel voorwaarden en overwegingen hebben gehad. Op de lange termijn wordt het bijwerken van de visuele weergave, afhankelijk van de veranderingen in de toestand van het onderdeel, een zware taak en veel code om te onderhouden.

Ook in de re-renderbenadering wordt de gebruikersinterface van de component declaratief en op één plaats gedefinieerd. De klassieke aanpak (update UI-onderdelen) is absoluut noodzakelijk en de logica is verdeeld over veel functies.

Onze eerste LitElement-component

Herinner je je de <password-checker> component die we hebben gemaakt met lit-html in het vorige bericht?

import { html, render } from 'lit-html';

class PasswordChecker extends HTMLElement {
  constructor() {
    super();
    this.attachShadow({mode: 'open'});
    this.password = this.getAttribute('password');
  }

  get password() { return this._password; }

  set password(value) {
    this._password = value;
    this.setAttribute('password', value);
    this.update();
  }

  update() {
    render(this.template(), this.shadowRoot, {eventContext: this});
  }

  isValid(passwd) {
    const re = /(?=.*\d)(?=.*[a-z])(?=.*[A-Z]).{4,}/;
    return re.test(passwd);
  }

  template() {
    return html`
      <span>Your password is <strong>${this.isValid(this.password) ? 'valid 👍' : 'INVALID 👎'}</strong></span>
      ${this.isValid(this.password) ?
        html`<div>Strength: <progress value=${this.password.length-3} max="5"</progress></div>` : ``}`;

  }
}

customElements.define('password-checker', PasswordChecker);

Laten we nu eens kijken hoe dat onderdeel wordt geïmplementeerd met behulp van LitElement .

import { LitElement, html } from 'lit-element';

class PasswordChecker extends LitElement {
  static get properties() {
    return {
      password: String
    }
  }

  isValid(passwd) {
    const re = /(?=.*\d)(?=.*[a-z])(?=.*[A-Z]).{4,}/;
    return re.test(passwd);
  }

  render() {
    return html`
      <span>Your password is <strong>${this.isValid(this.password) ? 'valid 👍' : 'INVALID 👎'}</strong></span>
      ${this.isValid(this.password) ?
        html`<div>Strength: <progress value=${this.password.length-3} max="5"</progress></div>` : ``}`;

  }
}

customElements.define('password-checker', PasswordChecker);

Het eerste dat opvalt is dat er geen constructor is. In dit geval hebben we het niet nodig. We hoeven geen schaduwwortel toe te voegen omdat LitElement doet dat voor ons. Wanneer LitElement een component rendert, roept het lit-html . aan 's render functie en die functie verwacht een argument dat een knooppunt is waar de component moet worden geschilderd. LitElement maakt de schaduwwortel voor de component en geeft deze door aan de lit-html 's render functie. Het lijkt erg op wat we deden in de update functie van de lit-html versie.
Als we om welke reden dan ook niet willen dat onze component schaduw-DOM gebruikt, kunnen we de createRenderRoot overschrijven functie en retourneer een this waarde.

createRenderRoot() {
  return this;
}

Hoewel LitElement doet veel dingen voor ons, het laat ons ook het standaardgedrag aanpassen door sommige functies te negeren. We zullen zien dat LitElement is erg flexibel.

Eigenschappen en update

Onze lit-html versie van <password-checker> had een zetter voor de eigenschap password en in die functie hebben we het HTML-attribuut bijgewerkt en de render . aangeroepen functie. LitElement doet hetzelfde voor ons. Alle briljante dingen gebeuren als we de getterfunctie properties definiëren :

  static get properties() {
    return {
      password: String
    }
  }

Elke eigenschap die binnen deze functie is gedefinieerd, wordt beheerd door LitElement zodat een wijziging van de waarde ervoor zorgt dat de component opnieuw wordt weergegeven.
Dus voor elke gedeclareerde eigenschap LitElement zal bieden:

  • een waargenomen attribuut
  • accessoires
  • hasChanged functie
  • fromAttribute functie
  • toAttribute functie

Laten we eens in detail bekijken wat ze zijn:

Waargenomen kenmerk

Stel dat uw component een eigenschap heeft met de naam birthYear , kunt u het kenmerk birthyear . gebruiken in de opmaak

<my-comp birthyear="1973">

en LitElement zal die waarde aan de eigenschap toewijzen, maar het converteert eerder de String waarde aan het type van de eigenschap (Number in dit geval) met de fromAttribute functie.

👉 Merk op dat de naam van het attribuut standaard de naam van de eigenschap is in kleine letters. U kunt het wijzigen met behulp van de attribute toets in de eigenschapsdefinitie:

static get properties() {
  return {
    birthYear: {
      type: Number,
      // the observed attribute will be birth-year instead of birthyear
      attribute: 'birth-year'
    }
  }
}

Hoewel standaard LitElement waarden van attributen naar eigenschappen doorgeeft, is het tegenovergestelde niet waar. Als u wilt dat een wijziging in een eigenschapswaarde wordt weergegeven in het HTML-kenmerk, moet u dit expliciet aangeven met de sleutel reflect .

static get properties() {
  return {
    birthYear: {
      type: Number,
      // the observed attribute will be birth-year instead of birthyear
      attribute: 'birth-year'
      reflect: true
    }
  }
}

isChanged

Het is een functie die controleert of de nieuwe waarde verschilt van de vorige waarde. In dat geval retourneert het true .

⚠️ Wees voorzichtig met waarden die objecten of arrays zijn, omdat de vergelijking op het hoogste niveau wordt gemaakt, het maakt geen oppervlakkige vergelijking, dus als u obj1 === obj2 evalueert je vergelijkt referenties. In dat geval moet u hasChanged over overschrijven om de juiste vergelijking te maken.

vankenmerk

Het is de functie die de String . converteert waarde van het waargenomen attribuut aan het werkelijke type van de eigenschap. U kunt uw aangepaste converter leveren door fromAttribute te overschrijven .

tokenmerk

Het is de functie die wordt gebruikt om de eigenschapswaarde om te zetten in een String waarde zodat deze kan worden toegewezen aan het waargenomen attribuut in de opmaakcode (HTML). Deze functie wordt gebruikt door LitElement wanneer de eigenschap is ingesteld op reflect .
Als je een aangepaste converter nodig hebt, overschrijf dan toAttribute .

Accessoires

LitElement genereert accessors, een getter en een setter , voor gedeclareerde eigenschappen. In de setter vertrouwt bijna alle 'magie' waardoor de component opnieuw wordt weergegeven wanneer een eigenschap verandert. Eerst wordt gecontroleerd of de eigenschap is gewijzigd (roep hasChanged op functie) en als dat het geval is, wordt een UI-update geactiveerd.
Ook als de eigenschap wordt weerspiegeld in een attribuut, zal de setter het waargenomen attribuut bijwerken met behulp van de functie toAttribute .

Als u uw eigen getter opgeeft of setter of beide, dan LitElement maakt geen accessor voor die eigenschap. Houd er rekening mee dat als je je eigen setter schrijft en je wilt dat een wijziging een re-render veroorzaakt, je dezelfde dingen moet doen als de getter van **LitElement**.
U kunt ook automatisch gegenereerde getter en setter vermijden met de sleutel noAccessor .

static get properties() {
  return { birthYear: { type: Number, noAccessor: true } };
}

Ik vat deze punten samen in een voorbeeld:

// properties getter
static get properties() {
  return {
    // by default, every declared property: 
    //  - has an observed attribute,
    //  - when the attribute changes, it updates the property
    //  - has a getter and a setter
    //  - changes in value triggers a render update
    //  - has a default hasChanged function
    //  - has default converters: fromAttribute, toAttribute
    //  - all of this can be customized
    firstName: { type: String }, // type is the minimum required information
    lastName:  { type: String,
                 attribute: 'last-name'
               },
    enrolled:  { type: Boolean },
    address:   { type: Object,
                 reflect: false,
                 noAccessor: true,
                 hasChanged(newValue, oldValue) {
                    return newValue.zipCode != oldValue.zipCode;
                 }
               },
    age:       {
                 converter: {
                   toAttribute(value) {
                     return String(value);
                   }
                   fromAttribute(value) {
                     return Number(value);
                   }
                }
    }
  };
}

Als laatste opmerking merken we op dat de properties getter lijkt erg op de properties getter die we gebruikten in PolymerElement maar het LitElement versie mist de volgende functies:

beginwaarde

In PolymerElement 's properties getter kunnen we een beginwaarde toewijzen aan een eigenschap, maar dat is niet mogelijk in LitElement , moeten we dat in de constructor doen.

// PolymerElement 
static get properties() {
  return { birthYear: { type: Number, value: 1973 } };
}

// LitElement
constructor() {
  super(); // Don't forget to call super() !!!
  this.birthYear = 1973;
}

waargenomen eigenschappen

LitElement heeft geen waargenomen attributen. U kunt een setter gebruiken om acties uit te voeren wanneer de eigenschap verandert.

// PolymerElement 
static get properties() {
  return { birthYear: { type: Number, observer: '_yearChanged' } };
}

// LitElement
set birthYear(value) {
  // Code to check if property hasChanged
  // and request UI update should go here
  // ...
  this._birthYear = value;  // private _birthYear with getter birthYear
  this._yearChanged();
}

berekende eigenschappen

LitElement heeft geen berekende eigenschappen. Gebruik getters om hetzelfde resultaat te bereiken.

// PolymerElement 
static get properties() {
  return { birthYear: { type: Number },
           age: { type: Number, computed: '_computeAge(birthYear)' }
 };
}

// LitElement
get age() {
  return (new Date()).getFullYear() - this.birthYear;
}

Nou, we hebben er al genoeg van.
Tot zover het eerste deel over LitElement . In de volgende post zal ik je in detail vertellen hoe het asynchrone weergaveproces en de levenscyclus van een LitElement onderdeel.
Tot ziens!