- Added UrlStateService to sync app state with URL parameters for note selection, tags, folders, and search - Implemented URL state effects in AppComponent to handle navigation from URL parameters - Updated sidebar and layout components to reflect URL state changes in UI - Added URL state updates when navigating via note selection, tag clicks, and search - Modified note sharing to use URL parameters instead of route paths - Added auto-opening of relevant
14 KiB
14 KiB
UrlStateService - Guide d'Intégration Complet
📋 Table des matières
- Vue d'ensemble
- Installation
- Intégration dans les composants
- Exemples d'URL
- Gestion des erreurs
- Cas d'usage avancés
- 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
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
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
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: `
<div *ngIf="currentNote() as note" class="note-view">
<h1>{{ note.title }}</h1>
<div [innerHTML]="note.content"></div>
</div>
`
})
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
import { Component, inject } from '@angular/core';
import { UrlStateService } from '../../services/url-state.service';
@Component({
selector: 'app-folders-sidebar',
standalone: true,
template: `
<div class="folders-list">
<button *ngFor="let folder of folders"
[class.active]="urlState.isFolderActive(folder.path)"
(click)="selectFolder(folder.path)">
{{ folder.name }}
</button>
</div>
`
})
export class FoldersSidebarComponent {
urlState = inject(UrlStateService);
selectFolder(folderPath: string): void {
this.urlState.filterByFolder(folderPath);
}
}
TagsComponent - Synchroniser les tags avec l'URL
import { Component, inject } from '@angular/core';
import { UrlStateService } from '../../services/url-state.service';
@Component({
selector: 'app-tags-view',
standalone: true,
template: `
<div class="tags-list">
<button *ngFor="let tag of tags"
[class.active]="urlState.isTagActive(tag.name)"
(click)="selectTag(tag.name)">
#{{ tag.name }}
</button>
</div>
`
})
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
async openNote(notePath: string): Promise<void> {
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
async filterByTag(tag: string): Promise<void> {
try {
await this.urlState.filterByTag(tag);
} catch (error) {
console.error('Tag not found:', tag);
this.toast.warning(`Tag inexistant: ${tag}`);
}
}
Dossier inexistant
async filterByFolder(folder: string): Promise<void> {
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
// Copier l'URL actuelle
async shareCurrentState(): Promise<void> {
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
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
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
// 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
// Retour à la vue par défaut
resetToDefault(): void {
this.urlState.resetState();
}
API Complète
Signaux (Computed)
// État actuel
currentState: Signal<UrlState>
// État précédent
previousState: Signal<UrlState>
// Note actuellement ouverte
currentNote: Signal<Note | null>
// Tag actif
activeTag: Signal<string | null>
// Dossier actif
activeFolder: Signal<string | null>
// Quick link actif
activeQuickLink: Signal<string | null>
// Terme de recherche actif
activeSearch: Signal<string | null>
Méthodes
Navigation
// Ouvrir une note
async openNote(notePath: string): Promise<void>
// Filtrer par tag
async filterByTag(tag: string): Promise<void>
// Filtrer par dossier
async filterByFolder(folder: string): Promise<void>
// Filtrer par quick link
async filterByQuickLink(quickLink: string): Promise<void>
// Mettre à jour la recherche
async updateSearch(searchTerm: string): Promise<void>
// Réinitialiser l'état
async resetState(): Promise<void>
Vérification
// 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
// Générer une URL partageble
generateShareUrl(state?: Partial<UrlState>): string
// Copier l'URL actuelle
async copyCurrentUrlToClipboard(): Promise<void>
État
// Obtenir l'état actuel
getState(): UrlState
// Obtenir l'état précédent
getPreviousState(): UrlState
Observables
// Observable des changements d'état
stateChange$: Observable<UrlStateChangeEvent>
// Observable des changements d'une propriété
onStatePropertyChange(property: keyof UrlState): Observable<UrlStateChangeEvent>
Types
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
UrlStateServicecréé - Service injecté dans
AppComponent NotesListComponentsynchronise les filtres avec l'URLNoteViewComponentouvre les notes via URLFoldersSidebarComponentsynchronise la sélection avec l'URLTagsComponentsynchronise 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:
// ❌ Mauvais
this.currentTag = 'Ideas';
// ✅ Correct
await this.urlState.filterByTag('Ideas');
La note n'est pas trouvée
Solution: Vérifiez le chemin exact:
// 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:
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
- Tests unitaires: Créer des tests pour chaque méthode
- Tests E2E: Tester les flux complets avec Playwright
- Monitoring: Tracker les URLs les plus utilisées
- Analytics: Analyser les patterns de navigation
- Optimisation: Compresser les URLs longues si nécessaire