Blog

Notes on the .Net ecosystem, Angular, Software Architecture, Machine Learning and CyberSecurity

← All posts
RockPlayer in Angular 22: structure and onboarding
  • Angular 22
  • Signals

RockPlayer in Angular 22: structure and onboarding

With RockPlayerApi complete since article 6 (recommendation, cache and the YouTube adapter already working end to end), what's missing is the part the person actually sees: the screen. This article builds RockPlayerWeb, starting with the first thing any new person finds, the onboarding.

The folder structure, designed to grow without pain

No NgModule here, every component is standalone. The organization follows a simple division by responsibility:

src/app/
  pages/        one folder per screen (onboarding, player)
  models/       the types that flow through the application (PlaybackSource...)
  services/     HTTP calls to RockPlayerApi
  layout/       visual shell (the header)

None of this requires a monorepo or Nx to exist. The advantage of organizing it this way from the start is a different one: if one day the project grows to the point where it's worth splitting into multiple front-ends, moving a whole folder from pages/ to a new project is much simpler than untangling mixed files.

The onboarding: where everything starts

The first screen asks you to pick your favorite bands, exactly the cold start explained in article 1 of RockRecommender. For that, it needs a list of bands to show, and this is where a new piece appears: a GET /bands.

This endpoint exposes the catalog of bands that RockRecommender already keeps internally, and RockPlayerApi just forwards the call, following the same rule established since article 5: Angular never talks directly to RockRecommender, only to RockPlayerApi.

public interface IRecommenderClient
{
    Task<Result<Guid>> CreateUserAsync(IEnumerable<string> likedBands);
    Task<Result<NextSong>> GetNextSongAsync(Guid userId);
    Task SubmitFeedbackAsync(Guid userId, Guid songId, bool liked);

    // new since article 5
    Task<Result<List<string>>> GetBandsAsync();
}

public sealed class BandService(IRecommenderClient recommenderClient)
{
    public Task<Result<List<string>>> GetBandsAsync() => recommenderClient.GetBandsAsync();
}

[Route("bands")]
public sealed class BandsController(BandService bandService) : ApiControllerBase
{
    [HttpGet]
    public async Task<ActionResult<List<string>>> GetBands()
    {
        var result = await bandService.GetBandsAsync();

        return ToActionResult(result);
    }
}

IRecommenderClient gains one more method since article 5, and the usual rule still applies: the controller only knows BandService, never IRecommenderClient directly.

Fetching the list of bands in Angular

A simple service, using HttpClient (which under the hood already uses Fetch, as we saw in article 4). API_BASE_URL is just a one-line constant holding RockPlayerApi's address, shared by every service that needs to call it:

@Injectable({ providedIn: 'root' })
export class BandsService {
  private readonly http = inject(HttpClient);

  getBands() {
    return this.http.get<string[]>(API_BASE_URL + '/bands');
  }
}

The onboarding, built with signals

The band selection is kept in a simple signal, checked and unchecked on every click:

@Component({
  selector: 'app-onboarding',
  standalone: true,
  changeDetection: ChangeDetectionStrategy.OnPush,
  templateUrl: './onboarding.component.html',
})
export class OnboardingComponent {
  private readonly bandsService = inject(BandsService);
  private readonly usersService = inject(UsersService);
  private readonly router = inject(Router);

  readonly bands = toSignal(this.bandsService.getBands(), { initialValue: [] as string[] });
  readonly selectedBands = signal<string[]>([]);

  toggleBand(band: string) {
    this.selectedBands.update((current) =>
      current.includes(band) ? current.filter((existing) => existing !== band) : [...current, band]);
  }

  submit() {
    this.usersService.createUser(this.selectedBands()).subscribe((userId) => {
      this.router.navigate(['/player', userId]);
    });
  }
}

No Subject, no ngOnInit to fetch data on entry, toSignal already takes care of that. And since OnPush is the default starting with Angular 22, this screen doesn't even need to declare anything extra to react correctly when bands arrives from the API.

What's missing

With the person registered and redirected, only the screen that actually plays the song, receives like and dislike and asks for the next recommendation is left. That is article 8, the last one in the series.

Sources