import { LitElement, html } from "lit"; import { customElement, property } from "lit/decorators.js"; import Subscriber from "@supersoniks/concorde/core/mixins/Subscriber"; import { templateContent } from "lit/directives/template-content.js"; import { repeat } from "lit/directives/repeat.js"; import Objects from "@supersoniks/concorde/core/utils/Objects"; import UrlPattern from "@supersoniks/concorde/core/utils/url-pattern"; import TemplatesContainer from "@supersoniks/concorde/core/mixins/TemplatesContainer"; import { PublisherInterface, TypeAndRecordOfType, } from "@supersoniks/concorde/core/_types/types"; import { DirectiveResult } from "lit/directive.js"; type UrlPatternExtended = { names: string[] } & UrlPattern; const tagName = "sonic-states"; /** * ### sonic-states affiche des états différents en fonction de la valeur d'une sous propriété de son dataProvider (attribut data-path en dot notation): * * Il boucle sur ses template enfants et test si la regexp contenue dans l'attribut *data-value* match la valeur de la propriété * * Si oui le contenu du template correspontant est affiché comme état. * * Si l'attribut dataProviderExpression est fourni le contenu est entouré d'une div : * * L'attribut "dataProvider" de cette div est le resultat de l'appel à la fonction replace sur valeur de la propriété avec comme paramettres la regexp et dataproviderExpression. * * Les subscribers/fetch... du template se réfèrerons à ce dataProvider * * On peut également utiliser les des expressions du type url-pattern pour les paramètres de la route voir les exemples * * **Exemples** * Avec ma.propriete= 2 : * * RegExp data-value = (\d+) et dataproviderExpression = /user/$1 l'attribut dataProvider vaudra "/user/2" * * url-pattern data-value = :id et dataproviderExpression = /user/:id l'attribut dataProvider vaudra "/user/2" * */ type ExtractParamNames = T extends `${string}:${infer Param}/${infer Rest}` ? Param | ExtractParamNames : T extends `${string}:${infer Param}` ? Param : never; type ParamsObject = { [K in ExtractParamNames]: string; }; type StateFunction = ( params: ParamsObject ) => DirectiveResult; type States = { fallback?: () => DirectiveResult; [path: string]: StateFunction | undefined; }; @customElement(tagName) export class SonicStates extends Subscriber(TemplatesContainer(LitElement)) { @property() state = ""; @property({ type: Boolean, reflect: true }) inverted = false; @property({ type: Object }) states?: States; statePath = ""; onStateAssign = (value: string) => { this.state = value; this.requestUpdate(); }; statePublisher?: TypeAndRecordOfType>; connectedCallback(): void { this.noShadowDom = ""; super.connectedCallback(); if (this.hasAttribute("data-path")) { this.statePath = this.getAttribute("data-path"); } if (this.statePath) { this.statePublisher = this.publisher; const split = this.statePath.split("."); for (const s of split) { this.statePublisher = this.statePublisher[s]; } this.statePublisher.onAssign(this.onStateAssign); } } disconnectedCallback(): void { if (this.statePath) { this.statePublisher?.offAssign(this.onStateAssign); } super.disconnectedCallback(); } handleStates(state: string) { if (!this.states) return []; const templates: DirectiveResult[] = []; for (const [stateExpression, renderFn] of Object.entries(this.states)) { if (stateExpression === "fallback" || !renderFn) continue; const regexp = new RegExp(stateExpression); if (regexp.test(state)) { const params = regexp.exec(state) || [stateExpression]; params.shift(); templates.push(renderFn([...params])); } else { try { const pattern = new UrlPattern("(/)*" + stateExpression + "*"); if (pattern.match(state)) { const params = pattern.match(state) || {}; templates.push(renderFn(params)); } } catch (e) { if ( state.indexOf( stateExpression.replace(document.location.origin, "") ) != -1 ) { templates.push(renderFn({})); } } } } //Gestion fallback if (templates.length == 0) { if (this.states?.fallback && this.isConnected) { templates.push(this.states.fallback()); } } return templates; } render() { let state = this.state; if ( (!Array.isArray(state) && Objects.isObject(state)) || state === undefined ) { state = ""; } // Gestion des états programmatiques const programmaticTemplates = this.handleStates(state); if (programmaticTemplates.length > 0) { return html`${programmaticTemplates}`; } const templates = []; // Gestion des templates HTML existants for (const t of this.templatePartsList) { let path = t.getAttribute(this.templateValueAttribute) as string; let stateToMatch = state; if (this.inverted) { stateToMatch = path; path = state; } if (path == "") path = this.inverted ? ".*?" : "^$"; const regexp = new RegExp(path); if (regexp.test(stateToMatch + "")) { templates.push(t); t.removeAttribute("mode"); } else { const urlPattern = new UrlPattern(path) as UrlPatternExtended; if (urlPattern.names.length > 0 && urlPattern.match(stateToMatch)) { t.setAttribute("mode", "patternMatching"); templates.push(t); } } } return html`${repeat( templates, (template, index) => { template; return index + new Date().getTime(); }, (template) => { if (template?.hasAttribute("dataProviderExpression")) { const dataProviderExpression = template.getAttribute( "dataProviderExpression" ) as string; let dataProvider = ""; let stateToMatch = state; let path: string = template.getAttribute( this.templateValueAttribute ) as string; if (this.inverted) { stateToMatch = path; path = state; } if (path == "") path = this.inverted ? "*" : "^$"; if (template.getAttribute("mode") == "patternMatching") { const matcher = new UrlPattern(path); const filler = new UrlPattern(dataProviderExpression); dataProvider = filler.stringify(matcher.match(stateToMatch)); } else { const regexp = new RegExp(path); const match = (stateToMatch + "").match(regexp); if (match) { dataProvider = match .shift() ?.replace(regexp, dataProviderExpression) as string; } } return html`
${templateContent(template)}
`; } return templateContent(template); } )}`; } }