# UrlStateService - Guide d'Intégration Complet ## 📋 Table des matières 1. [Vue d'ensemble](#vue-densemble) 2. [Installation](#installation) 3. [Intégration dans les composants](#intégration-dans-les-composants) 4. [Exemples d'URL](#exemples-durl) 5. [Gestion des erreurs](#gestion-des-erreurs) 6. [Cas d'usage avancés](#cas-dusage-avancés) 7. [API Complète](#api-complète) --- ## Vue d'ensemble Le `UrlStateService` synchronise l'état de l'interface avec l'URL, permettant: - ✅ **Deep-linking**: Ouvrir une note directement via URL - ✅ **Partage de liens**: Générer des URLs partageables - ✅ **Restauration d'état**: Retrouver l'état après rechargement - ✅ **Filtrage persistant**: Tags, dossiers, quick links via URL - ✅ **Recherche persistante**: Termes de recherche dans l'URL ### Architecture ``` URL (query params) ↓ UrlStateService (parsing + validation) ↓ Angular Signals (currentState, activeTag, etc.) ↓ Composants (NotesListComponent, NoteViewComponent, etc.) ``` --- ## Installation ### 1. Service déjà créé Le service est disponible à: ``` src/app/services/url-state.service.ts ``` ### 2. Injection dans AppComponent ```typescript import { Component, inject } from '@angular/core'; import { UrlStateService } from './services/url-state.service'; @Component({ selector: 'app-root', standalone: true, template: `...` }) export class AppComponent { private urlStateService = inject(UrlStateService); // Le service est automatiquement initialisé et écoute les changements d'URL } ``` --- ## Intégration dans les composants ### NotesListComponent - Synchroniser les filtres avec l'URL ```typescript import { Component, inject, effect } from '@angular/core'; import { UrlStateService } from '../../services/url-state.service'; @Component({ selector: 'app-notes-list', standalone: true, template: `...` }) export class NotesListComponent { private urlState = inject(UrlStateService); // Signaux dérivés de l'URL activeTag = this.urlState.activeTag; activeFolder = this.urlState.activeFolder; activeQuickLink = this.urlState.activeQuickLink; activeSearch = this.urlState.activeSearch; constructor() { // Écouter les changements d'état effect(() => { const state = this.urlState.currentState(); // Réagir aux changements if (state.tag) { console.log('Tag filter applied:', state.tag); this.applyTagFilter(state.tag); } if (state.folder) { console.log('Folder filter applied:', state.folder); this.applyFolderFilter(state.folder); } if (state.search) { console.log('Search applied:', state.search); this.applySearch(state.search); } }); } // Mettre à jour l'URL quand l'utilisateur change de vue onTagClick(tag: string): void { this.urlState.filterByTag(tag); } onFolderClick(folder: string): void { this.urlState.filterByFolder(folder); } onQuickLinkClick(quickLink: string): void { this.urlState.filterByQuickLink(quickLink); } onSearch(searchTerm: string): void { this.urlState.updateSearch(searchTerm); } } ``` ### NoteViewComponent - Ouvrir une note via URL ```typescript import { Component, inject, effect } from '@angular/core'; import { UrlStateService } from '../../services/url-state.service'; import { VaultService } from '../../../services/vault.service'; @Component({ selector: 'app-note-view', standalone: true, template: `

{{ note.title }}

` }) export class NoteViewComponent { private urlState = inject(UrlStateService); private vault = inject(VaultService); // Signal de la note actuelle depuis l'URL currentNote = this.urlState.currentNote; constructor() { // Charger la note quand l'URL change effect(async () => { const note = this.currentNote(); if (note) { // Charger le contenu complet si nécessaire await this.vault.ensureNoteLoadedByPath(note.filePath); } }); } // Ouvrir une note en mettant à jour l'URL openNote(notePath: string): void { this.urlState.openNote(notePath); } } ``` ### FoldersSidebarComponent - Synchroniser la sélection avec l'URL ```typescript import { Component, inject } from '@angular/core'; import { UrlStateService } from '../../services/url-state.service'; @Component({ selector: 'app-folders-sidebar', standalone: true, template: `
` }) export class FoldersSidebarComponent { urlState = inject(UrlStateService); selectFolder(folderPath: string): void { this.urlState.filterByFolder(folderPath); } } ``` ### TagsComponent - Synchroniser les tags avec l'URL ```typescript import { Component, inject } from '@angular/core'; import { UrlStateService } from '../../services/url-state.service'; @Component({ selector: 'app-tags-view', standalone: true, template: `
` }) export class TagsComponent { urlState = inject(UrlStateService); selectTag(tagName: string): void { this.urlState.filterByTag(tagName); } } ``` --- ## Exemples d'URL ### 1. Ouvrir une note spécifique ``` https://app.example.com/viewer?note=Docs/Architecture.md ``` **Résultat**: Ouvre la note `Docs/Architecture.md` dans la vue note ### 2. Filtrer par tag ``` https://app.example.com/viewer?tag=Ideas ``` **Résultat**: Affiche toutes les notes avec le tag `Ideas` ### 3. Filtrer par dossier ``` https://app.example.com/viewer?folder=Notes/Meetings ``` **Résultat**: Affiche toutes les notes du dossier `Notes/Meetings` ### 4. Afficher un quick link ``` https://app.example.com/viewer?quick=Favoris ``` **Résultat**: Affiche les notes marquées comme favoris ### 5. Rechercher ``` https://app.example.com/viewer?search=performance ``` **Résultat**: Affiche les résultats de recherche pour "performance" ### 6. Combinaisons ``` https://app.example.com/viewer?note=Docs/Architecture.md&search=performance ``` **Résultat**: Ouvre la note et met en surbrillance les occurrences de "performance" ``` https://app.example.com/viewer?folder=Notes/Meetings&tag=Important ``` **Résultat**: Affiche les notes du dossier `Notes/Meetings` avec le tag `Important` --- ## Gestion des erreurs ### Note introuvable ```typescript async openNote(notePath: string): Promise { try { await this.urlState.openNote(notePath); } catch (error) { console.error('Note not found:', notePath); // Afficher un message d'erreur à l'utilisateur this.toast.error(`Note introuvable: ${notePath}`); // Réinitialiser l'état this.urlState.resetState(); } } ``` ### Tag inexistant ```typescript async filterByTag(tag: string): Promise { try { await this.urlState.filterByTag(tag); } catch (error) { console.error('Tag not found:', tag); this.toast.warning(`Tag inexistant: ${tag}`); } } ``` ### Dossier inexistant ```typescript async filterByFolder(folder: string): Promise { try { await this.urlState.filterByFolder(folder); } catch (error) { console.error('Folder not found:', folder); this.toast.warning(`Dossier inexistant: ${folder}`); } } ``` --- ## Cas d'usage avancés ### 1. Générer un lien de partage ```typescript // Copier l'URL actuelle async shareCurrentState(): Promise { try { await this.urlState.copyCurrentUrlToClipboard(); this.toast.success('Lien copié!'); } catch (error) { this.toast.error('Erreur lors de la copie'); } } // Générer une URL personnalisée generateShareUrl(note: Note): string { return this.urlState.generateShareUrl({ note: note.filePath }); } ``` ### 2. Écouter les changements d'état ```typescript constructor() { // Écouter tous les changements this.urlState.stateChange$.subscribe(event => { console.log('État précédent:', event.previous); console.log('Nouvel état:', event.current); console.log('Propriétés changées:', event.changed); }); // Écouter un changement spécifique this.urlState.onStatePropertyChange('note').subscribe(event => { console.log('Note changée:', event.current.note); }); this.urlState.onStatePropertyChange('tag').subscribe(event => { console.log('Tag changé:', event.current.tag); }); } ``` ### 3. Restaurer l'état après rechargement ```typescript constructor() { // L'état est automatiquement restauré depuis l'URL effect(() => { const state = this.urlState.currentState(); // Restaurer la vue if (state.note) { this.openNote(state.note); } else if (state.tag) { this.filterByTag(state.tag); } else if (state.folder) { this.filterByFolder(state.folder); } else if (state.quick) { this.filterByQuickLink(state.quick); } }); } ``` ### 4. Historique de navigation ```typescript // Obtenir l'état précédent const previousState = this.urlState.getPreviousState(); // Revenir à l'état précédent if (previousState.note) { this.urlState.openNote(previousState.note); } else if (previousState.tag) { this.urlState.filterByTag(previousState.tag); } ``` ### 5. Réinitialiser l'état ```typescript // Retour à la vue par défaut resetToDefault(): void { this.urlState.resetState(); } ``` --- ## API Complète ### Signaux (Computed) ```typescript // État actuel currentState: Signal // État précédent previousState: Signal // Note actuellement ouverte currentNote: Signal // Tag actif activeTag: Signal // Dossier actif activeFolder: Signal // Quick link actif activeQuickLink: Signal // Terme de recherche actif activeSearch: Signal ``` ### Méthodes #### Navigation ```typescript // Ouvrir une note async openNote(notePath: string): Promise // Filtrer par tag async filterByTag(tag: string): Promise // Filtrer par dossier async filterByFolder(folder: string): Promise // Filtrer par quick link async filterByQuickLink(quickLink: string): Promise // Mettre à jour la recherche async updateSearch(searchTerm: string): Promise // Réinitialiser l'état async resetState(): Promise ``` #### Vérification ```typescript // Vérifier si une note est ouverte isNoteOpen(notePath: string): boolean // Vérifier si un tag est actif isTagActive(tag: string): boolean // Vérifier si un dossier est actif isFolderActive(folder: string): boolean // Vérifier si un quick link est actif isQuickLinkActive(quickLink: string): boolean ``` #### Partage ```typescript // Générer une URL partageble generateShareUrl(state?: Partial): string // Copier l'URL actuelle async copyCurrentUrlToClipboard(): Promise ``` #### État ```typescript // Obtenir l'état actuel getState(): UrlState // Obtenir l'état précédent getPreviousState(): UrlState ``` ### Observables ```typescript // Observable des changements d'état stateChange$: Observable // Observable des changements d'une propriété onStatePropertyChange(property: keyof UrlState): Observable ``` ### Types ```typescript interface UrlState { note?: string; // Chemin de la note tag?: string; // Tag de filtrage folder?: string; // Dossier de filtrage quick?: string; // Quick link de filtrage search?: string; // Terme de recherche } interface UrlStateChangeEvent { previous: UrlState; current: UrlState; changed: (keyof UrlState)[]; } ``` --- ## Checklist d'intégration - [ ] Service `UrlStateService` créé - [ ] Service injecté dans `AppComponent` - [ ] `NotesListComponent` synchronise les filtres avec l'URL - [ ] `NoteViewComponent` ouvre les notes via URL - [ ] `FoldersSidebarComponent` synchronise la sélection avec l'URL - [ ] `TagsComponent` synchronise les tags avec l'URL - [ ] Gestion des erreurs implémentée - [ ] Tests unitaires écrits - [ ] Documentation mise à jour - [ ] Déploiement en production --- ## Troubleshooting ### L'URL ne change pas quand je change de vue **Solution**: Vérifiez que vous appelez les méthodes du service: ```typescript // ❌ Mauvais this.currentTag = 'Ideas'; // ✅ Correct await this.urlState.filterByTag('Ideas'); ``` ### La note n'est pas trouvée **Solution**: Vérifiez le chemin exact: ```typescript // Afficher tous les chemins disponibles console.log(this.vault.allNotes().map(n => n.filePath)); // Utiliser le bon chemin await this.urlState.openNote('Docs/Architecture.md'); ``` ### L'état n'est pas restauré après rechargement **Solution**: Assurez-vous que le service est injecté dans `AppComponent`: ```typescript export class AppComponent { private urlStateService = inject(UrlStateService); // Le service s'initialise automatiquement } ``` --- ## Performance - ✅ Utilise Angular Signals pour les mises à jour réactives - ✅ Pas de polling, écoute les changements d'URL natifs - ✅ Décodage/encodage URI optimisé - ✅ Gestion automatique du cycle de vie --- ## Sécurité - ✅ Validation des chemins de notes - ✅ Validation des tags existants - ✅ Validation des dossiers existants - ✅ Encodage URI pour les caractères spéciaux - ✅ Pas d'exécution de code depuis l'URL --- ## Prochaines étapes 1. **Tests unitaires**: Créer des tests pour chaque méthode 2. **Tests E2E**: Tester les flux complets avec Playwright 3. **Monitoring**: Tracker les URLs les plus utilisées 4. **Analytics**: Analyser les patterns de navigation 5. **Optimisation**: Compresser les URLs longues si nécessaire