Sujet, Prédicat, Object : valeurs, audit et historisation

Cet article est une réflexion faisant suite à l’article concernant l’explication de mon approche ontologique et de la structure des données du projet Folkopedia avec un retour d’expériences.

Initialement nous avons notre ligne ontologique telle que :

S, P, O, V, Qui, Quand, Visibilité, Ordre

Avec pour lecture :

  • Sujet
  • Prédicat
  • Objet au sens pointeur vers un sujet
  • Valeur primitive de l’objet (c’est valeur ou objet mais pas les 2 en même temps)
  • Qui a créé ou éditer la ligne en dernier
  • Quand est-ce que ça été créé ou édité en dernier
  • Visibilité au sens de qui peut consulter cette valeur
  • Ordre au sens de l’ordonnancement des prédicats et valeurs

Les problèmes rencontrés

  1. D’abord un truc gênant concernant l’ordre, celui-ci n’est pas toujours nécessaire et c’est donc une colonne qui n’a pas toujours de sens.
  2. Dans le cas où un prédicat a plusieurs valeurs, celle-ci ne sont pas ordonnées, excepté par l’ordre d’insertion.
  3. La valeur elle-même peut avoir une historisation comme discuté ce qui demande un Sujet, du coup quid de la valeur primitive ?
  4. Comment être SPO sans avoir 2 colonnes pour respecter les théories SGBD ? Comment gérer les 2 en 1 et différencier au bon moment ?
  5. L’audit Qui et Quand n’est pas historisé, ce qui est un manque à l’objectif, pareil pour Visibilité et Ordre.

Solutions ?

Déjà reprenons le point n° 2. Elles peuvent être ordonnée si on utilise la colonne Ordre. Là où elle ne le sont pas, c’est dans le système actuel, où le fait d’avoir un objet ayant pour clef le prédicat et les valeurs simplifiées en tableau perdent cette notion. De plus la requête actuelle ne l’extrait pas.

Ceci mis à part, le constat est que les colonnes Qui, Quand, Visibilité et Ordre ne sont pas correcte, supprimons les et regardons comment respecter l’énoncée de départ.

S, P, O, V

Si on projette un exemple, nous aurons :

SujetPrédicatObjetValeur
#entity1SurnomnullKillan

Là on a une Valeur primitive pour un Prédicat d’un Sujet. Mais si on veut rajouter les 4 colonnes retirées, en suivant le modèle ontologique, nous allons devoir faire ainsi :

SujetPrédicatObjetValeur
#entity1Surnom#entity2null
#entity2AuditQui#entity1null
#entity2AuditQuandnull2025-02-13 21:08…
#entity2Visibilité#entity39null
#entity2Ordrenull1

Visibilité pourrait même être défini au niveau du Prédicat, par défaut, et supplanté par l’utilisateur potentiellement. La valeur disparait au profit d’une nouvelle entité contenant les informations relatives, mais la valeur a disparu, on la rajoute :

SujetPrédicatObjetValeur
#entity2ValeurnullKillan

Dès lors nous avons tout à nouveau, on est complet et il n’y a plus qu’à faire une requête pour chaque prédicat, simple… POUR CHAQUE PRÉDICAT ! Déjà que la console du système n’est pas triste avec le peu qu’on a fait jusque là, mais si en plus on double… Et en plus il faudra potentiellement trier les lignes reçues pour les identifier d’une certaine manière par les prédicats.

Réflexions sur la valeur

Si on observe le résultat, dans ce cas-ci, Objet est utilisé 50% du temps et Valeur 50%, équilibré en soit, mais plus largement ça sera surtout la colonne Objet qui sera attribuée. La valeur primitive est largement moins sollicitée.

De plus, si nous avons comme contenu l’historique complet d’un groupe folklorique, qui plus est formatté par exemple, nous allons avoir une quantité folle de données dans ce champs de manière anecdotique et là je me dis qu’on passe à côté de quelque chose.

On est d’accord qu’on n’enregistre pas un document ou une image dans ce champs, du coup, les contenus potentiellement massif, ou identifiés comme tels, ne devraient pas y être non plus. Cela n’a pas d’intérêt ontologique ou structurel.

Nous avions imaginé qu’une image serait parsemée de légendes, de tags, et de commentaires, ce qui à la fois contient des informations larges et d’autres courtes. Le tout pouvant faire l’objet d’une recherche. On peut partir dans tous les sens alors car il faut indexer du contenu pour chercher dedans. Pour ce faire, ce n’est pas à l’ontologie de s’en occuper, elle ne garanti que le lien entre les informations et la structure générale par ses définitions.

On peut imaginer un système annexe comme une table de contenus, ou un système comme un Elasticsearch. Le système de recherche ferait une demande à ces systèmes qui permettrait de relier un (ou plusieurs) Sujet(s) de l’ontologie. Ça semble cohérent.

Si on extrait les contenus large comme des documents et des images, il va falloir penser à une technique pour les relier dans l’ontologie. Comment écrire que la Valeur ou l’Objet fait référence à une structure externe ?

subject UUID not null,
predicate UUID not null,
object UUID,
o_value TEXT,

Petit extrait de notre table ontology pour vous montrer que les colonnes sont fortement typées. On ne peut pas indiquer n’importe quoi dans object. Et o_value, sur base de ce que l’on vient de dire, devrait devenir un varchar dont la longueur X est encore à définir, en soit si inférieur à X alors valeur courte et si supérieur à X alors contenu large externalisé.

Soit on garde la théorie SGBD avec notre clé secondaire (FK) pointant vers une clé primaire (PK), en dur dans le marbre bien respectueux et on garde une colonne Valeur en tant que varchar. Soit on transforme les 2 en une seule, de type JSON ou varchar. L’avantage du varchar est l’indexation pour définir un index et/ou une PK globale, permettant de valider l’unicité du Triplet. Avantages et inconvénients de chaque approche.

On peut également partir sur le principe de diviser la table en 2-3 pour séparer les colonne du fait de l’exclusivité mutuelle. Une table SPO et une table SPV, voire une table SP de référence centrale. C’est le code qui doit alors rassembler les morceaux. (Clin d’œil à Julian pour le rappel)

Là où ça se corse, quelque soit la solution, c’est l’identification extérieure. Comment interpréter Valeur ? On pourrait préfixer le contenu, tel que :

  • doc:refdoc#1
  • img:refimg#1
  • txt:reftxtlarge#1
  • v:Killan

Le symbole « deux points » ( : ) nous permettant, telle une sérialisation, de faire la part des choses, sans empêcher l’usage dudit caractère dans sa partie droite, à vérifier bien sur ou a encapsuler dans des guillemets (  »  » ). Attention tout de même de ne pas devenir une usine pour peu d’intérêts. Les images, textes et documents font partie du cœur de l’objectif, donc jusque là pas de soucis, on en prend soin.

On peut imaginer que la partie ref (refdoc#1, refimg#1, reftxtlarge#1) pointe sur une table autre que ontology avec comme valeur un chemin vers le fichier avec peut-être des propriétés système de ce fichier (nom, taille, format) dont l’intérêt ne sera pas ontologique et plutôt commun au système de référence.

Dans le cas d’une valeur primitive ( v: ou v: » … « ), là on interprète la valeur directement. J’en rajoute une couche avec la traduction ? Doit-on traduire la valeur en elle-même ou est-ce que le prédicat sera démultiplier avec un attribut supplémentaire pour contenir cette différenciation, ça demandera pas mal de requêtes, mais ça fonctionnerait.

SujetPrédicatObjetValeur
#entity3Titre#entity4null
#entity4AuditQui#entity1null
#entity4AuditQuandnullv: »2025-02-13 21:45:27 … »
#entity4Visibilité#entity39null
#entity4Ordrenullv: »1″
#entity4Valeurnullv: »Un titre un peu long qi pourrait poser un certain soucis quelque part »
#entity4Langnullv: »fr_be »
#entity3Titre#entity5null
#entity5
#entity5Valeurnullv: »A somewhat long title which could cause some problems somewhere »
#entity5Langnullv: »en_en »

Mais philosophiquement, n’est pas la même entité qui est traduite ? Serait-ce alors plusieurs clefs Valeur avec un code de langue du texte ?

SujetPrédicatObjetValeur
#entity4
#entity4Valeurnullv:fr_fr: »Un titre un peu long qi pourrait poser un certain soucis quelque part »
#entity4Valeurnullv:en_en: »A somewhat long title which could cause some problems somewhere »

Le principe ontologique est respecté, un prédicat et plusieurs valeurs, mais notre colonne valeur demande toute une interprétation. Ou alors, on pousse le bouchon encore plus loin et quelque soit la valeur devient un Sujet.

SujetPrédicatObjetValeur
#entity4
#entity4Valeur#entity7null
#entity7Type#entity46null
#entity7ContenunullUn titre un peu long qi pourrait poser un certain soucis quelque part
#entity7Langnullfr_fr
#entity4Valeur#entity8null
#entity8Type#entity46null
#entity8ContenunullA somewhat long title which could cause some problems somewhere
#entity8Langnullen_en

Et là c’est top côté ontologie, il nous faut :

  • 1 requête pour avoir la référence du Sujet
  • 1 requête pour avoir tous les prédicats du Sujet
  • 1 requête par Prédicat pour déterminer sa nature (peut-être à conditionner selon que la colonne Objet ou Valeur est utilisée)
  • 1 requête par Prédicat ayant une valeur sous forme de Sujet

À savoir un minimum de 6 requêtes, au lieu de 1 en début d’article (1 pour trouver le Sujet puis 1 pour avoir ses Prédicats, ou les 2 d’un coup si on a déjà la première info).

D’où l’importance des choix avant même de penser aux optimisations techniques.

Historisation

Nous ne sommes pas revenu sur l’historisation et là il y a 2 possibilités. Soit on part de la valeur que l’on encapsule virtuellement par répétition du prédicat, il faudra trouver la valeur la plus récente :

SujetPrédicatObjetValeur
#entity3Titre#entity4null
#entity4AuditQuandnull/date1/
#entity3Titre#entity5null
#entity5AuditQuandnull/date2/
Solution de plusieurs valeurs pour l’historisation

Soit par ligne individuelle, ce qui nous demandera de faire un travail différenciel pour recomposer le cheminement de l’information dans le temps, mais au final on en revient à la structure précédent Qui, Quand, Valeur. On pourrait imaginer un chainage, modifier la valeur entraine un nouveau bloc Valeur qui chaine en s’intercalant, comme en C avec les listes chaînée.

SujetPrédicatObjetValeur
#entity3Titre#entity5null
#entity5HistPrécédant#entity4null
#entity4
Chaînage de l’historique

La valeur

Revenons maintenant sur la valeur (Objet, Valeur). Ils sont mutuellement exclusif et comme dit plus tôt, il y a plusieurs solutions.

  • Soit diviser la table ontology en 2-3, ce qui, pour moi, ne correspond pas à l’idée de l’ontologie.
  • Soit fusionner la colonne Objet et Valeur en une seul de type varchar dont le contenu peut être déterminé comme vu précédemment.

Si on considère la seconde option, il faut parler du Prédicat qui nous donne déjà des informations de Type, donc en soit le type de valeur n’est à faire que dans le cadre d’un Prédicat Valeur. J’y vois déjà une arborescence de Types de Valeurs et de Prédicats relatifs. Le but est de toujours savoir comment interpréter la valeur.

Transposer les solutions

Si on essaye de tout regrouper pour voir si ça fonctionne virtuellement, prenons l’exemple de mon surnom.

SujetPrédicatObjet (valeur)Explication
#entity1Surnom#entity8
#entity8Type#entity3Entité de type Valeur
#entity8Qui#entity1
#entity8Quand/date1/Quand est de type date
#entity8Visibilité#entity4
#entity8Ordre1Ordre est de type nombre
#entity8ValeurContenu#entity7Entité de type contenu/valeur
#entity7Type#entity6
#entity7Langfr_be
#entity7ContenuKillan
#entity8HistPrec#entity2
#entity2Type#entity3Ancienne capsule pour la valeur
#entity2Qui#entity1
#entity2Quand/date2/
#entity2Visibilité#entity4
#entity2Ordre1
#entity2ValeurContenu#entity5
#entity5Type#entity6Ancienne valeur
#entity5Langfr_fr
#entity5ContenuKilan

Traduisons tout cela :

  • On a un Sujet me représentant #entity1 avec un Prédicat Surnom qui a une valeur #entity8,
  • #entity8 est pourvu des Prédicats permettant d’auditer la valeur et d’obtenir la valeur #entity7,
  • #entity7 est un Sujet ayant la valeur finale pour une langue initiale,
  • #entity8 à une valeur précédente en #entity2 pour corriger une typo (ça aurait pu être un changement de visibilité ou d’ordre),
  • #entity2 est pourvu des Prédicats permettant d’auditer la valeur et d’obtenir la valeur #entity5 également, et ici elle n’a pas de valeur précédente,

Attention aux noms des Types : Surnom, Valeur, Contenu et ContenuValeur; il va falloir y prêter attention pour ne pas se perdre en chemin. Peut-être est-ce toute une chaîne typée héritée de types respectifs plus macro comme les modèles.

En gros on a fait x10 à notre ligne initiale, et on corrige nos soucis d’historisation, de stockage de valeur et de typage. C’est le programme qui va se perdre en requêtes et recomposition d’un information intelligible pour traitement. Dans un sens comme dans l’autre. :/

2 requêtes pour la valeur d’un Prédicat qui a déjà demandé 1-2 requête(s). Et l’historique, ainsi que l’audit, à la demande dans des requêtes annexes par 2n.

Optimisation

Comme vous l’aurez compris ça va coûter cher en requêtes et temps de processus. On a une table déjà énorme en prévision, si en plus on a l’historisation aussi verticale ça va devenir longuet. Vient une idée (merci Adrien), de sortir la partie historique de la table ontology, de ne garder que l’instant courant dans celle-ci et de sortir le reste en dehors, vu que son taux de consultation sera bien bien moindre. On aurait donc une table historique contenant une structure légère afin de pointer le Sujet adéquat dans l’ontologie, voire même un Prédicat, à voir, et à stocker sa valeur précédente. En gros une capture de l’objet et descendants directement liés et stocker sous format JSON.

Dans l’exemple précédent on aurait alors #entity2 et #entity5 de sorti dans ce format JSON et stocké en historique, pointant #entity1:Surnom. Le reste serait une ordonnancement par date par exemple.

Validation de contenu

De part la volonté communautaire du projet et d’autogestion/autorégulation par la communauté, une valeur doit être approuvée, toute ou partiellement selon le type d’information. Cette nouvelle structure nous permet d’ajouter des Prédicats de contrôles.

SujetPrédicatObjet (valeur)
#entity17Validation#entity1

Le Prédicat pouvant être multiple, on peut avoir plusieurs validateur et la validation peut être à son tour un objet déterminant un commentaire et une valeur d’approbation et date. Cela peut être un refus commenté par exemple. C’est au système ensuite de considérer ou non la valeur. Dans le cas d’un correctif refusé, le système doit aller chercher la valeur initiale si approuvée. Je vous laisse imaginer le bazar futur.

Conclusion

Nous voilà revenu à notre structure la plus pure et théorique :

S, P, O

Dont O est un V encapsulé et dont il faudra définir l’entité avec soin.

subject UUID not null,
predicate UUID not null,
object VARCHAR(X) not null,

Adieu facilité ! Bonjour complexité et flexibilité…

Il ne me reste plus qu’à réécrire la moulinette de conversion, créer les nouvelles structures d’entités et créer les outils nécessaires pour gérer ça plus facilement, et évidemment revoir le lien front OntologyMyAdmin en profondeur du coup.

Rien que ça… ^^

Notation en point

Petit article vite fait en complément des rendus de cellules :

Pour rappel mes rendus de cellules fonctionnent avec le prototype suivant :

static fn(rowData: any, field: string): string { }

rowData contient l’objet de la ligne du tableau, à savoir un objet potentiellement complexe, et field contient le champs à rendre.

Mais

Mais si on a un objet complexe et que l’on souhaite rendre une sous-valeur, par exemple rue de notre adresse ?

{
  adresse: {
    rue: string
    numero: string
    ...
  }
  ...
}

Si je fais rowData[‘rue’] on est d’accord que ça nous donnera une erreur d’index non-trouvé ?

Notation en point

Une notation en point c’est, dans notre exemple : 'adresse.rue', c’est à dire la description du chemin pour avoir notre contenu à partir de la racine de l’objet.

Mais si on fait rowData[‘adresse.rue’], ça restera une erreur d’index non-trouvé ! Pourquoi ? Parce que ! Il ne va pas décortiquer tout seul votre clef, ça serait trop facile…

À noter que PrimeFace le traite par défaut quand vous utilisez leur système, mais dès qu’on part en « fait maison », mode dynamique, extension des templates, etc. ben zou ça ne fonctionne plus tout seul et il n’y a pas de service appelable pour vous aider.

Donc on va le faire nous même.

Résoudre la notation en point

Visuellement c’est simple, je veux descendre d’un cran à chaque point, sur base de la clef courante. Donc une récursive à plat en quelque sorte. Sans chipoter voici ma solution :

static solveDotNotation<T>(rowData: any, field: string) : T | undefined {
  const keys = field.split('.')

  let err = false
  let obj = rowData
  for (let ki in keys) {
    const k = keys[ki]

    if (obj[k]) {
      // Continue to dig next key
      obj = obj[k]
    } else {
      err = true
      break
    }
  }

  return err ? undefined : obj as T
}

On divise field sur le point pour avoir une liste de clefs (keys) puis on parcours la liste. On a une variable obj, tel un pointeur mobile, initialement positionné à la racine de rowData, qui va évoluer à chaque passage de la boucle pour devenir le sous-élément. Si on ne trouve pas une étape on sort le plus vite possible, sinon on continue de descendre dans le terrier du lapin blanc.

En cas d’erreur, ici le choix est fait de renvoyer undefined qui permet, dans mon cas, de pouvoir jouer avec la notation de chainage optionnel (?.) dont voici mon exemple dans le cadre de mon rendu de valeurs ontologiques.

static OntologyValues(rowData: any, field: string): string {
  return CellRenderer.solveDotNotation<OntologyValue[]>(rowData, field)?.map<string>(ov=> ov.value || ov.object).join("\n<br/>") || ""
}

Un système de tabs en Angular+PrimeNg

J’ai été amené à réaliser en Vue3 un système de tabs avec une zone d’actions (ZA) qui s’affiche à droite des onglets. Ceci n’étant pas proposé par le composant de base de PrimeVue. À nos claviers !

Schéma de principe

Étant sur mon projet OntologyMyAdmin en Angular et donc avec PrimeNg, c’est à l’inverse de certains articles « Angular vers Vue » que je vais réaliser un système de tabs en Angular sur base de l’expérience en Vue que j’ai mené il y a une bonne année.

En Vue, dans les grandes lignes, sur base d’une props contenant la liste des ids des tabs avec leur options (visible, désactivé, classe CSS, …), on boucle dessus pour générer les onglets et pour générer les slots nommé sur base des ids, du coup la magie opère après l’init et les templates se placent bien dans leur slot avec gestion par le composant tabs. La zone action fonctionne de la même manière avec les templates suffixés « -actions » par exemple. Le code est minimaliste et la logique se trouve dans la données préparée et la boucle de traitement.

En Angular ce n’est pas tout à fait la même musique, on ne peut pas avoir de ng-content au nom dynamique. Pour ne pas vous faire languir, il faut passer par l’exploration des enfants de notre composant (@ContentChildren) par ce qu’on appelle la projection de contenu.

Là où ça se corse c’est : comment ? Il vous faudra une directive ou un composant pour mapper l’annotation afin qu’elle trouve quelque chose, et ce, pas avant votre évènement ngAfterContentInit.

@ContentChildren(TabComponent) tabs!: QueryList<TabComponent>
@ContentChildren(TabActionComponent) tabsAction!: QueryList<TabActionComponent>

Grâce à cette façon de faire, au lieu d’avoir une liste de tabs en input comme en Vue, on pilotera nos onglets par leurs attributs directement. Dans l’exemple ci-dessous vous verrez qu’on lui indique son titre et un id personnalisé pour lier son action (les autres récupérant un id automatique).

<oma-tabs>
  <oma-tab title="struct">
    <h2>Tab sans actions</h2>
  </oma-tab>

  <oma-tab title="items" tabId="items">
    <h2>Tab avec action</h2>
  </oma-tab>
  <oma-tab-action tabId="items">
    <p-button label="actionA" />
  </oma-tab-action>

  <oma-tab title="options" tabId="options">
    <h2>Autre tab avec action</h2>
  </oma-tab>
  <oma-tab-action tabId="options">
    <p-button label="actionB" />
  </oma-tab-action>
</oma-tabs>
Exemple de résultat du système de tabs

En positionnant un ng-content avec l’attribut select du type de votre composant, vous garantissez que seuls ceux-là défini se retrouveront dans cet espace, comme un filtre.

<div class="tabs-actions">
  <div class="flex justify-between">
    <ul class="list-none">
      @for (t of tabs; track t.id; let idx = $index) {
        <li class="inline-block" [ngClass]="{ selected: selectedTabId === t.id }">
          <p-button [label]="t.title" (onClick)="changeTab(t)" />
        </li>
      }
    </ul>
    <div class="text-right self-center">
      <ng-content select="oma-tab-action" />
    </div>
  </div>
  <div>
    <ng-content select="oma-tab" />
  </div>
</div>

Il nous reste à initialiser les valeurs par défaut, qui apparait, etc.

@Input()
selected: number = 0

protected selectedTabId: number | string = 0

ngAfterContentInit() {
  let indexing = 0;
  const fnIndexing = (t: TabCommonComponent) => {
    if (!t.id) {
      t.id = indexing++
    }
  }
  this.tabs.forEach(fnIndexing)
  this.tabsAction.forEach(fnIndexing)

  this.selectedTabId = this.selected || this.tabs.first.id || 0
  const tabActive = this.tabs.find(t => this.selectedTabId === t.id)
  if (tabActive) {
    tabActive.active = true
  }
  const tabActionActive = this.tabsAction.find(t => this.selectedTabId === t.id)
  if (tabActionActive) {
    tabActionActive.active = true
  }
}

Le changement de tab fait suite à l’initialisation, c’est la même chose, on clique, on reset l’état et on active l’onglet voulu et sa zone d’action si disponible.

changeTab(tab: TabComponent) {
  if (this.selectedTabId != tab.id) {
    this.selectedTabId = tab.id

    // Change visibility
    const fnChange = (t: TabCommonComponent) => {
      t.active = t.id === this.selectedTabId
    }
    this.tabs.forEach(fnChange)
    this.tabsAction.forEach(fnChange)

    // TODO on change
  }
}

Notez le TODO qui précise qu’il faudra remonter un événement pour l’host dans le cas où ceci peut avoir un intérêt.

C’est tout, il n’y a rien de plus, c’est aussi simple 🙂 Par contre je peux vous parler d’héritage de composant afin de ne pas se répéter inutilement (DRY).

Héritage de composant

En effet, nous avons 2 composants enfant de notre tabs : le tab lui-même et le tab-action (ZA). Ils ont tout deux besoin d’être activé et d’avoir un id. Seul le tab aura un titre par exemple qui permet de générer son onglet.

<div [ngClass]="{ hidden: !active }">
  <ng-content />
</div>
@Component({
  selector: 'oma-tab-common',
  imports: [
    NgClass
  ],
  templateUrl: "tab-common.component.html"
})
export class TabCommonComponent {
  @Input('tabId') id: number | string = 0
  @Input() active: boolean = false
}

Notez que l’attribut « id » est déjà prévu par HTML, il nous faut donc le nommer autrement, mais en interne vous faites ce que vous voulez.

Maintenant que vous avez votre composant commun, héritons-le !

@Component({
  selector: 'oma-tab',
  imports: [
    NgClass
  ],
  templateUrl: "tab-common.component.html"
})
export class TabComponent extends TabCommonComponent {
  @Input() title: string = ""
}
@Component({
  selector: 'oma-tab-action',
  imports: [
    NgClass
  ],
  templateUrl: "tab-common.component.html"
})
export class TabActionComponent extends TabCommonComponent {

}

Et c’est tout ! À nouveau ^^.

Lazy loading

En complément on peut parler de lazy loading, c’est à dire le fait de ne charger le contenu du tab que si on l’affiche. Ceci permettant, par exemple, de ne pas faire de grosses requêtes dans mon second tab inutilement. Ceci en complément d’un cache par exemple si on compte répéter les aller-retours entre les tabs. On peut aussi considérer la mise à jour d’un tab quand on y revient, c’est à dire qu’au lieu de le masquer/afficher (css hidden) on peut utiliser une directive conditionnelle (@if) afin de relancer le tab (son contenu) à son prochain affichage.

@if ((lazy && active) || !lazy) {
  <div [ngClass]="{ hidden: !active }">
    <ng-content />
  </div>
}

Si on active le côté lazy, le @if jouera le rôle de constucteur/destructeur du composant qui repartira à zéro à chaque chargement de l’onglet.

Mais si on veut que ça n’arrive que la première fois, il faudra ajouter une notion de en cours de chargement/chargé. On peut combiner différentes idées. Et, effectivement, ici, c’est intéressant pour ne pas relancer l’ontologie à chaque fois, mais de la charger quand même au besoin. Mais comment le tab sait qu’il est en cours de chargement ou chargé. On peut juste dire que lors de la première activation, on le note pour ne pas rejouer le @if. Mais on pourrait aller plus loin avec notion de chargement, si l’enfant pouvait nous en informer.

Soit via une services partagé avec Id du système d’onglets, soit via le contenu poussé dans l’onglet piloté en extérieur, les 2 sont valables, tout dépend du besoin.

Signal

Vous l’avez remarqué, le code est à l’ancienne, pas de Signal dedans, juste @Input(), et pour une raison toute bête, Signal ne permet pas de faire ainsi. L’usage de input() génère un InputSignal qui ne peut être modifié en tant que propriété. Du coup une alternative est de transformer input() en model(), un ModelSignal(), qui lui le peut. Au moment d’écrire cet article, j’ai eu quelques déboires avec signal et ses contraintes, ce qui a été résolu et donc je vous met en gist la version signal :), et ici quelques exemple de transformations.

  selected = input(0)
  selectedTabId = signal<number | string>(0)

  ngAfterContentInit() {
    let indexing = 0;
    const fnIndexing = (t: TabCommonComponent) => {
      if (!t.id()) {
        t.id.set(indexing++)
      }
    }
    this.tabs.forEach(fnIndexing)
    this.tabsAction.forEach(fnIndexing)

    this.selectedTabId.set(this.selected() || this.tabs.first.id() || 0)
    const tabActive = this.tabs.find(t => this.selectedTabId() === t.id())
    if (tabActive) {
      tabActive.active.set(true)
    }
    const tabActionActive = this.tabsAction.find(t => this.selectedTabId() === t.id())
    if (tabActionActive) {
      tabActionActive.active.set(true)
    }
  }

  changeTab(tab: TabComponent) {
    if (this.selectedTabId() != tab.id()) {
      this.selectedTabId.set(tab.id())

      // Change visibility
      const fnChange = (t: TabCommonComponent) => {
        t.active.set(t.id() === this.selectedTabId())
      }
      this.tabs.forEach(fnChange)
      this.tabsAction.forEach(fnChange)

      // TODO on change
    }
  }
export class TabCommonComponent {
  id = model<number | string>(0, {
    alias: 'tabId'
  })

  active = model(false)
  lazy = model(false)

  constructor() {
    effect(() => {
      const active = this.active()

      active ? this.onActive.emit(active) : this.onInactive.emit(active)

      if (active) {
        this.lazy.set(false)
      }
    });
  }

  onActive = output<boolean>()
  onInactive = output<boolean>()
}