Magazine Gadgets

Lier les composants Web légers de Lit et WebR pour la réactivité de Vanilla JS

Publié le 26 mars 2023 par Mycamer

Voir en direct Avant la lecture!

Il s’agit d’une reproduction Lit + WebR du Application de démonstration OG Shiny

Allumé est une bibliothèque javascript qui facilite un peu le travail avec Composants Webet est particulièrement bien adapté aux environnements réactifs.

Mes récentes expériences hack-y WebR ont utilisé Récif qui est un encore plus léger-weight javascript web components-esque library, et c’est un peu plus (initialement) accessible que Lit. L’accent mis par Lit sur “les composants Web d’abord” signifie que vous êtes en quelque sorte forcé dans une structure, ce qui est bien, car les éléments réactifs peuvent exploser s’ils ne sont pas bien gérés.

Je pense aussi que les gens de Shiny pourraient se sentir un peu plus chez eux.

C’est la structure de notre exemple Lit + WebR (Je n’arrête pas de modifier cette mise en page, ce qui frustre probablement beaucoup de gens )_

lit-webr
├── css
│   └── style.css             # you know what this is
├── favicon.ico               # when developing locally I want my icon
├── index.html                # you know what this is
├── main.js                   # the core experiment runner
├── md
│   └── main.md               # the core experiment markdown file
├── r-code
│   └── region-plot.R         # we keep longer bits of R code in source files here
├── r.js                      # place for WebR work
├── renderers.js              # these experiment templates always use markdown
├── themes
│   └── ayu-dark.json         # my fav shiki theme
├── utils.js                  # handy utilities (still pretty bare)
├── wc
│   ├── region-plot.js        # 👉🏼 WEB COMPONENT for the plot
│   ├── select-list.js        # 👉🏼               for the regions popup menu
│   └── status-message.js     # 👉🏼               for the status message
├── webr-serviceworker.js.map # not rly necessary; just for clean DevTools console
└── webr-worker.js.map        # ☝🏽

Beaucoup de choses ont changé (grâce à l’utilisation de Lit) depuis la dernière fois que vous avez vu l’une de ces expériences. Tu devrais parcourir la source avant de continuer.

Le noyau se transforme en index.html sommes-nous juste en train d’enregistrer nos composants Web :

<script type="module" src="https://securityboulevard.com/2023/03/linking-lits-lightweight-web-components-and-webr-for-vanilla-js-reactivity/amp/./wc/select-list.js"></script>
<script type="module" src="./wc/region-plot.js"></script>
<script type="module" src="./wc/status-message.js"></script>

Nous pourrait les avons regroupés dans un seul fichier JS et les avons réduits, mais nous gardons les choses simples pour ces expériences.

Les Composants Web (« composants » désormais) deviennent un « citoyen égal » en termes de HTMLElementset ils sont enregistrés directement dans le DOM.

Le prochain grand changement est dans ce fichier (le rendu main.md), où nous utilisons ces nouveaux composants au lieu de nos <div>s. La version réduite de celui-ci est essentiellement:

<status-message id="status"></status-message>

<region-plot id="regionsOutput" svgId="lit-regions">
  <select-list label="Select a region:" id="regionsInput"></select-list>
</region-plot>

Le intention de ces éléments est assez clair (beaucoup plus clair que le <div> versions), ce qui est un aspect des composants que j’aime beaucoup.

Vous remarquerez également que les composants sont - (tiret) fou. Cela fait partie de la spécification des composants Web et est obligatoire.

Nous utilisons des composants assez ciblés. Ce que je veux dire par là, c’est qu’ils ne sont pas très réutilisables dans d’autres projets sans copier/coller. Une partie de cela est sur moi puisque je ne fais pas de trucs sur le Web pour gagner ma vie. Il s’agissait également de faciliter la démonstration de leur utilisation avec WebR.

Avec plus de code modulaire, plus la séparation de gros morceaux de source R signifie que nous pouvons réellement mettre l’intégralité de main.js ici (J’ai supprimé toutes les annotations ; veuillez consulter main.js de les voir; nous expliquerons une chose en profondeur ici, par rapport à là, cependant.):

import { renderMarkdownInBody } from "./renderers.js";
import * as d3 from "https://cdn.jsdelivr.net/npm/d3@7/+esm";

await renderMarkdownInBody(
  `main`,
  "ayu-dark",
  [ 'javascript', 'r', 'json', 'md', 'xml', 'console' ],
  false
)

let message = document.getElementById("status");
message.text = "WebR Loading…"

import * as R from "./r.js";

message.text = "Web R Initialized!"

await R.webR.installPackages([ "svglite" ])

await R.library(`svglite`)
await R.library(`datasets`)

const regionRender = await globalThis.webR.evalR(await d3.text("r-code/region-plot.R"))

message.text = "{svglite} installed"

const regions = document.getElementById("regionsInput")
const plotOutput = document.getElementById("regionsOutput")

regions.options = await (await R.webR.evalR(`colnames(WorldPhones)`)).toArray()
plotOutput.region = regions.options[ 0 ]
plotOutput.renderFunction = regionRender
plotOutput.render()

message.text = "Ready"

Je veux parler un peu de cette ligne de main.js:

const regionRender = await globalThis.webR.evalR(
  await d3.text("r-code/region-plot.R")
)

Cela récupère la source du fichier R unique que nous avons dans cette application, l’évalue et renvoie la valeur évaluée (qui est un R function objet) en javascript. C’est le scénario :

renderRegions <- function(region, id = "region-plot") {

  # our base plot theme

  list(
    panel.fill = "#001e38",
    bar.fill = "#4a6d88",
    axis.color = "#c6cdd7",
    label.color = "#c6cdd7",
    subtitle.color = "#c6cdd7",
    title.color = "#c6cdd7",
    ticks.color = "#c6cdd7",
    axis.color = "#c6cdd7"
  ) -> theme

  # get our svg graphics device amp'd
  s <- svgstring(width = 8, height = 4, pointsize = 8, id = id, standalone = FALSE)

  # setup theme stuff we can't do in barplot()
  par(
    bg = theme$panel.fill,
    fg = theme$label.color
  )

  # um, it's a barplot
  barplot(
    WorldPhones[, region],
    main = region,
    col = theme$bar.fill,
    sub = "Data from AT&T (1961) The World's Telephones",
    ylab = "Number of Telephones (K)",
    xlab = "Year",
    border = NA,
    col.axis = theme$axis.color,
    col.lab = theme$label.color,
    col.sub = theme$subtitle.color,
    col.main = theme$title.color
  )

  dev.off()

  # get the stringified SVG
  plot_svg <- s()

  # make it responsive
  plot_svg <- sub("width="\\d+(\\.\\d+)?pt"", "width="100%"", plot_svg)
  plot_svg <- sub("height="\\d+(\\.\\d+)?pt"", "", plot_svg)

  # return it
  plot_svg

}

Cette fonction R est appelable directement depuis javascript. Créer cette capacité était super brillant de la part de George (le parrain de WebR). Nous finissons par le donner au composant qui trace le barplot (voir region-plot.js) ici:

plotOutput.renderFunction = regionRender

On prend un peu d’avance sur nous, puisque nous n’avons pas encore parlé des composants. Nous allons le faire, en commençant par le plus simple à grok, qui est en status-message.js et est représenté par le <status-message></status-message> étiqueter.

Ces composants Lit personnalisés obtiennent tout HTMLElement a, plus tout ce que vous fournissez. Je ne vais pas montrer toute la source pour status-message.js ici tel qu’il est (légèrement) annoté. Nous ne couvrirons que les principes fondamentaux, car les composants Lit ont également beaucoup de choses à faire et nous n’utilisons qu’une fraction de ce qu’ils peuvent faire. Voici l’aperçu de ce qu’il y a dans notre status-message:

export class StatusMessage extends LitElement {
  static properties = { /* things you can assign to and read from */ }
  static styles = [ /* component-scoped CSS */ ]
  constructor() { /* initialization bits */  render() { /* what gets called when things change */ }
}
// register it
customElements.define('status-message', StatusMessage);

Notre status-message properties a juste une propriété :

static properties = {
  text: {type: String}, // TypeScript annotations are requried by Lit
};

Cela signifie que lorsque nous faisons :

let message = document.getElementById("status");
message.text = "WebR Loading…"

nous trouvons notre composant dans le DOM, puis mettons à jour la propriété que nous avons définie. Cela déclenchera render() à chaque fois, et utilisez n’importe quel CSS restreint aux composants que nous avons configuré.

Les choses deviennent un peu plus compliqué dans select-list.js. Nous n’aborderons que les faits saillants, en commençant par le properties:

static properties = {
  id: { type: String },    // gives us easy access to the id we set
  label: { type: String }, // lets us define the label up front
  options: { type: Array } // where the options for the popup will go
};

Si vous vous souvenez, voici comment nous les avons utilisés dans la source :

<region-plot id="regionsOutput" svgId="lit-regions">
  <select-list label="Select a region:" id="regionsInput"></select-list>
</region-plot>

Le id et label Les propriétés seront disponibles immédiatement après la création de l’élément personnalisé.

Nous commençons option avec une liste vide :

constructor() {
  super()
  this.options = []
}

Notre render() la fonction place le <label> et <select> balises dans le DOM et finira par remplir le menu une fois qu’il aura des données :

render() {
  const selectId = `select-list-${this.id}`;
  return html`
  <label for="${selectId}">${this.label} 
    <select id="${selectId}" @change=${this._dispatch}>
      ${this.options.map(option => html`<option>${option}</option>`)}
    </select>
  </label>
  `;
}

Leur utilisation intelligente des chaînes de modèle JS facilite grandement la concaténation de chaînes laide.

Ce html dans le return fait beaucoup de travail, et pas seulement de renvoyer du texte. Vous devez lire Lit pour obtenir plus d’informations car c’est déjà trop long.

La façon dont nous avons câblé la réactivité dans mes exemples Reef me semblait maladroite, et même la meilleure façon de le faire dans Reef me semblait maladroite. C’est vraiment agréable en Lit. Ce petit ajout à la <select> étiqueter:

@change=${this._dispatch}

dit d’appeler une fonction nommée _dispatch chaque fois que la valeur change. C’est aussi dans le composant:

_dispatch(e) {
  const options = {
    detail: e.target,
    bubbles: true,
    composed: true,
  };
  this.dispatchEvent(new CustomEvent(`regionChanged`, options));
}

Nous configurons une structure de données, puis déclenchons un événement personnalisé que notre composant de tracé écoutera. Nous venons de les relier d’un côté. Il ne nous reste plus qu’à remplir le options list, en utilisant des données de R :

const regions = document.getElementById("regionsInput")
regions.options = await (await R.webR.evalR(`colnames(WorldPhones)`)).toArray()

Cela fera apparaître le menu.

En écoutant le main.js configuration de l’intrigue :

const plotOutput = document.getElementById("regionsOutput")
plotOutput.region = regions.options[ 0 ]
plotOutput.renderFunction = regionRender
plotOutput.render()

on voit qu’on :

  • trouver l’élément
  • définir la région par défaut sur la première dans la fenêtre contextuelle
  • assignez-lui notre fonction de rendu R-created
  • et demandez-lui gentiment de rendre en ce moment plutôt que d’attendre que quelqu’un sélectionne quelque chose

L’autre côté de cela (region-plot.js) est un peu plus complexe. Commençons par le properties:

static properties = {
  // we keep a local copy for fun
  region: { type: String },

  // this is where our S
  asyncSvg: { type: String },

  // a DOM-accessible id string (cld be handy)
  svgId: { type: String },

  // the function to be called to render
  renderFunction: { type: Function }
};

WebR === “async”, c’est pourquoi vous voyez que asyncSvg. Async est génial et aussi une douleur. Il y a bien plus de fonctions dans region-plot.js par conséquent.

Nous devons avoir quelque chose dans renderFunction avant la mise sous tension de WebR car le composant sera actif avant cela. Nous allons lui donner une fonction asynchrone anonyme qui renvoie un SVG vide.

this.renderFunction = async () => `<svg></svg>`

Curieusement, notre render fonction n’appelle pas la fonction de traçage. C’est ce que ça fait:

render() {
  return html`
  <div>
  <slot></slot>
  ${unsafeSVG(this.asyncSvg)}
  </div>`;
}

Ce morceau :

<slot></slot>

dit juste render() pour prendre tout ce qui est enveloppé dans l’étiquette et le pousser là (c’est un peu plus puissant que cela).

Ce morceau :

${unsafeSVG(this.asyncSvg)}

prend simplement notre chaîne avec SVG et fait savoir à Lit que nous voulons vraiment vivre dangereusement. Lit fait de son mieux pour vous aider à éviter les problèmes de sécurité et les SVG sont dangereux.

Alors, comment faire nous rendons l’intrigue? Avec deux nouvelles fonctions :

// this is a special async callback mechanism that 
// lets the component behave normally, but do things 
// asynchronously when necessary.
async connectedCallback() {

  super.connectedCallback();

  // THIS IS WHERE WE CALL THE PLOT FUNCTION
  this.asyncSvg = await this.renderFunction(this.region, this.svgId);

  // We'll catch this event when the SELECT list changes or when
  // WE fire it, like we do down below.
  this.addEventListener('regionChanged', async (e) => {
    this.region = e.detail.value;
    const res = await this.renderFunction(this.region, this.svgId);
    // if the result of the function call is from the R function and
    // not the anonymous one we initialized the oject with
    // we need to tap into the `values` slot that gets return
    // with any call to WebR's `toJs()`
    if (res.values) this.asyncSvg = res.values[ 0 ] ;
  });

}

// special function that will get called when we 
// programmatically ask for a forced update
performUpdate() {
  super.performUpdate();
  const options = {
    detail: { value: this.region },
    bubbles: true,
    composed: true,
  };

  // we fire the event so things happen async
  this.dispatchEvent(new CustomEvent(`regionChanged`, options));
}

Cela a terminé le câblage du côté du traçage.

Servir des desserts “justes” (localement)

je très recommande d’utiliser le petit mais génial alimenté par Rust mini-serviette pour servir les choses localement pendant le développement :

miniserve \
  --header "Cache-Control: no-cache; max-age=300" \
  --header "Cross-Origin-Embedder-Policy: require-corp" \
  --header "Cross-Origin-Opener-Policy: same-origin" \
  --header "Cross-Origin-Resource-Policy: cross-origin" \
  --index index.html \
  .

Vous pouvez l’utiliser (une fois installé) à partir du local justefichierqui a (actuellement) quatre actions sémantiquement nommées :

  • installer-miniserve
  • servir
  • rsync
  • github

Vous devrez modifier le chemin si vous décidez de l’utiliser.

AILETTE

Je me rends compte que c’est un peu à prendre en compte et, comme je le dis sans cesse, la plupart des gens feraient mieux d’utiliser WebR dans Shiny (lorsqu’il est disponible) ou Quarto.

Lit nous donne de la réactivité sans le ballonnement qui vient pour le trajet avec Vue et React, nous pouvons donc rester au pays de Vanilla JS. Vous remarquerez qu’il n’y a pas de “npm” ou “bundling” ou “rollup” ici. Vous pouvez coder dans l’environnement de votre choix, et servir des pages alimentées par WebR est alors aussi simple qu’un rsync.

Déposer les problèmes à le dépôt.

*** Ceci est un blog syndiqué du Security Bloggers Network de rud.est Rédigé par hrbrmstr. Lire le message d’origine sur : https://rud.is/b/2023/03/25/linking-lits-lightweight-web-components-and-webr-for-vanilla-js-reactivity/

to securityboulevard.com


Abonnez-vous à notre page Facebook: https://www.facebook.com/mycamer.net
Pour recevoir l’actualité sur vos téléphones à partir de l’application Telegram cliquez ici: https://t.me/+KMdLTc0qS6ZkMGI0
Nous ecrire par Whatsapp : Whatsapp +44 7476844931



Retour à La Une de Logo Paperblog

A propos de l’auteur


Mycamer Voir son profil
Voir son blog

l'auteur n'a pas encore renseigné son compte l'auteur n'a pas encore renseigné son compte

Magazines