TekOnline

# Building a Stateful Tag Selection System in Angular with Signals and Services

Introduction

In modern web applications, maintaining state across navigation while providing a responsive user interface can be challenging. In this article, we’ll explore how to build a robust tag selection system using Angular’s latest features including signals, services, and Material Design components.

The Challenge

When implementing a tag selection system, we often face several challenges:

  1. Maintaining selection state across route navigation
  2. Keeping the UI in sync with the underlying data
  3. Managing loading states during API operations
  4. Handling edge cases like duplicate selections
  5. Providing immediate feedback while ensuring data consistency

The Solution: A Three-Layer Architecture

Our solution uses a three-layer architecture:

  1. State Management: A singleton service using RxJS BehaviorSubject
  2. Component Logic: Angular signals for local state
  3. UI Layer: Material Design chips with two-way binding

1. The Tags Service

The service acts as our single source of truth:


@Injectable({
  providedIn: 'root',
})
export class TagsService {
  private tagSubject = new BehaviorSubject([]);
  public alltags$ = this.tagSubject.asObservable();
  public selectedTagsSubject = new BehaviorSubject([]);
  public selectedTags$ = this.selectedTagsSubject.asObservable();

  constructor(private client: Client) {
    this.fetchTags();
  }

  public fetchTags() {
    this.client.tagsAll().subscribe(tags => {
      this.updateTags(tags);
    });
  }

  public updateSelectedTags(tags: ITag[]) {
    this.selectedTagsSubject.next(tags);
  }
}

2. The Component Implementation

The component uses signals for local state management and subscribes to the service:


export class TagindexComponent {
  selectedChips = signal([]);

  ngOnInit() {
    // Keep local state in sync with service
    this.tagsservice.selectedTags$.subscribe(tags => {
      this.selectedChips.set(tags);
    });
  }

  isTagSelected(tag: ITag): boolean {
    return this.selectedChips().some(t => t.id === tag.id);
  }

  chipGridChange(event: MatChipGridChange, tag: ITag) {
    let chips = [...this.selectedChips()];
    if (event.selected) {
      if (!chips.some(c => c.id === tag.id)) {
        chips.push(tag);
      }
    } else {
      chips = chips.filter(c => c.id !== tag.id);
    }
    this.selectedChips.set(chips);
    this.tagsservice.updateSelectedTags(chips);
  }
}

3. The Template

The template uses Material Design chips with proper bindings:



  @for (tag of tagsservice.alltags$|async; track tag.id) {
    
      {{tag.tagstring}}
    
  }

Key Features

1. State Persistence

The service maintains state across navigation using BehaviorSubject, ensuring selected tags persist even when the component is destroyed and recreated.

2. Immutable Updates

We use the spread operator to create new arrays when updating state:


let chips = [...this.selectedChips()];

3. Duplicate Prevention

Before adding a tag, we check if it already exists:


if (!chips.some(c => c.id === tag.id)) {
  chips.push(tag);
}

4. Loading State Management

The component tracks loading states during API operations:


this.loading = true;
this.client.tagsPOST(new Tag2({ tagstring: value }))
  .subscribe({
    next: () => {
      this.tagsservice.fetchTags();
      this.loading = false;
    },
    error: () => this.loading = false
  });

Best Practices Implemented

  1. Single Source of Truth: The TagsService is the authoritative source for tag data
  2. Immutable State Updates: Always creating new arrays for state changes
  3. Type Safety: Using proper TypeScript interfaces and types
  4. Error Handling: Proper error handling in API calls
  5. Loading States: Managing loading indicators for better UX
  6. Clean Code: Following Angular style guide and best practices

Performance Considerations

  1. Using trackBy for optimal rendering performance
  2. Implementing proper change detection strategies
  3. Using signals for efficient state management
  4. Avoiding unnecessary template expressions

Conclusion

This implementation provides a robust, maintainable, and user-friendly tag selection system. By leveraging Angular’s latest features and following best practices, we’ve created a solution that:

  • Maintains state across navigation
  • Provides immediate user feedback
  • Handles edge cases gracefully
  • Scales well with growing application needs

The combination of signals for local state and services for global state management offers a powerful pattern for building complex, stateful features in Angular applications.

Next Steps

Consider extending this system with:

  • Drag and drop reordering
  • Tag categories or hierarchies
  • Batch operations
  • Advanced filtering
  • Undo/redo functionality

Remember to always consider your specific use case and requirements when implementing similar features in your own applications.


Posted

in

by

Tags:

Comments

Leave a Reply

Your email address will not be published. Required fields are marked *