🇫🇷 LitElement, sans Polymer CLI et avec de vrais fichiers CSS

Je continue mon apprentissage avec LitElement, mais jusqu'ici j'ai plus passé de temps sur 2 sujets liés aux paramétrages de mon application de départ:

  • Comment intégrer facilement une webapp LitElement dans un projet Express ou Vert-x (ou tout autre framework)?
  • Comment utiliser un framework css existant (comme BootStrap par exemple) avec LitElement, sachant que les frameworks css du moment n'ont pas été pensés pour jouer "simplement" avec le shadow dom?

Mais voyons déjà comment régler le premier sujet.

Comment oublier la Polymer CLI

La Polymer CLI impose une structure de projet très spécifique, qui rend difficile l'intégration d'une WebApp LitElement dans un projet web de type Express ou Vert-x, ou alors il faut faire quelques gymnastiques "scriptiques" qui n'étonnent plus les "JavaScripteurs", mais qui me piquent les yeux et me donne le mal de mer. Sans compter le fait, que d'être obligé de builder toute une application JavaScript juste pour vérifier que j'ai bien corrigé une typo, je trouve ça un peu "sur dimensionné"...

Mon problème

désolé, c'est un peu long

Le plus souvent la structure de mes applications Express est la suivante:

.
├── index.js # mon application Express
├── public
│   ├── index.html # ma webapp JavaScript
│   ├── js
│   ├── css
│   └── components

Si vous utilisez LitElement de façon "classique" (avec la Polymer CLI), vous aurez une structure qui va plutôt ressembler à ceci:

.
├── index.html # webapp LitElement
├── css
├── src
│   ├── main-application.js
│   └── my-title.js

Et pour pouvoir l'intégrer dans un autre projet, il faut "builder", cela va générer votre web app dans build/es6-bundled et ensuite vous pourrez mettre le contenu dans le répertoire public de votre projet Express.

.
├── build
│   └── es6-bundled
│       ├── css
│       └── index.html
├── index.html # webapp LitElement
├── css
├── src
│   ├── main-application.js
│   └── my-title.js

En mode "développement" pour avoir les modifications en "temps réel", la Polymer CLI fournit son propre serveur http 😀... mais si votre webapp "appelle" des services de votre application web Express qui est elle même un serveur http, ça se complique 😡: vous devrez tenir compte du fait qu'en mode développement vos appels ressembleront à:

fetch("http://my-express-application.test/api/say-hello")

cela va probablement vous obliger à régler 2,3 choses liées à CORS 😛

et en mode production à ceci:

fetch("/api/say-hello")

ben oui, on est dans le même projet

"Tout ça pour dire" que c'est tout même super c%%%%t en termes d'expérience utilisateur/développeur pour afficher une page html...

La solution

J'ai des notes partout (papier, fichier texte, ...) parce que je n'ai pas de mémoire, et je suis retombé sur l'une d'entre elles qui date du dernier DevFestLille ou Horacio Gonzalez(mon maître web components) m'avait expliqué que Pika pouvait régler mon problème. Pika pour simplifier, c'est un outil qui prend toutes les dépendances des modules node pour les transformer en un fichier JavaScript que vous pouvez facilement inclure "à l'ancienne" dans votre page html, et vous n'aurez plus besoin de "builder" pendant le développement 🎉

Cette excellente vidéo vous explique comment faire ceci en détail: https://www.youtube.com/watch?v=bCsS-M4a1rg&feature=youtu.be, mais je vous explique tout de suite (de manière courte) comment le faire pour répondre à "ma problématique"

Initialisation de ma webapp pour faciliter ma vie de développeur

Structure du projet

Je crée une structure de projet comme celle-ci:

.
├── index.js # mon application Express
├── public
│   ├── index.html # ma webapp LitElement
│   ├── js
│   ├── css
│   └── components
│       ├── MainApplication.js
│       ├── AppTitle.js
│       └── AppSubtitle.js
├── package.json

Le fichier package.json va contenir tout ce dont j'ai besoin pour installer les outils nécessaires:

{
  "name": "lit-simple",
  "main": "index.js",
  "scripts": {
    "start": "node index.js"
  },
  "dependencies": {
    "@pika/web": "^0.5.3",
    "express": "^4.17.1",
    "lit-element": "^2.2.1"
  }
}

Le fichier index.js est mon application Express (donc mon serveur d'application, ou mon serveur http)

const express = require('express')

const app = express()
const port = process.env.PORT || 9090

app.use(express.static('public'))
app.use(express.json());

app.listen(port, () => console.log(
  `🌍 listening on port ${port}!`
))

Installation de départ

Dans votre répertoire projet, tapez les commandes suivantes:

npm install
npx @pika/web --dest /public/web_modules

c'est npx @pika/web --dest /public/web_modules qui va faciliter votre vie de développeur. Cette commande va créer un répertoire web_modules et générer dans ce répertoire le fichier lit-elements.js directement utilisable dans votre page index.html:

.
├── index.js # mon application Express
├── public
│   ├── index.html # ma webapp LitElement
│   ├── js
│   ├── css
│   ├── components
│   │   ├── MainApplication.js
│   │   ├── AppTitle.js
│   │   └── AppSubtitle.js
│   └── web_modules
│   │   ├── import-map.json
│   │   └── lit-element.js # tout est là  😍
├── package.json

Nous pouvons maintenant écrire nos composants

Les composants LitElement

AppTitle.js

import { LitElement, html } from '../web_modules/lit-element.js'

export class AppTitle extends LitElement {
    
  render() {
    return html`<h1>🖖 live long and prosper 🌍</h1>`
  }
}

customElements.define('app-title', AppTitle)

AppSubtitle.js

import { LitElement, html } from '../web_modules/lit-element.js'

export class AppSubtitle extends LitElement {
    
  render() {
    return html`<h2>made with 🧡 and 🍵</h2>`
  }
}

customElements.define('app-subtitle', AppSubtitle)

MainApplication.js

import { LitElement, html } from '../web_modules/lit-element.js'
import {} from './AppTitle.js'
import {} from './AppSubtitle.js'

export class MainApplication extends LitElement {

  static get styles() { return [window.simpleCssResult] }
  
  render() {
    return html`
      <div class="container">
        <div>
          <app-title></app-title>
          <app-subtitle></app-subtitle>
        </div>
      </div>
    `
  }
}

customElements.define('main-application', MainApplication)

La PAGE index.html

index.html

<!DOCTYPE html>
<head>
  <meta charset="utf-8">
  <meta http-equiv="x-ua-compatible" content="ie=edge">
  <title>Hello World!</title>
  <meta name="viewport" content="width=device-width, initial-scale=1">
</head>
<body>
  <main-application></main-application>
  
  <script type="module" src="./components/MainApplication.js"></script>
</body>
</html>

On lance le tout

Lancez un npm start ou un node index.js et allez sur http://localhost:9090/:

text

Vous pouvez modifier vos composants et faire un "refresh" de la page, les modifications seront prises en charge sans avoir aucun build à faire.

Maintenant, il faut rendre cette page un peu plus "jolie"

Maintenant, la partie sur le CSS 🥶

Alors, styler les webcomponents n'est pas forcément la chose la plus facile à faire. Le projet LitElement recommande d'utiliser le système des "Constructable Stylesheets". Voir l'explication par ici: "Define styles in a static styles property", où grosso modo il est expliqué que les "styles statiques" sont plus performants:

We recommend using static styles for optimal performance. LitElement uses Constructable Stylesheets in browsers that support it, with a fallback for browsers that don’t. Constructable Stylesheets allow the browser to parse styles exactly once and reuse the resulting Stylesheet object for maximum efficiency.

Pour faire court, cela signifie que vous allez "mettre votre css dans du js". Ok, c'est très court, alors voici un exemple appliqué à LitElement:

Vous définissez vos styles dans un fichiers JavaScript (ou plusieurs), comme cela:

main-styles.js

import { css } from 'lit-element'

export const style = css`
  .container {
    min-height: 100vh;
    display: flex;
    justify-content: center;
    align-items: center;
    text-align: center;
  }
  .title {
    font-family: "Source Sans Pro", "Helvetica Neue", Arial, sans-serif;
    display: block;
    font-weight: 300;
    font-size: 100px;
    color: #35495e;
    letter-spacing: 1px;
  }
`

donc littéralement, vous avez collé votre css dans une string et dans du JavaScript et css va vous transformer ça en un objet CSSResult qui est une représentation "JavaScript" de votre feuille de style. (cf. https://lit-element.polymer-project.org/api/classes/lib_css_tag.cssresult.html)

Et vous pourrez l'utiliser comme ceci dans vos composants LitElement:

import { LitElement, html } from 'lit-element'
import {style} from './main-styles.js' // #1

export class MyTitle extends LitElement {

  static get styles() { return [style] } // #2

  render(){
    return html`<h1 class="title">👋 Hello 🌍</h1>` // #3
  }
}
customElements.define('my-title', MyTitle)
  1. vous importez votre style
  2. vous l'associez aux styles de votre composant
  3. vous l'utilisez

C'est facile, est avec import {style} from './main-styles.js' vous pouvez facilement partager vos styles entre vos composants. Mais ...

Nouveau problème: comment j'utilise un framework css existant?

Alors vous pourriez déclarer les feuilles de styles à chaque fois pour chaque composant, dirextement dans leur code html (ce que j'ai fait à mes débuts avec LitElement), cela fonctionne, mais c'est loin d'être optimisé (vous chargez les feuilles de styles à chaque fois 😱).

Mais les frameworks css existants ne sont pas au format Constructable Stylesheets 😢 et le faire vous même à la main, comment dire ...

Vous avez différentes solutions:

  • Vous pouvez utiliser la solution de Horacio Gonzalez qui fournit l'outillage pour cela par exemple pour Bootstrap avec granite-bootstrap ou pour d'autres frameworks css granite-lit-bulma, granite-lit-spectre
  • Ou vous pouvez ma solution (de faignasse), très courte et facile à mettre en oeuvre (moins optimisée au chargement, mais elle fonctionne bien tout de même)

Ma solution pour transformer directement les fichiers css en CSSResult

Tout d'abord, ajoutez un fichier simple.css à votre projet:

.
├── index.js # mon application Express
├── public
│   ├── index.html # ma webapp LitElement
│   ├── js
│   ├── css
│   │   └── simple.css # ma ✨ feuille de styles
│   ├── components
│   │   ├── MainApplication.js
│   │   ├── AppTitle.js
│   │   └── AppSubtitle.js
│   └── web_modules
│   │   ├── import-map.json
│   │   └── lit-element.js # tout est là  😍
├── package.json

avec le contenu suivant:

body {
  background-color: beige;
}

.container { 
  min-height: 100vh; 
  display: flex;
  justify-content: center; 
  align-items: center; 
  text-align: center; 
}
.title {
  font-family: "Source Sans Pro", "Helvetica Neue", Arial, sans-serif; 
  display: block; 
  font-weight: 300; 
  font-size: 100px; 
  color: #35495e; 
  letter-spacing: 1px; 
}
.subtitle { 
  font-family: "Source Sans Pro", "Helvetica Neue", Arial, sans-serif; 
  font-weight: 300; 
  font-size: 42px; 
  color: #526488; 
  word-spacing: 5px; 
  padding-bottom: 15px; 
}

Et on va faire faire le travail par le navigateur via la page index.html que nous allons modifier de la façon suivante:

ma méthode est largement inspirée de celle d'Horacio, mais je le fait dynamiquement

<!DOCTYPE html>
<head>
  <meta charset="utf-8">
  <meta http-equiv="x-ua-compatible" content="ie=edge">
  <title>Hello World!</title>
  <meta name="viewport" content="width=device-width, initial-scale=1">
</head>
<body>
  <main-application></main-application>
  
  <script type="module">
    import { css, unsafeCSS } from './web_modules/lit-element.js' // #1

    let styleSheetPromise = (cssPath, variableName) => fetch(cssPath) // #2
      .then(response => response.text())  // #3
      .then(cssRaw => {
        let cssResult = css`${unsafeCSS(cssRaw)}` // #4
        window[variableName] = cssResult // #5
      })
      .catch(error => console.log(`😡 [${variableName}]`, error))

    Promise.all([ // #6
      styleSheetPromise("/css/simple.css", "simpleCssResult") // #7
    ])
    .then(styleSheets => {
      import("./components/MainApplication.js") // #8
    })
        
  </script>

</body>
</html>  
  1. j'importe les éléments dont j'ai besoin pour retraiter ma feuille de stylme
  2. je crée une promise via une lambda à laquelle je passe le chemin du fichier css et un nom de variable qui me servira à "stocker" le résultat du traitement
  3. je "charge" le texte du fichier css
  4. je transforme le texte en un objet de type CSSResult
  5. je stocke cet objet dans une variable globale window.simpleCssResult que je pourrais utiliser dans mes composants
  6. j'appelle ma promise avec Promise.all, comme cela j'ai la possibilité de charger plusieurs feuilles de styles
  7. je définie la promise et la "passeé à Promise.all
  8. une fois la(les) feuille(s) de styles chargée(s) je charge mon composant principal MainApplication

Il ne me reste plus qu'à modifier mes composants de la manière suivante pour prendre en compte les styles:

MainApplication.js

import { LitElement, html } from '../web_modules/lit-element.js'
import {} from './AppTitle.js'
import {} from './AppSubtitle.js'

export class MainApplication extends LitElement {

  static get styles() { return [window.simpleCssResult] } // #1
  
  render() { // #2
    return html`
      <div class="container">
        <div>
          <app-title></app-title>
          <app-subtitle></app-subtitle>
        </div>
      </div>
    `
  }
}

customElements.define('main-application', MainApplication)
  1. je définis la propriété statique styles pour qu'elle retourne un tableau contenant ma variable globale: [window.simpleCssResult]
  2. je peux ensuite utiliser mes styles simplement

Et je modifie donc les autres composants de la même façon:

AppTitle.js

import { LitElement, html } from '../web_modules/lit-element.js'

export class AppTitle extends LitElement {
    
  static get styles() { return [window.simpleCssResult] }

  render() {
    return html`<h1 class="title">🖖 live long and prosper 🌍</h1>`
  }
}

customElements.define('app-title', AppTitle)

AppSubtitle.js

import { LitElement, html } from '../web_modules/lit-element.js'

export class AppSubtitle extends LitElement {
    
  static get styles() { return [window.simpleCssResult] }

  render() {
    return html`<h2 class="subtitle">made with 🧡 and 🍵</h2>`
  }
}

customElements.define('app-subtitle', AppSubtitle)

Et nous obtiendrons (après un refresh) cette magnifique page:

text

C'est plus joli, mais ... Nous avons encore un problème 🥵

Nouveau problème: il manque quelque chose...

Souvenez vous de cette partie du fichier css:

body {
  background-color: beige;
}

Normalement la couleur de fond de ma page devrait être beige, et elle est blanche! A aucun moment en fait le style de body est appliqué. Je purrais faire un include du fichier css dans ma page index.html, mais cela reviendrait à la charger 2 fois, ce qui n'est pas tip top.

Modifions notre code de chargement

Pour pouvoir utiliser notre objet CSSResult avec notre page index.html, nous allons utiliser la propriété adoptedStyleSheets qui permet d'appliquer à un document un objet CSSStyleSheet et il se trouve que notre objet CSSResult a une propriété styleSheet de type CSSStyleSheet qui contient tous les éléménts de notre feuille de style.

Vous pouvez aller lire le § "Using Constructed StyleSheets" de ce document https://developers.google.com/web/updates/2019/02/constructable-stylesheets

Mais revenons à nos modifications, qui tiennent en l'ajout de 2 lignes uniquement:

  <script type="module">
    import { css, unsafeCSS } from './web_modules/lit-element.js'

    let styleSheetPromise = (cssPath, variableName) => fetch(cssPath)
      .then(response => response.text())
      .then(cssRaw => {
        let cssResult = css`${unsafeCSS(cssRaw)}`
        window[variableName] = cssResult
        return cssResult.styleSheet // #1
      })
      .catch(error => console.log(`😡 [${variableName}]`, error))

    Promise.all([
      styleSheetPromise("/css/simple.css", "simpleCssResult")
    ])
    .then(styleSheets => { // #2
      document.adoptedStyleSheets = styleSheets // #3
      import("./components/MainApplication.js")
    })
  </script>
  1. Ma promise va retourner une valeur, qui est la propriété styleSheet de cssResult
  2. styleSheets est un Array qui contient tous les objets de type styleSheet retournés par les promises
  3. j'applique le tableau styleSheets de style au document courant

Et si vous sauvegardez et raffraichissez, vous avez enfin un fond beige pour votre page:

text

Et ce système fonctionne très bien avec Bootstrap (je vous donnerais un lien vers une démo à la fin de l'article).

Un dernier problème 😤... Facile à régler 😍

J'avais oublié de vous dire, adoptedStyleSheets ne fonctionne que dans Chrome (à partir de la version 73).

Heureusement, il existe un polyfill pour cela: https://github.com/calebdwilliams/construct-style-sheets#readme et il fonctionne très bien.

Il suffit d'insérer <script src="./js/adoptedStyleSheets.js"></script> juste avant votre script de chargement, et l'affaire est dans le sac (cela fonctionnera même avec Safari).

Quelques ressources

Avant de nous quitter

Démos

à venir: des starterkits avec probablement Bulma, Spectre, Semantic UI, ...

Quelques pages à lire

👋 bonne fin de week-end et bon courage pour lundi 😘

Last Articles

Last Updated: 9/1/2019, 12:58:56 PM