192 lines
6.1 KiB
TypeScript
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;
|
|
}
|