javascript<!DOCTYPE html> <link rel="shortcut icon" href="#" /> <html lang="fr"> <body> <potions-product-list strategy="best-purchase_in_category" ></potions-product-list> <script src="main.js"></script> </body> </html>
javascriptimport { PotionsProductItem } from "./PotionsProductItem.js"; import { PotionsProductList } from "./PotionsProductList.js"; customElements.define("potions-product-list", PotionsProductList); customElements.define("potions-product-item", PotionsProductItem);
Two things of interest here : the fromStrategy that is returning an observable with the result. of an API call, and listToElements that can be seen as a “directive” (a piece of code that returns a template string from arguments).
javascriptimport { Component } from "./Component.js"; import { switchMap } from "rxjs"; import { fromStrategy, listToElements } from "./utils.js"; export class PotionsProductList extends Component { static get observedAttributes() { return ["strategy"]; } connectedCallback() { super.connectedCallback(); this.state$ = super.observedAttributes$.pipe( switchMap(observedAttributes => fromStrategy(observedAttributes["strategy"]) ) ); } templateStyle(state) { return ` <style> :host { display:flex; padding:20px; margin:10px; justify-content:center; flex-direction:column; } .list { display:flex; flex-direction:row; } </style>`; } template(list) { if (list) { return `<div> <div> ${list.title} </div> <div class="list"> ${listToElements( "potions-product-item", ["id", "price", "title", "img_link"], list.products )} </div> </div>`; } else { return ""; } } }
Here we set the state to be directly the attributes. We don’t need more for the moment.
javascriptimport { Component } from "./Component.js"; export class PotionsProductItem extends Component { static get observedAttributes() { return ["id", "title", "price", "img_link"]; } connectedCallback() { super.connectedCallback(); this.state$ = super.observedAttributes$; } templateStyle(state) { return ` <style> :host { display:flex; border: 1px solid black; padding:20px; margin:10px; justify-content:center; } </style>`; } template(product) { return product ? `<div> <div>${product.id}<div> <img src="${product.img_link}"></img> <div>${product.title}<div> <div>${product.price}<div> </div> ` : ""; } }
javascriptimport { BehaviorSubject, tap } from "rxjs"; export class Component extends HTMLElement { shadow = this.attachShadow({ mode: "open" }); // START - ADDED CODE isRootState = false; // END - ADDED CODE _state$ = new BehaviorSubject(null); _observedAttributes$ = new BehaviorSubject(null); connectedCallback() { this._observedAttributes$.next( this.constructor.observedAttributes ?.map(attr => ({ [attr]: this.getAttribute(attr) })) .reduce((prev, curr) => ({ ...prev, ...curr }), {}) ); this._renderSubscription = this._state$ .pipe(tap(s => this.render(s))) .subscribe(); } disconnectedCallback() { this._stateSubscription?.unsubscribe(); this._renderSubscription?.unsubscribe(); } attributeChangedCallback(name, oldValue, newValue) { const newObservedAttributes = { ...this._observedAttributes$.value, ...{ [name]: newValue } }; this._observedAttributes$.next(newObservedAttributes); } templateStyle(state) { return ""; } template(state) { return ""; } render(state) { this.shadow.innerHTML = ` ${this.templateStyle(state)} ${this.template(state)} `; } // START - ADDED CODE rootState$() { const parentOrHost = el => el.parentNode ?? el.host; let parent = parentOrHost(this); while (parent) { if (parent.isRootState) { return parent.state$; } parent = parentOrHost(parent); } } // END - ADDED CODE set state$(state$) { this._stateSubscription?.unsubscribe(); this._stateSubscription = state$.subscribe(this._state$); } get state$() { return this._state$; } get observedAttributes$() { return this._observedAttributes$; } }
javascriptimport { from, map } from "rxjs"; const strategies = { "best-purchase_in_category": { algorithm: 3, limit: 4, category_id: 130 }, "best-purchase": { algorithm: 1, limit: 4 } }; const fromAPI = (endpoint = "strategy", { algorithm, limit, category_id }) => from( fetch( `https://client.experiences.get-potions.com/v1/www.levapoteur-discount.fr/${endpoint}`, { method: "POST", headers: { Accept: "application/json", "Content-Type": "application/json" }, body: `[ { "algorithms": [${algorithm}], "limit": ${limit}, ${category_id ? `"category_id":${category_id},` : ""} "fields": "all" } ]` } ).then(response => response.json()) ); export const fromStrategy = strategy => { return fromAPI("strategy", strategies[strategy]).pipe( map(response => { const algorithm = response.strategies[0].algorithms[0]; return { id: strategy, title: algorithm.title, products: algorithm.products }; }) ); }; export const listToElements = (tag, properties, list) => { const itemToElement = (tag, properties) => item => { const props = properties .map(property => { return item[property] ? `${property}="${item[property]}"` : ""; }) .join(" "); return `<${tag} ${props}></${tag}>`; }; return list.map(itemToElement(tag, properties)).join("\r\n"); };