# đŸ‡«đŸ‡· 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 (opens new window) ou Horacio Gonzalez (opens new window) (mon maĂźtre web components) m'avait expliquĂ© que Pika (opens new window) 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 (opens new window), 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/ (opens new window):

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 (opens new window)". Voir l'explication par ici: "Define styles in a static styles property (opens new window)", 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 (opens new window))

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:

# 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 (opens new window)

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 (opens new window) 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