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!
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
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
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
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With