Angular: style or class binding, seat map

Viewed 776

I'm struggling with project for my postgrad studies. I need to make interactive seats map in Angular with max selection of 9 seats. After looking through my previous classes I was able to make some magic in JS but without max selection (link to codepen below).

HTML code contains huge SVG graphic - you can find it in codepen attached below

CSS

.occupied {
    fill: red;
}

.free {
    fill: #afafaf;
}

Javascript

const buttonArray = document.querySelectorAll("#seats path");
console.log("query", typeof buttonArray);


var myArray = Array.from(buttonArray);
console.log(myArray);

for (let i = 0; i < myArray.length; i++) {
    myArray[i].setAttribute("id", `seat_${i + 1}`);

    document.getElementById(`seat_${i + 1}`).addEventListener("click", function() {
        if (document.getElementById(this.id).getAttribute("class") == "occupied") {
            document.getElementById(this.id).removeAttribute("style");
            document.getElementById(this.id).setAttribute("class", "free");
        } else {
            document.getElementById(this.id).removeAttribute("style");
            document.getElementById(this.id).setAttribute("class", "occupied");
        }
    });
}

Codepen

I have no clue how to implement this into Angular, or how to even make it simplier. And of course how to make this maximum selection of 9 seats.

I would be grateful for any of your advices and help.

Stackblitz

3 Answers

You can add onClick function to path=seats

<g id="seats"  (click)="onClick($event)">

And then with closest method get seat, which was clicked. In your example all seat paths have class st121. That's mean we can get seat with $event.target.closest('.st121');

For max seats, you can add a counter and watch for it.

import {
    Component,
    OnInit
} from '@angular/core';

const MAX_SEATS = 9;
@Component({
    selector: 'app-seatmap',
    templateUrl: './seatmap.component.html',
    styleUrls: ['./seatmap.component.css']
})
export class SeatmapComponent implements OnInit {
    constructor() {}
    ngOnInit() {}

    counter = 0;

    onClick($event) {
        const seat = $event.target.closest('.st121');
        if (!seat) {
            return;
        }

        const res = seat.getAttribute("class").split(' ').indexOf('occupied');

        if (res > -1) {
            seat.removeAttribute("style");
            seat.setAttribute("class", "free st121");
            this.counter -= 1;
        } else if (this.counter < MAX_SEATS) {
            seat.removeAttribute("style");
            seat.setAttribute("class", "occupied st121");
            this.counter += 1;
        }


    }

}

Why not use Angular?

We are going to define a Directive, but the first thing we need if modify the svg for to be grouped the seat (not the letter and the background separate)

One seat becomes then like

   <g seat id="26F" >
        <path #background class="st121" d="M285.8,1126.1h-17c-0.6,0-1-0.4-1-1v-22c0-0.6,0.4-1,1-1h17c0.6,0,1,0.4,1,1v22
            C286.8,1125.7,286.3,1126.1,285.8,1126.1z "/>
      <path #letter class="st122" d="M275.8,1115.2v-6.4h4.2v1.2h-2.9v1.7h2.6v1.1h-2.6v2.5L275.8,1115.2L275.8,1115.2z"></path>
   </g>

See that it's not more than in a "group" put the two paths

I write <g seat..>. Yes an attribute directive. See also that we use two reference variables, one for background and another one for the letter

the directive becomes like

@Directive({
  selector: '[seat]',
  exportAs: 'seat'
})
export class SeatDirective {
  @ContentChild('background') background:ElementRef //<--reference to background
  @ContentChild('letter') letter:ElementRef  //<--reference to letter

  @HostListener('click', ['$event.target'])  //<--a HostListener
  onClick()                                  //when click
  {
    this.seatmap.seatClick(this) //call to a funcion of the "seatmap"
  }
  checked:boolean=false  //a variable that store if checked or not


  constructor(public el:ElementRef,@Host() private seatmap:SeatmapComponent ){}
}

See the "@Host" in constructor, we are looking for in the component where the element who has the directive belong. This make easy call to a function in our SeatmapComponent

Well, in our SeatmapComponent we are get in a QueryList the "seats" to allow count them, and a simple function that change the fill and the stroke of the seat and the letter or our seat using renderer2

  @ViewChildren(SeatDirective) seats:QueryList<SeatDirective>
  constructor(private renderer:Renderer2) { }

  seatClick(seat:SeatDirective)
  {
    if (!seat.checked && this.count()>=9)
      return;
    seat.checked=!seat.checked;
    this.renderer.setStyle(seat.background.nativeElement,"fill",seat.checked?'red':'white')
    this.renderer.setStyle(seat.letter.nativeElement,"fill",seat.checked?'white':'black')
    this.renderer.setStyle(seat.letter.nativeElement,"stroke",seat.checked?'white':'black')
  }
  count()
  {
    return this.seats.filter(x=>x.checked).length
  }

  seatsChoosed()
  {
     return this.seat.filter(x=>x.checked)
        .map(x=>x.el.nativeElement.getAttribute('id'))
  }

There're another aproach that is create the seats "on-fly", using an array to put the positions and use rect and text in the svg

If we has an array of rows

  filas = [
    { y: 250, x: [0, 0, 0, 195.5, 175.2, 154.8] },
    { y: 281.9, x: [285.8, 265.5, 245.1, 195.5, 175.2, 154.8] },
    { y: 313.9, x: [285.8, 265.5, 245.1, 195.5, 175.2, 154.8] },
    { y: 345.9, x: [285.8, 265.5, 245.1, 195.5, 175.2, 154.8] },
        ...
    ]

We can create on fly the svg, see that if we use 0 as coordenate don't create the seat

<svg version="1.1" id="whole_x5F_plane" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px"
     y="0px" viewBox="0 0 425 1467.9" style="enable-background:new 0 0 425 1467.9;" xml:space="preserve">
<style type="text/css">
  .st0{clip-path:url(#SVGID_15_);}
  .st1{clip-path:url(#SVGID_24_);}
    ...
  .free{fill:#FFFFFF;stroke:#050404;stroke-width:0.25;stroke-miterlimit:10;}
  .booked{fill:#8E8E8E;stroke:#050404;stroke-width:0.25;stroke-miterlimit:10;}
  .selected{fill:#FFFF00;stroke:#050404;stroke-width:0.25;stroke-miterlimit:10;}
</style>
...
<ng-container *ngFor="let fila of filas;let i=index">
    <ng-container *ngFor="let x of fila.x;let j=index">
        <!--if x==0 don't create the seat-->
        <g style="cursor:pointer" *ngIf="x" (click)="click(i,j)">
           <!--a rectangle-->
            <rect [ngClass]="status[i][j]" [attr.x]="x-18" [attr.y]="fila.y-22" width="20" height="20" rx="2"
                ry="2" />
            <!--and the text-->
            <text [attr.x]="x-16" [attr.y]="fila.y-8" font-family="Verdana" 
                font-size="11" stroke="transparent"
                fill="#333">
                {{this.letras[j]}}
            </text>
        </g>
    </ng-container>
</ng-container>
</svg>

As we has in the svg the class booked,free,select, the click becomes easy, remember we use an array of array to store the "class" of the seats

  status = this.filas.map(x => [
    "free",
    "free",
    "free",
    "free",
    "free",
    "free"
  ]);

  click(fila, column) {

    if (this.status[fila][column] == "free" && this.count() < 3) //here check the maximum
      this.status[fila][column] = "selected";
    else {
      if (this.status[fila][column] == "selected")
        this.status[fila][column] = "free";
    }
  }

  count() {
    return this.status
      .map(x => x.filter(s => s == "selected").length)
      .reduce((a, b) => a + b);
  }

The stackblitz

NOTE: Really I'm not very happy with my choose of the arrays, perhaps it's better define an array of object with row,letter,x,y and status. but it's a to do

Related