1

My front-end is Angular 12 and backend - ASP.NET Core 5. I have a CRUD page for the rooms. The problem is that roomType is displayed with numbers - 1, 2 or 3. I want to display a description label instead. How can I do that?

  • 1 = "Small room"
  • 2 = "Medium room"
  • 3 = "Lecture hall"

Backend

public enum RoomType : int
{
    SmallRoom = 1,
    MediumRoom = 2,
    LectureHall = 3
}

public class RoomDto : IMapFrom<Room>
{
    public int Id { get; set; }
    public string Name { get; set; }
    public RoomType RoomType { get; set; }
    public int Seats { get; set; }
    public int DepartmentId { get; set; }
}

Frontend

room.ts

export enum RoomType {
  SmallRoom = 1,
  MediumRoom = 2,
  LectureHall = 3
}

export interface Room {
  id?: number;
  name?: string;
  roomType?: RoomType;
  seats?: number;
  departmentId?: number;
}

view-rooms.component.ts

import { Component, OnDestroy, OnInit, ViewChild } from '@angular/core';
import { Subject, takeUntil } from 'rxjs';

import { Room } from '../room';
import { RoomService } from '../room.service';
import { ConfirmationService, MessageService, PrimeNGConfig } from 'primeng/api';
import { BreadcrumbService } from '@core/services';
import { Table } from 'primeng/table';

@Component({
  selector: 'app-view-rooms',
  templateUrl: './view-rooms.component.html'
})
export class ViewRoomsComponent implements OnInit, OnDestroy {
  roomDialog: boolean;
  rooms: Room[];
  room: Room;
  selectedRooms: Room[];
  submitted: boolean;
  loading: boolean = true;

  private componentDestroyed$ = new Subject<boolean>();

  @ViewChild('dt') table: Table;

  constructor(
    private roomService: RoomService,
    private messageService: MessageService,
    private confirmationService: ConfirmationService,
    private breadcrumbService: BreadcrumbService,
    private primengConfig: PrimeNGConfig
  ) {
    this.breadcrumbService.setItems([{ label: 'Зали' }, { label: 'Преглед' }]);
  }

  ngOnInit() {
    this.roomService
      .getAllRooms()
      .pipe(takeUntil(this.componentDestroyed$))
      .subscribe((data) => {
        this.rooms = data;
        this.loading = false;
      });

    this.primengConfig.ripple = true;
  }

  ngOnDestroy() {
    this.componentDestroyed$.next(true);
    this.componentDestroyed$.complete();
  }

  applyFilterGlobal($event: any) {
    this.table.filterGlobal(($event.target as HTMLInputElement).value, 'contains');
  }

  openNew() {
    this.room = {};
    this.submitted = false;
    this.roomDialog = true;
  }

  deleteSelectedRooms() {
    this.confirmationService.confirm({
      message: 'Сигурни ли сте, че искате да изтриете данните за избраните зали?',
      header: 'Потвърждение',
      icon: 'pi pi-exclamation-triangle',
      acceptLabel: 'Да',
      rejectLabel: 'Не',
      accept: () => {
        this.rooms = this.rooms.filter((val) => !this.selectedRooms.includes(val));
        this.selectedRooms = [];
        this.messageService.add({
          severity: 'success',
          summary: 'Успешно',
          detail: 'Данните за залите са изтрити',
          life: 3000
        });
      }
    });
  }

  editRoom(room: Room) {
    this.room = { ...room };
    this.roomDialog = true;
  }

  deleteRoom(room: Room) {
    this.confirmationService.confirm({
      message: 'Сигурни ли сте, че искате да изтриете данните за зала ' + room.name + '?',
      header: 'Потвърждение',
      icon: 'pi pi-exclamation-triangle',
      acceptLabel: 'Да',
      rejectLabel: 'Не',
      accept: () => {
        this.rooms = this.rooms.filter((val) => val.id !== room.id);
        this.room = {};
        this.messageService.add({
          severity: 'success',
          summary: 'Успешно',
          detail: 'Данните за залата са изтрити',
          life: 3000
        });
      }
    });
  }

  hideDialog() {
    this.roomDialog = false;
    this.submitted = false;
  }

  saveRoom() {
    this.submitted = true;

    if (this.room.name?.trim()) {
      if (this.room.id) {
        this.rooms[this.findIndexById(this.room.id)] = this.room;
        this.messageService.add({
          severity: 'success',
          summary: 'Успешно',
          detail: 'Данните за залата са обновени',
          life: 3000
        });
      } else {
        this.rooms.push(this.room);

        // update to what's in db instead

        this.messageService.add({
          severity: 'success',
          summary: 'Успешно',
          detail: 'Залата е добавена',
          life: 3000
        });
      }

      this.rooms = [...this.rooms];
      this.roomDialog = false;
      this.room = {};
    }
  }

  findIndexById(id: number): number {
    let index = -1;
    for (let i = 0; i < this.rooms.length; i++) {
      if (this.rooms[i].id === id) {
        index = i;
        break;
      }
    }

    return index;
  }
}

view-rooms.component.html

<div class="p-grid">
  <div class="p-col-12">
    <p-toast></p-toast>

    <div class="card">
      <p-toolbar styleClass="p-mb-4">
        <ng-template pTemplate="left">
          <button
            pButton
            pRipple
            label="Добавяне"
            icon="pi pi-plus"
            class="p-button-success p-mr-2 p-mb-2"
            (click)="openNew()"
          ></button>
          <button
            pButton
            pRipple
            label="Изтриване"
            icon="pi pi-trash"
            class="p-button-danger p-mb-2"
            (click)="deleteSelectedRooms()"
            [disabled]="!selectedRooms || !selectedRooms.length"
          ></button>
        </ng-template>
      </p-toolbar>

      <p-table
        #dt
        [value]="rooms"
        [(selection)]="selectedRooms"
        dataKey="id"
        styleClass="p-datatable-rooms"
        [rowHover]="true"
        [rows]="10"
        [showCurrentPageReport]="true"
        [rowsPerPageOptions]="[10, 25, 50]"
        [loading]="loading"
        [paginator]="true"
        currentPageReportTemplate="Показват се от {first} до {last} от общо {totalRecords} записа"
        [globalFilterFields]="['name', 'roomType', 'seats', 'departmentId']"
      >
        <ng-template pTemplate="caption">
          <div class="p-d-flex p-flex-column p-flex-md-row p-jc-md-between table-header">
            <h5 class="p-m-0">Зали</h5>
            <span class="p-input-icon-left">
              <i class="pi pi-search"></i>
              <input
                pInputText
                type="text"
                (input)="applyFilterGlobal($event)"
                placeholder="Търсене..."
              />
            </span>
          </div>
        </ng-template>
        <ng-template pTemplate="header">
          <tr>
            <th style="width: 3rem">
              <p-tableHeaderCheckbox></p-tableHeaderCheckbox>
            </th>
            <th pSortableColumn="name">Зала <p-sortIcon field="name"></p-sortIcon></th>
            <th pSortableColumn="roomType">Тип <p-sortIcon field="roomType"></p-sortIcon></th>
            <th pSortableColumn="seats">Места <p-sortIcon field="seats"></p-sortIcon></th>
            <th pSortableColumn="departmentId">
              Катедра <p-sortIcon field="departmentId"></p-sortIcon>
            </th>
            <th></th>
          </tr>
        </ng-template>
        <ng-template pTemplate="body" let-room>
          <tr>
            <td>
              <p-tableCheckbox [value]="room"></p-tableCheckbox>
            </td>
            <td>
              {{ room.name }}
            </td>
            <td>
              {{ room.roomType }}
            </td>
            <td>
              {{ room.seats }}
            </td>
            <td>
              {{ room.departmentId }}
            </td>
            <td>
              <button
                pButton
                pRipple
                icon="pi pi-pencil"
                class="p-button-rounded p-button-success p-mr-2"
                (click)="editRoom(room)"
              ></button>
              <button
                pButton
                pRipple
                icon="pi pi-trash"
                class="p-button-rounded p-button-warning"
                (click)="deleteRoom(room)"
              ></button>
            </td>
          </tr>
        </ng-template>
        <ng-template pTemplate="summary">
          <div class="p-d-flex p-ai-center p-jc-between">
            Общо {{ rooms ? rooms.length : 0 }} зали.
          </div>
        </ng-template>
      </p-table>
    </div>

    <p-dialog
      [(visible)]="roomDialog"
      [style]="{ width: '450px' }"
      header="Данни за зала"
      [modal]="true"
      styleClass="p-fluid"
    >
      <ng-template pTemplate="content">
        <div class="p-field">
          <label for="name">Зала</label>
          <input
            #name="ngModel"
            id="name"
            type="text"
            pInputText
            [(ngModel)]="room.name"
            [ngClass]="{
              'ng-dirty': (name.invalid && submitted) || (name.dirty && name.invalid)
            }"
            required
            autofocus
          />
          <small class="p-error" *ngIf="(name.invalid && submitted) || (name.dirty && name.invalid)"
            >Залата е задължителна.</small
          >
        </div>
        <div class="p-field">
          <label for="roomType">Тип</label>
          <input
            #roomType="ngModel"
            id="roomType"
            type="text"
            pInputText
            [(ngModel)]="room.roomType"
            [ngClass]="{
              'ng-dirty': (roomType.invalid && submitted) || (roomType.dirty && roomType.invalid)
            }"
            required
          />
          <small
            class="p-error"
            *ngIf="(roomType.invalid && submitted) || (roomType.dirty && roomType.invalid)"
            >Типът е задължителен.</small
          >
        </div>
      </ng-template>

      <ng-template pTemplate="footer">
        <button
          pButton
          pRipple
          label="Отказ"
          icon="pi pi-times"
          class="p-button-text"
          (click)="hideDialog()"
        ></button>
        <button
          pButton
          pRipple
          label="Запази"
          icon="pi pi-check"
          class="p-button-text"
          (click)="saveRoom()"
        ></button>
      </ng-template>
    </p-dialog>

    <p-confirmDialog [style]="{ width: '450px' }"></p-confirmDialog>
  </div>
</div>

room.service.ts

import { HttpClient } from '@angular/common/http';
import { Injectable } from '@angular/core';

import { environment } from '@env';
import { Room } from './room';

@Injectable({
  providedIn: 'root'
})
export class RoomService {
  private actionUrl: string;

  constructor(private httpClient: HttpClient) {
    this.actionUrl = `${environment.apiUrl}/rooms`;
  }

  getAllRooms() {
    return this.httpClient.get<Room[]>(this.actionUrl);
  }

  getRoomById(id: number) {
    return this.httpClient.get<Room>(`${this.actionUrl}/${id}`);
  }

  addRoom(room: Room) {
    return this.httpClient.post<Room>(this.actionUrl, room);
  }

  updateRoom(room: Room) {
    return this.httpClient.put<Room>(`${this.actionUrl}/${room.id}`, room);
  }

  deleteRoom(id: number) {
    return this.httpClient.delete(`${this.actionUrl}/${id}`);
  }
}

nop
  • 4,711
  • 6
  • 32
  • 93
  • `RoomType[0] RoomType[1]` etc ... https://stackoverflow.com/questions/42299257/cast-int-to-enum-strings-in-typescript – Stavm Aug 01 '21 at 12:14
  • @Stavm, note that I need descriptions that I can later translate to multiple languages. "Small room", not the enum names SmallRoom. – nop Aug 01 '21 at 12:17
  • `RoomType[0].replace(/(?<!^)([A-Z])/, " $1")` https://stackoverflow.com/questions/5582228/insert-space-before-capital-letters – Stavm Aug 01 '21 at 12:19

1 Answers1

2

One option is to simply have a separate object that records metadata for every enum value:

export enum RoomType {
    SmallRoom = 1,
    MediumRoom = 2,
    LectureHall = 3
}

export const RoomDescriptions: Record<RoomType, string> = {
    [RoomType.SmallRoom]: 'A small room',
    [RoomType.MediumRoom]: 'A medium room',
    [RoomType.LectureHall]: 'A lecture hall',
};

const smallRoomDesc: string = RoomDescriptions[RoomType.SmallRoom];

Another is to replace your enum with an object:

export interface Room {
    name: string;
    description: string;
}

export const Room = (<T extends Record<keyof T, Room>>(types: T) => types)({
    SmallRoom: {
        name: 'Small room',
        description: 'A small room',
    },
    MediumRoom: {
        name: 'Medium room',
        description: 'A medium room',
    },
    LectureHall: {
        name: 'Lecture hall',
        description: 'A lecture hall',
    },
});

export type RoomTypes = keyof typeof Room;
// ^ "SmallRoom" | "MediumRoom" | "LectureHall"

const smallRoomDesc: string = Room.SmallRoom.description;

As shown as a comment, RoomTypes is the union of all keys. The Room constant has this as type: Type of Room

That means Room.SmallRoom is an object instead of a simple string/number. For serialization, you'd probably want to use the key instead of the object.

Alternatively, you can use this definition:

export const Room = (<T extends Record<keyof T, Room>>(types: T): { [key in keyof T]: Room } => types)({
    SmallRoom: {
        name: 'Small room',
        description: 'A small room',
    },
    MediumRoom: {
        name: 'Medium room',
        description: 'A medium room',
    },
    LectureHall: {
        name: 'Lecture hall',
        description: 'A lecture hall',
    },
});

To have the type of const Room be nicer to look at:

Type of alternative Room definition

Both definitions come with type checking, i.e. it would show an error if you don't define (or use the wrong type for) name and such.

Kelvin Schoofs
  • 8,323
  • 1
  • 12
  • 31
  • Thanks you for your descriptive answer! Which way do you recommend mostly, taking into account that the page is CRUD and I gotta send the room_type back to the backend as an integer when I add/update item. – nop Aug 01 '21 at 12:33
  • @nop The first approach might make it easiest. Then your `RoomEnum.Something` is a simple number you can transmit. Otherwise with the 2nd (and 3rd) approach, `RoomEnum.Something` would be a whole object, meaning you'd want to store the key instead. It really depends on what's best/easiest, which might be the 1st approach in your case. _Also my `RoomDescriptions` is a `Record`, but `string` could also be an interface for a complex object, if you want more than just a description._ – Kelvin Schoofs Aug 01 '21 at 12:37
  • Thanks! How do I "bind" it to the `Room` object, because I'm actually displaying in the HTML file the Room object and RoomType is just the enum. https://pastebin.com/U8NY4pd7 – nop Aug 01 '21 at 12:43
  • You mean how to get the description for a `Room`? Assuming you have `const room: Room = ...;`, you can do `RoomDescription[room.roomType]` to get the description. – Kelvin Schoofs Aug 01 '21 at 12:44
  • I mean in `view-rooms.component.html`, I have ` ... {{ room.roomType }}`. How do I display the room description in that case? `this.roomService.getAllRooms()` returns `Room[]`. – nop Aug 01 '21 at 12:46
  • I don't actually use Angular, but I assume it's simply `RoomDescription[room.roomType]` (once you imported `RoomDescription`, of course) – Kelvin Schoofs Aug 01 '21 at 12:47
  • I had to create a function `getRoomType(roomType: RoomType) { return RoomDescription[roomType]; }` and then `{{ getRoomType(room.roomType) }}`. Thank you again! – nop Aug 01 '21 at 12:52