Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Style binding on (click) in Angular

Tags:

angular

svg

I'm trying to make an interactive seats map where if you click on the seat it changes its color. I found some solutions but they're not working.

I have SVG element. I want the the black rectangle to change its color into red when clicked.

<g>
  <rect [style.color]="toggleColor()" (click)="toggleStyle = !toggleStyle;" id="2a" x="90.714" y="65.012" width="27.97" height="30.994"/>
</g>

The code in component is as below:

public toggleStyle: boolean = false;

toggleColor() {
  console.log("does it work?")
  if (this.toggleStyle) {
    return "red";
  } else {
    return "";
  }
} 

}

As you can see on the stackblitz - this does not add the color to the rectangle. Additionally, the function runs twice due to the fact that it's part of the element.

STACKBLITZ

Thank you for any suggestions on how to fix this!

like image 378
JoseTurron Avatar asked Jul 28 '26 04:07

JoseTurron


2 Answers

Since it's a seat selection, better way would be to define an array of object with x, y and selected properties. It can then be toggled directly in the template without using any event handler. Try the following

Controller

export class ClickSVGComponent implements OnInit {
  public seats: Array<Array<{x: number, y: number, selected: boolean}>> = [];
  range = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9];

  public toggleStyle: boolean = false;

  constructor() {
    for (let i = 0; i < 10; i++) {
      this.seats[i] = [];
      for (let j = 0; j < 10; j++) {
        this.seats[i].push({x: (i * 10), y: (j * 10), selected: false});
      }
    }
  }

  ngOnInit() {
  }
}

Template

<svg width="210mm" height="297mm" version="1.1" viewBox="0 0 210 297" xmlns="http://www.w3.org/2000/svg" xmlns:cc="http://creativecommons.org/ns#" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#">

  <g>
    <ng-container *ngFor="let i of range">
      <ng-container *ngFor="let j of range">
        <rect 
          [attr.fill]="seats[i][j].selected ? 'red': ''"      <!-- use ternary operator to set the fill value -->
          id="{{i}}{{j}}a" 
          [attr.x]="seats[i][j].x" 
          [attr.y]="seats[i][j].y" 
          width="8" 
          height="8" 
          (click)="seats[i][j].selected = !seats[i][j].selected"
        />
      </ng-container>
    </ng-container>
  </g>
</svg>

I've modified your Stackblitz

Update: max selection condition

I've changed the object structure to introduce a maxSelected boolean that denotes if the maximum no. of selections has been reached. To check for the condition multiple array maps and an array concat is applied in a counter() event handler for click event.

The quickest way to understand it would be to dissect the conditions and observe of the output of each statement. It is a fairly straight-forward condition written as a single statement.

The template is also adjusted for the condition. We need to allow re-selection if the maxReached is again set to false.

Controller

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

export const MAX_SELECTION = 9;       // <-- max no. of seats allowed to select

interface Seats {       // <-- a collection of seats
  maxReached: boolean,
  attr: Array<Array<Attribute>>   // array of array - `x` denotes the row, `y` denotes the column
}

interface Attribute {    // <-- properties of each seat
  x: number,
  y: number,
  selected: boolean
}

@Component({
  selector: 'app-click-svg',
  templateUrl: './click-svg.component.html',
  styleUrls: ['./click-svg.component.css']
})
export class ClickSVGComponent implements OnInit {
  public seats: Seats = {maxReached: false, attr: []};
  range = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9];

  public toggleStyle: boolean = false;

  constructor() {
    for (let i = 0; i < 10; i++) {     // <-- loop through row of seats
      this.seats.attr[i] = [];         // <-- each row is initially empty
      for (let j = 0; j < 10; j++) {   // <-- loop through each seat of the row
        this.seats.attr[i].push({x: (i * 10), y: (j * 10), selected: false});
        //                          ^             ^           ^
        //                          |             |           |
        // use row value to set x ---             |           |
        // use column value to set y coord. -------           |
        // by default the seat is not selected ----------------
      }
    }
  }

  ngOnInit() {
  }

  counter() {
    /* 
    Do the following to understand what each statement does

    const seatsAll = this.seats.attr.map(row => { 
      console.log('row': row)
      row.map(seat => {
        console.log('seat': seat);
        seat.selected;
      })
    })
    console.log('all seats': seatsAll);
    const seatsSelectedConcat = [].concat.apply([], seatsAll);
    console.log('all seats single array': seatsSelectedConcat);
    const seatsSelectedTrue = seatsSelectedConcat.filter(status => status);
    console.log('all seats single array': seatsSelectedTrue);
    */

    const selected = (
      [].concat.apply([], (         // <-- output (Array(100)): [true, false, true, false, true,...]
        this.seats.attr.map(        // <-- output (Array<Array(10)>(10)): [[true, false...], [false, true,...], ...]  
          row => row.map(
            seat => seat.selected   // <-- output (Array(10)): [true, false, true...]  
          )
        )
      ))
    )
    .filter(status => status)       // <-- output only true: [true, true, true]
    .length;                        // <-- number of seats selected
    
    if (selected === MAX_SELECTION) {
      this.seats.maxReached = true;
    } else {
      this.seats.maxReached = false;
    }
  }
}

Template

<p>
  Please select a maximum of 9 seats. <br>
  <span style="color: red" *ngIf="seats.maxReached">
    Maximum number of seats selected.
  </span>
</p>

<svg width="210mm" height="297mm" version="1.1" viewBox="0 0 210 297" xmlns="http://www.w3.org/2000/svg" xmlns:cc="http://creativecommons.org/ns#" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#">

  <g>
    <ng-container *ngFor="let i of range">        <!-- same loop as the controller - row -->
      <ng-container *ngFor="let j of range">      <!-- column -->
        <rect 
          [attr.fill]="seats.attr[i][j].selected ? 'red': ''" 
          id="{{i}}{{j}}a" 
          [attr.x]="seats.attr[i][j].x" 
          [attr.y]="seats.attr[i][j].y" 
          width="8" 
          height="8"
          (click)="
            seats.attr[i][j].selected ?           
              seats.attr[i][j].selected = !seats.attr[i][j].selected :     <!-- if seat selected already deselect it --> 
              !seats.maxReached ?
                seats.attr[i][j].selected = !seats.attr[i][j].selected :   <!-- selected unselected seat only if max condition `false` -->
                '';                                                        <!-- don't select the seat if max condition `true` -->
            counter()" 
        />
      </ng-container>
    </ng-container>
  </g>
</svg>

Updated Stackblitz

like image 164
ruth Avatar answered Jul 30 '26 17:07

ruth


You have to use fill to change the color of an svg rect.

<rect [style.fill]="toggleColor()" (click)="toggleStyle = !toggleStyle;"></rect>

You can also use the attribute binding:

<rect [attr.fill]="toggleColor()" (click)="toggleStyle = !toggleStyle;"></rect>

Property binding does not work, because the attributes of svg are not reflected as properties on the element, so binding on svg happens with the [attr.*] notation

working example

You can also think about setting a property, instead of calling a function to get the current color. The reason it's calling the function twice, is because angular is running in development mode, and does two calls of change detection to make sure nothing changed after the first round

like image 44
Poul Kruijt Avatar answered Jul 30 '26 18:07

Poul Kruijt



Donate For Us

If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!