meals-made-easy-app/src/app/pages/recipe-upload-page/steps/enter-recipe-data/enter-recipe-data.ts
2026-01-29 17:23:10 -06:00

192 lines
6.1 KiB
TypeScript

import {
afterNextRender,
Component,
ElementRef,
inject,
Injector,
input,
OnInit,
output,
runInInjectionContext,
viewChild,
viewChildren,
} from '@angular/core';
import { RecipeUploadClientModel } from '../../../../shared/client-models/RecipeUploadClientModel';
import { FormArray, FormControl, FormGroup, ReactiveFormsModule, Validators } from '@angular/forms';
import { MatFormField, MatInput, MatLabel } from '@angular/material/input';
import { MatButton } from '@angular/material/button';
import { EnterRecipeDataSubmitEvent } from './EnterRecipeDataSubmitEvent';
import {
MatCell,
MatCellDef,
MatColumnDef,
MatHeaderCell,
MatHeaderCellDef,
MatHeaderRow,
MatHeaderRowDef,
MatRow,
MatRowDef,
MatTable,
} from '@angular/material/table';
import { FaIconComponent } from '@fortawesome/angular-fontawesome';
import { faEllipsis } from '@fortawesome/free-solid-svg-icons';
import { MatMenu, MatMenuItem, MatMenuTrigger } from '@angular/material/menu';
@Component({
selector: 'app-enter-recipe-data',
imports: [
ReactiveFormsModule,
MatFormField,
MatLabel,
MatInput,
MatButton,
MatTable,
MatColumnDef,
MatHeaderCell,
MatHeaderCellDef,
MatCell,
MatCellDef,
MatHeaderRow,
MatHeaderRowDef,
MatRow,
MatRowDef,
FaIconComponent,
MatMenuTrigger,
MatMenu,
MatMenuItem,
],
templateUrl: './enter-recipe-data.html',
styleUrl: './enter-recipe-data.css',
})
export class EnterRecipeData implements OnInit {
public readonly model = input.required<RecipeUploadClientModel>();
public readonly submit = output<EnterRecipeDataSubmitEvent>();
public readonly deleteDraft = output<void>();
protected recipeTextTextarea = viewChild.required<ElementRef<HTMLTextAreaElement>>('recipeTextTextarea');
protected ingredientsTable = viewChild.required<
MatTable<
FormGroup<{
amount: FormControl<string | null>;
name: FormControl<string | null>;
notes: FormControl<string | null>;
}>
>
>('ingredientsTable');
protected ingredientAmountControls = viewChildren<ElementRef<HTMLInputElement>>('ingredientAmount');
protected readonly recipeFormGroup = new FormGroup({
title: new FormControl('', Validators.required),
slug: new FormControl('', Validators.required),
ingredients: new FormArray(
[] as Array<
FormGroup<{
amount: FormControl<string | null>;
name: FormControl<string | null>;
notes: FormControl<string | null>;
}>
>,
),
text: new FormControl('', Validators.required),
});
protected readonly ingredientsColumnsToDisplay = ['amount', 'name', 'notes'];
private readonly injector = inject(Injector);
public ngOnInit(): void {
const model = this.model();
this.recipeFormGroup.patchValue({
title: model.draft?.title ?? '',
slug: model.draft?.slug ?? '',
text: model.draft?.rawText ?? '',
ingredients: model.draft?.ingredients ?? [],
});
runInInjectionContext(this.injector, () => {
afterNextRender({
mixedReadWrite: () => {
this.updateTextareaHeight(this.recipeTextTextarea().nativeElement);
},
});
});
}
private updateTextareaHeight(textarea: HTMLTextAreaElement) {
const windowScrollX = window.scrollX;
const windowScrollY = window.scrollY;
textarea.style.height = 'auto';
textarea.style.height = textarea.scrollHeight + 'px';
requestAnimationFrame(() => {
window.scrollTo(windowScrollX, windowScrollY);
});
}
protected onRecipeTextChange(event: Event): void {
this.updateTextareaHeight(event.target as HTMLTextAreaElement);
}
protected addIngredient() {
const control = new FormGroup({
amount: new FormControl(''),
name: new FormControl('', Validators.required),
notes: new FormControl(''),
});
this.recipeFormGroup.controls.ingredients.push(control);
this.ingredientsTable().renderRows();
const addedIndex = this.recipeFormGroup.controls.ingredients.length - 1;
const target = this.ingredientAmountControls()[addedIndex];
target.nativeElement.focus();
}
protected removeIngredient(index: number) {
this.recipeFormGroup.controls.ingredients.removeAt(index);
}
protected getIngredientControl(index: number, column: 'amount' | 'name' | 'notes'): FormControl {
const ingredientGroup = this.recipeFormGroup.controls.ingredients.controls[index].controls;
switch (column) {
case 'amount':
return ingredientGroup.amount;
case 'name':
return ingredientGroup.name;
case 'notes':
return ingredientGroup.notes;
}
}
protected onIngredientKeydown(event: KeyboardEvent, index: number) {
if (event.key === 'Enter') {
event.preventDefault();
this.onIngredientEnterKey(index);
}
}
private onIngredientEnterKey(index: number) {
if (index === this.recipeFormGroup.controls.ingredients.length - 1) {
// last control row
this.addIngredient();
}
}
protected onSubmit(event: SubmitEvent): void {
event.preventDefault();
const value = this.recipeFormGroup.value;
this.submit.emit({
title: value.title!,
slug: value.slug!,
ingredients: value.ingredients?.map(ingredient => ({
amount: ingredient.amount ?? null,
name: ingredient.name!,
notes: ingredient.notes ?? null,
})) ?? [],
rawText: value.text!
})
}
protected onDraftDelete(): void {
this.deleteDraft.emit();
}
protected readonly faEllipsis = faEllipsis;
}