Skip to content

Commit

Permalink
🛠️ Signals!!!
Browse files Browse the repository at this point in the history
  • Loading branch information
brathis committed May 25, 2024
1 parent a6f6285 commit 604111b
Show file tree
Hide file tree
Showing 14 changed files with 177 additions and 182 deletions.
14 changes: 7 additions & 7 deletions src/app/app.component.html
Original file line number Diff line number Diff line change
Expand Up @@ -11,16 +11,16 @@ <h3>for Robinson R44</h3>
<app-fuel-gages
class="gages"
[tanks]="tanks"
[showOverlay]="showOverlay"
[showOverlay]="showOverlay()"
></app-fuel-gages>
<app-controls
class="controls"
[totalCapacity]="totalCapacity"
[state]="fuelGagesState"
[time]="time"
[result]="result"
(restartButtonClicked$)="restartButtonClicked()"
(guessSubmitted$)="guessSubmitted($event)"
(overlayToggled$)="overlayToggled($event)"
[state]="fuelGagesState()"
[time]="time()"
[result]="result()"
(restartButtonClicked)="restartButtonClicked()"
(guessSubmitted)="guessSubmitted($event)"
(overlayToggled)="overlayToggled($event)"
></app-controls>
</div>
89 changes: 49 additions & 40 deletions src/app/app.component.ts
Original file line number Diff line number Diff line change
@@ -1,88 +1,97 @@
import { Component, Inject, OnInit } from '@angular/core';
import { FuelTank } from './fuel-gages/fuel-tank.model';
import { Subscription, combineLatest, interval, timer } from 'rxjs';
import {
ChangeDetectionStrategy,
Component,
Inject,
OnInit,
computed,
signal,
} from '@angular/core';
import { FuelTank } from './fuel-tank.model';
import { Subscription, interval, timer } from 'rxjs';
import { FuelGagesState } from './fuel-gages/fuel-gages.state';
import { Result } from './result.model';
import { RESULTS_SERVICE, ResultsService } from './results.service';
import { Result } from './results/result.model';
import { RESULTS_SERVICE, ResultsService } from './results/results.service';

@Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.css'],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class AppComponent implements OnInit {
private static readonly MAX_DURATION = 30;

readonly tanks = [new FuelTank(64), new FuelTank(112)];
readonly totalCapacity = this.tanks
readonly totalCapacity: number = this.tanks
.map((tank) => tank.getCapacity())
.reduce((a, b) => a + b, 0);

private static readonly MAX_DURATION = 30;
totalQuantity = computed(() => {
let sum = 0;
for (const tank of this.tanks) {
sum += tank.getQuantity$()();
}
return sum;
});
fuelGagesState = signal(FuelGagesState.HIDDEN);
time = signal(0);
result = signal<Result | null>(null);
showOverlay = signal(false);

totalQuantity: number = 0;
fuelGagesState = FuelGagesState.HIDDEN;
time = 0;
timeSubscription: Subscription | null = null;
result: Result | null = null;
showOverlay: boolean = false;
private _timeSubscription: Subscription | null = null;

constructor(
@Inject(RESULTS_SERVICE) private readonly resultsService: ResultsService,
) {
const quantityObservables = [];
for (const tank of this.tanks) {
quantityObservables.push(tank.getQuantity$());
}
combineLatest(quantityObservables).subscribe((quantities) => {
let sum = 0;
for (let quantity of quantities) {
sum += quantity;
}
this.totalQuantity = sum;
});
}
) {}

ngOnInit(): void {
this.roll();
}

restartButtonClicked(): void {
if (this.fuelGagesState === FuelGagesState.VISIBLE) {
if (this.fuelGagesState() === FuelGagesState.VISIBLE) {
this.roll();
}
}

private roll(): void {
this.fuelGagesState = FuelGagesState.RESETTING;
this.fuelGagesState.set(FuelGagesState.RESETTING);
for (const tank of this.tanks) {
tank.setLevel(Math.random() * 100);
}
// FIXME: Instead of a timer, can we get an event when the CSS transition has completed?
timer(2000).subscribe((_value) => {
this.fuelGagesState = FuelGagesState.HIDDEN;
this.time = 0;
this.timeSubscription = interval(1000).subscribe((_value) => {
++this.time;
if (this.time === AppComponent.MAX_DURATION) {
this.timeSubscription?.unsubscribe();
this.fuelGagesState.set(FuelGagesState.HIDDEN);
this.time.set(0);
this._timeSubscription = interval(1000).subscribe((_value) => {
this.time.update((t) => t + 1);
if (this.time() === AppComponent.MAX_DURATION) {
this._timeSubscription?.unsubscribe();
}
});
});
}

private reveal(guess: number): void {
this.timeSubscription?.unsubscribe();
this.fuelGagesState = FuelGagesState.VISIBLE;
this.result = new Result(new Date(), guess, this.totalQuantity, this.time);
this.resultsService.add(this.result);
this._timeSubscription?.unsubscribe();
this.fuelGagesState.set(FuelGagesState.VISIBLE);
const result = new Result(
new Date(),
guess,
this.totalQuantity(),
this.time(),
);
this.result.set(result);
this.resultsService.add(result);
}

guessSubmitted(guess: number): void {
if (this.fuelGagesState === FuelGagesState.HIDDEN) {
if (this.fuelGagesState() === FuelGagesState.HIDDEN) {
this.reveal(guess);
}
}

overlayToggled(visible: boolean): void {
this.showOverlay = visible;
this.showOverlay.set(visible);
}
}
4 changes: 2 additions & 2 deletions src/app/app.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@ import { FuelGagesComponent } from './fuel-gages/fuel-gages.component';
import { FuelGageComponent } from './fuel-gages/fuel-gage/fuel-gage.component';
import { ControlsComponent } from './controls/controls.component';
import { FormsModule } from '@angular/forms';
import { LocalStorageResultsService } from './local-storage-results.service';
import { RESULTS_SERVICE } from './results.service';
import { LocalStorageResultsService } from './results/local-storage-results.service';
import { RESULTS_SERVICE } from './results/results.service';
import { OverlayComponent } from './fuel-gages/fuel-gage/overlay/overlay.component';

@NgModule({
Expand Down
27 changes: 15 additions & 12 deletions src/app/controls/controls.component.html
Original file line number Diff line number Diff line change
@@ -1,41 +1,44 @@
<div class="controls">
<div class="result" *ngIf="state === 'VISIBLE' && result">
<div class="result" *ngIf="state() === 'VISIBLE' && result">
<div class="result-guess">
<span class="label">Your Guess</span>
<span class="value">{{ result.getGuess() | number: "1.0-0" }} l</span>
<span class="value">{{ result()?.getGuess() | number: "1.0-0" }} l</span>
</div>
<div class="result-total">
<span class="label">Total</span>
<span class="value">{{ result.getActual() | number: "1.0-0" }} l</span>
<span class="value">{{ result()?.getActual() | number: "1.0-0" }} l</span>
</div>
<div class="result-error">
<span class="label">Error</span>
<span class="value">{{ result.getError() | number: "1.0-0" }} l</span>
<span class="value">{{ result()?.getError() | number: "1.0-0" }} l</span>
</div>
<div class="result-time">
<span class="label">Time Taken</span>
<span class="value">{{ result.getTime() }} s</span>
<span class="value">{{ result()?.getTime() }} s</span>
</div>
</div>
<div class="button">
<button *ngIf="state === 'VISIBLE'" (click)="restartButtonClicked()">
<button *ngIf="state() === 'VISIBLE'" (click)="restartButtonClicked.emit()">
Start over
</button>
</div>
<div class="timer" *ngIf="state === 'HIDDEN'">{{ time }} s</div>
<div class="guess" *ngIf="state === 'RESETTING' || state === 'HIDDEN'">
<div class="timer" *ngIf="state() === 'HIDDEN'">{{ time() }} s</div>
<div class="guess" *ngIf="state() === 'RESETTING' || state() === 'HIDDEN'">
<div class="label">
{{ state === "RESETTING" ? "Resetting..." : "Enter your guess:" }}
{{ state() === "RESETTING" ? "Resetting..." : "Enter your guess:" }}
</div>
<div class="inputs">
<input
type="number"
(keydown.enter)="guessSubmitted()"
(keydown.enter)="emitGuessSubmitted()"
[(ngModel)]="guess"
[disabled]="state === 'RESETTING'"
[disabled]="state() === 'RESETTING'"
#guessInput
/>
<button (click)="guessSubmitted()" [disabled]="state === 'RESETTING'">
<button
(click)="emitGuessSubmitted()"
[disabled]="state() === 'RESETTING'"
>
✔️
</button>
</div>
Expand Down
65 changes: 26 additions & 39 deletions src/app/controls/controls.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,71 +2,58 @@ import {
ChangeDetectionStrategy,
Component,
ElementRef,
EventEmitter,
Input,
OnChanges,
Output,
SimpleChanges,
ViewChild,
effect,
input,
output,
viewChild,
} from '@angular/core';
import { FuelGagesState } from '../fuel-gages/fuel-gages.state';
import { Result } from '../result.model';
import { Result } from '../results/result.model';

@Component({
selector: 'app-controls',
templateUrl: './controls.component.html',
styleUrls: ['./controls.component.css'],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class ControlsComponent implements OnChanges {
@Input() totalCapacity: number = 0;
@Input() state: FuelGagesState = FuelGagesState.RESETTING;
@Input() time: number = 0;
@Input() result: Result | null = null;
@Output() restartButtonClicked$: EventEmitter<void> = new EventEmitter();
@Output() guessSubmitted$: EventEmitter<number> = new EventEmitter();
@Output() overlayToggled$: EventEmitter<boolean> = new EventEmitter();
@ViewChild('guessInput') guessInput: ElementRef<HTMLInputElement> =
{} as ElementRef;
export class ControlsComponent {
totalCapacity = input.required<number>();
state = input.required<FuelGagesState>();
time = input.required<number>();
result = input.required<Result | null>();
overlayToggled = output<boolean>();
restartButtonClicked = output<void>();
guessSubmitted = output<number>();
guessInput = viewChild('guessInput', { read: ElementRef<HTMLInputElement> });
guess = '';
showOverlayState = false;

ngOnChanges(changes: SimpleChanges): void {
if (changes['state']) {
switch (changes['state'].currentValue) {
constructor() {
effect(() => {
switch (this.state()) {
case FuelGagesState.RESETTING:
this.guess = '';
if (this.showOverlayState) {
this.showOverlayState = false;
this.overlayToggled$.emit(false);
}
this.overlayToggled.emit(false);
break;
case FuelGagesState.HIDDEN:
// TODO: what is the correct lifecycle method to use to avoid setTimeout?
setTimeout(() => this.guessInput.nativeElement.focus(), 0);
// TODO: what is the correct way to use to avoid setTimeout?
setTimeout(() => this.guessInput()?.nativeElement.focus(), 0);
break;
}
}
}

restartButtonClicked(): void {
this.restartButtonClicked$.emit();
});
}

guessSubmitted(): void {
emitGuessSubmitted(): void {
const guess = Number.parseInt(this.guess);
if (!Number.isNaN(guess) && 0 <= guess && guess <= this.totalCapacity) {
this.guessSubmitted$.emit(guess);
if (!Number.isNaN(guess) && 0 <= guess && guess <= this.totalCapacity()) {
this.guessSubmitted.emit(guess);
}
}

showOverlay(): void {
this.showOverlayState = true;
this.overlayToggled$.emit(this.showOverlayState);
this.overlayToggled.emit(true);
}

hideOverlay(): void {
this.showOverlayState = false;
this.overlayToggled$.emit(this.showOverlayState);
this.overlayToggled.emit(false);
}
}
6 changes: 3 additions & 3 deletions src/app/fuel-gages/fuel-gage/fuel-gage.component.html
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,11 @@
<img
class="hand"
src="assets/fuel_gage_hand.svg"
[ngStyle]="{ transform: handRotationDeg }"
[ngStyle]="{ transform: handRotationDeg() }"
/>
<app-overlay
class="overlay"
[levels]="overlayLevels"
[ngStyle]="{ display: showOverlay ? 'block' : 'none' }"
[levels]="overlayLevels()"
[ngStyle]="{ display: showOverlay() ? 'block' : 'none' }"
></app-overlay>
</div>
26 changes: 10 additions & 16 deletions src/app/fuel-gages/fuel-gage/fuel-gage.component.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
import {
ChangeDetectionStrategy,
Component,
Input,
OnChanges,
SimpleChanges,
computed,
input,
} from '@angular/core';

@Component({
Expand All @@ -12,26 +11,21 @@ import {
styleUrls: ['./fuel-gage.component.css'],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class FuelGageComponent implements OnChanges {
@Input() level: number | null = 0;
@Input() overlayLevels: { level: number; label: string }[] = [];
@Input() showOverlay: boolean = false;
export class FuelGageComponent {
level = input.required<number | null>();
overlayLevels = input.required<{ level: number; label: string }[]>();
showOverlay = input.required<boolean>();

handRotationDeg = computed(() => {
return `rotate(${this.getDegrees(this.level() ?? 0)}deg`;
});

private static readonly a0 = 5.5;
private static readonly a1 = 0.445;
private static readonly a2 = 0.00183;
private static readonly a3 = 0.000104;
private static readonly a4 = -0.00000101;

handRotationDeg: string = 'none';

ngOnChanges(changes: SimpleChanges): void {
if (changes['level']) {
const level = changes['level'].currentValue ?? 0;
this.handRotationDeg = `rotate(${this.getDegrees(level)}deg`;
}
}

private getDegrees(level: number): number {
level = Math.max(Math.min(level, 100), 0);
return (
Expand Down
Loading

0 comments on commit 604111b

Please sign in to comment.