Feature: Create Event - First Step Frontend #17

Merged
igorpropisnov merged 20 commits from feature/create-event into main 2024-08-22 14:58:36 +02:00
6 changed files with 151 additions and 21 deletions
Showing only changes of commit 4a69d916c6 - Show all commits

View File

@ -21,7 +21,7 @@
<div class="flex-grow overflow-y-auto px-4 sm:px-8 py-8">
<div class="w-full max-w-4xl mx-auto">
@if (currentStep() === 0) {
<app-basic-step></app-basic-step>
<app-basic-step [form]="form"></app-basic-step>
}
@if (currentStep() === 1) {
<app-tickets-step></app-tickets-step>

View File

@ -4,8 +4,8 @@ import {
ChangeDetectionStrategy,
signal,
WritableSignal,
OnInit,
} from '@angular/core';
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
import { BasicStepComponent } from './steps/basic-step.component';
import { TicketsStepComponent } from './steps/tickets-step.component';
@ -18,13 +18,19 @@ import { TicketsStepComponent } from './steps/tickets-step.component';
templateUrl: './create-event.component.html',
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class CreateEventComponent implements OnInit {
export class CreateEventComponent {
public currentStep: WritableSignal<number> = signal(0);
public readonly steps: string[] = ['Basic', 'Tickets', 'Review'];
public form!: FormGroup;
public constructor() {}
public ngOnInit(): void {}
public constructor(private readonly formBuilder: FormBuilder) {
this.form = this.formBuilder.group({
eventTitle: ['', [Validators.required, Validators.minLength(3)]],
eventLocation: ['', Validators.required],
eventDate: ['', Validators.required],
eventTime: ['', Validators.required],
});
}
public getStepContent(index: number): string {
if (index < this.currentStep()) {

View File

@ -1,4 +1,4 @@
<form class="flex flex-col">
<form [formGroup]="form()" class="flex flex-col">
<h1 class="text-2xl font-bold mb-6">Event Basic Information</h1>
<label class="form-control w-full mb-4">
@ -7,6 +7,8 @@
<span class="label-text-alt">Make it catchy and descriptive</span>
</div>
<input
formControlName="eventTitle"
[ngClass]="getInputClass('eventTitle')"
type="text"
placeholder="Enter your event title"
class="input input-bordered w-full" />
@ -18,6 +20,7 @@
<div class="form-control w-full">
<app-dropdown
formControlName="eventLocation"
label="Event Location"
placeholder="Search for a Location"
hintTopRight="Select or create a new Location by using the dropdown"
@ -34,7 +37,12 @@
<span class="label-text">Date</span>
<span class="label-text-alt">Select the event date</span>
</div>
<input type="date" id="event-date" class="input input-bordered w-full" />
<input
formControlName="eventDate"
[ngClass]="getInputClass('eventDate')"
type="date"
id="event-date"
class="input input-bordered w-full" />
<div class="label">
<span class="label-text-alt"></span>
<span class="label-text-alt"></span>
@ -46,12 +54,21 @@
<span class="label-text">Time</span>
<span class="label-text-alt">Choose the event time</span>
</div>
<input type="time" id="event-time" class="input input-bordered w-full" />
<input
formControlName="eventTime"
[ngClass]="getInputClass('eventTime')"
type="time"
id="event-time"
class="input input-bordered w-full" />
<div class="label">
<span class="label-text-alt"></span>
<span class="label-text-alt"></span>
</div>
</label>
<div>
<pre>{{ form().value | json }}</pre>
</div>
</div>
</form>

View File

@ -1,6 +1,12 @@
import { CommonModule } from '@angular/common';
import { ChangeDetectionStrategy, Component, ViewChild } from '@angular/core';
import { FormsModule } from '@angular/forms';
import {
ChangeDetectionStrategy,
Component,
InputSignal,
ViewChild,
input,
} from '@angular/core';
import { FormGroup, FormsModule, ReactiveFormsModule } from '@angular/forms';
import { DropdownComponent } from '../../../../shared/components/dropdown/dropdown.component';
import {
@ -18,6 +24,7 @@ import {
FormsModule,
DropdownComponent,
LocationDialogComponent,
ReactiveFormsModule,
],
changeDetection: ChangeDetectionStrategy.OnPush,
})
@ -25,6 +32,7 @@ export class BasicStepComponent {
@ViewChild(LocationDialogComponent)
public locationModal!: LocationDialogComponent;
public items: string[] = ['Nachtigal Köln'];
public form: InputSignal<FormGroup> = input.required<FormGroup>();
public constructor() {}
@ -32,8 +40,17 @@ export class BasicStepComponent {
this.openLocationModal();
}
public getInputClass(controlName: string): string {
const control = this.form().get(controlName);
if (control?.touched) {
return control.valid ? 'input-success' : 'input-error';
}
return '';
}
public onLocationSubmit(location: EventLocation): void {
console.log(location);
//TODO: save location
}
private openLocationModal(): void {

View File

@ -1,4 +1,4 @@
<div class="form-control w-full mb-4">
<label class="form-control w-full mb-4">
<div class="label">
<span class="label-text">{{ label() }}</span>
<span class="label-text-alt">
@ -6,7 +6,9 @@
</span>
</div>
<div class="relative">
<label class="input input-bordered flex items-center gap-2">
<label
[ngClass]="getInputClass()"
class="input input-bordered flex items-center gap-2">
<input
type="text"
[placeholder]="placeholder()"
@ -14,6 +16,7 @@
(focus)="onFocus()"
(blur)="onBlur()"
(input)="onInput($event)"
(keyup.backspace)="onInputClear()"
[(ngModel)]="searchTerm" />
<svg
xmlns="http://www.w3.org/2000/svg"
@ -112,4 +115,4 @@
<span class="label-text-alt">{{ hintBottomLeft() }}</span>
<span class="label-text-alt">{{ hintBottomRight() }}</span>
</div>
</div>
</label>

View File

@ -1,6 +1,7 @@
import { CommonModule } from '@angular/common';
import {
Component,
forwardRef,
input,
InputSignal,
output,
@ -8,15 +9,35 @@ import {
signal,
WritableSignal,
} from '@angular/core';
import { FormsModule } from '@angular/forms';
import {
AbstractControl,
ControlValueAccessor,
FormsModule,
NG_VALIDATORS,
NG_VALUE_ACCESSOR,
ValidationErrors,
Validator,
} from '@angular/forms';
@Component({
selector: 'app-dropdown',
templateUrl: './dropdown.component.html',
standalone: true,
imports: [CommonModule, FormsModule],
providers: [
{
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => DropdownComponent),
multi: true,
},
{
provide: NG_VALIDATORS,
useExisting: forwardRef(() => DropdownComponent),
multi: true,
},
],
})
export class DropdownComponent {
export class DropdownComponent implements ControlValueAccessor, Validator {
public label: InputSignal<string> = input.required<string>();
public placeholder: InputSignal<string> = input.required<string>();
public items: InputSignal<string[]> = input.required<string[]>();
@ -37,6 +58,36 @@ export class DropdownComponent {
public submitNewItems: OutputEmitterRef<boolean> = output<boolean>();
public itemEdit: OutputEmitterRef<string> = output<string>();
public writeValue(value: string | null): void {
if (value && this.isValidOption(value)) {
this.searchTerm.set(value);
this.selectedItem.set(value);
} else {
this.searchTerm.set('');
this.selectedItem.set(null);
}
}
public registerOnChange(fn: (value: string) => void): void {
this.onChange = fn;
}
public registerOnTouched(fn: () => void): void {
this.onTouched = fn;
}
public validate(control: AbstractControl): ValidationErrors | null {
const value = control.value;
if (this.required() && (!value || value.trim().length === 0)) {
return { required: true };
}
if (value && value.trim().length > 0 && !this.isValidOption(value)) {
return { invalidOption: true };
}
return null;
}
public onInput(event: Event): void {
const value = (event.target as HTMLInputElement).value;
@ -54,13 +105,30 @@ export class DropdownComponent {
if (exactMatch) {
this.selectedItem.set(exactMatch);
this.onChange(exactMatch);
this.itemSelected.emit(exactMatch);
} else {
this.selectedItem.set(null);
this.onChange('');
this.itemSelected.emit('');
}
this.showDropdown.set(true);
this.onTouched();
}
public getInputClass(): string {
if (this.selectedItem()) {
return 'input-success';
} else if (
this.searchTerm().trim() !== '' &&
!this.isValidOption(this.searchTerm())
) {
return 'input-error';
} else if (this.required() && this.searchTerm().trim() === '') {
return 'input-error';
}
return '';
}
public editItem(item: string, event: MouseEvent): void {
@ -71,16 +139,27 @@ export class DropdownComponent {
}
public selectItem(item: string): void {
this.searchTerm.set(item);
this.selectedItem.set(item);
this.showDropdown.set(false);
this.itemSelected.emit(item);
if (this.isValidOption(item)) {
this.searchTerm.set(item);
this.selectedItem.set(item);
this.showDropdown.set(false);
this.itemSelected.emit(item);
this.onChange(item);
this.onTouched();
}
}
public submitNewItem(): void {
this.submitNewItems.emit(true);
}
public onInputClear(): void {
this.searchTerm.set('');
this.selectedItem.set(null);
this.onChange('');
this.onTouched();
}
public onFocus(): void {
this.showDropdown.set(true);
this.filteredItems.set(this.items());
@ -89,6 +168,14 @@ export class DropdownComponent {
public onBlur(): void {
setTimeout(() => {
this.showDropdown.set(false);
this.onTouched();
}, 100);
}
private isValidOption(value: string): boolean {
return this.items().includes(value);
}
private onChange: (value: string) => void = () => {};
private onTouched: () => void = () => {};
}