feature: fetch, edit and add address

- fetch existing addresses from api,
- user can edit existing address
- user can add new address
This commit is contained in:
kusowl 2026-03-17 10:58:22 +05:30
parent 3059a923b4
commit 24bdfe9cc6
9 changed files with 203 additions and 32 deletions

View File

@ -3,10 +3,10 @@
<app-go-back route="/" text="Home" />
<div class="grid grid-cols-3 gap-x-10">
<div class="col-span-2 flex flex-col space-y-4">
<app-address-select />
<app-address-select />
<app-address-select />
<app-address-form />
@for (address of addresses(); track address.id) {
<app-address-select [address]="address" (addressUpdated)="updateAddress($event)" />
}
<app-address-form (submitAddress)="createNewAddress($event)" />
</div>
<div>
<app-order-summery />

View File

@ -1,9 +1,16 @@
import { Component, inject, OnInit } from "@angular/core";
import { Component, inject, OnInit, signal } from "@angular/core";
import { AddressForm } from "../components/address-form/address-form";
import { GoBack } from "@app/shared/components/go-back/go-back";
import { AddressSelect } from "../components/address-select/address-select";
import { OrderSummery } from "../components/order-summery/order-summery";
import { AddressService } from "@app/features/checkout/services/address-service";
import {
AddressRequest,
AddressResponse,
AddressService,
} from "@app/features/checkout/services/address-service";
import { AuthService } from "@core/services/auth-service";
import { User } from "@core/models/user.model";
import { switchMap } from "rxjs";
@Component({
selector: "app-address",
@ -13,6 +20,41 @@ import { AddressService } from "@app/features/checkout/services/address-service"
})
export class Address implements OnInit {
addressService = inject(AddressService);
authService = inject(AuthService);
private user: User | undefined;
protected addresses = signal<AddressResponse[]>([]);
ngOnInit() {}
ngOnInit(): void {
this.authService
.getCurrentUser()
.pipe(
switchMap((user) => {
this.user = user;
if (user?.id) {
return this.addressService.fetchAddresses(user.id);
}
return [];
}),
)
.subscribe({
next: (addresses) => {
this.addresses.set(addresses.data);
},
});
}
createNewAddress(addressData: AddressRequest) {
this.addressService.createAddress(this.user!.id, addressData).subscribe({
next: (address) => this.addresses.update((addresses) => [...addresses, address]),
});
}
updateAddress(addressData: AddressResponse) {
console.log(addressData);
this.addressService.updateAddress(addressData.id, addressData).subscribe({
next: (address) =>
this.addresses.update((addresses) =>
addresses.map((a) => (a.id === address.id ? address : a)),
),
});
}
}

View File

@ -1,8 +1,14 @@
<details class="card p-0!" open>
<summary class="p-4">
<label for="currentAddress" class="font-medium text-gray-600 ml-2">Add new address</label>
<details class="card p-0!" title="Click to add a new address">
<summary class="p-6">
<label for="currentAddress" class="font-medium text-gray-600 ml-2"
>{{isEditing() ? 'Update address' : 'Add new address'}}</label
>
</summary>
<form [formGroup]="addressForm" class="w-full flex flex-col gap-y-2 pt-0 p-4">
<form
[formGroup]="addressForm"
(ngSubmit)="submitForm()"
class="w-full flex flex-col gap-y-2 pt-0 p-4"
>
<fieldset class="flex space-x-4 w-full">
<fieldset class="fieldset w-full">
<legend class="fieldset-legend">First Name</legend>
@ -17,13 +23,8 @@
</fieldset>
<fieldset class="fieldset w-full">
<legend class="fieldset-legend">Street Address</legend>
<input
type="text"
class="input"
formControlName="streetAddress"
placeholder="Your street address"
/>
<app-error fieldName="Street address" [control]="addressForm.get('streetAddress')" />
<input type="text" class="input" formControlName="street" placeholder="Your street address" />
<app-error fieldName="Street address" [control]="addressForm.get('street')" />
</fieldset>
<fieldset class="flex space-x-4 w-full">
<fieldset class="fieldset w-full">
@ -43,8 +44,12 @@
</fieldset>
</fieldset>
<div class="ml-auto flex space-x-4">
<button type="button" class="btn btn-ghost px-3 text-sm">Cancel</button>
<button class="btn btn-primary px-3 text-sm">Use this address</button>
<button type="button" (click)="cancelEditing()" class="btn btn-ghost px-3 text-sm">
Cancel
</button>
<button class="btn btn-primary px-3 text-sm">
{{isEditing() ? 'Update this address' : 'Use this address'}}
</button>
</div>
</form>
</details>

View File

@ -1,6 +1,7 @@
import { Component } from "@angular/core";
import { Component, EventEmitter, Input, Output, signal } from "@angular/core";
import { FormControl, FormGroup, ReactiveFormsModule, Validators } from "@angular/forms";
import { Error } from "@app/shared/components/error/error";
import { AddressRequest, AddressResponse } from "@app/features/checkout/services/address-service";
@Component({
selector: "app-address-form",
@ -9,12 +10,55 @@ import { Error } from "@app/shared/components/error/error";
styleUrl: "./address-form.css",
})
export class AddressForm {
@Input() set initialData(address: AddressResponse) {
if (address) {
this.addressForm.patchValue(address);
this.address.set(address);
this.isEditing.set(true);
}
}
@Output() submitAddress: EventEmitter<AddressRequest> = new EventEmitter<AddressRequest>();
@Output() updateAddress: EventEmitter<AddressResponse> = new EventEmitter<AddressResponse>();
@Output() editingCanceled: EventEmitter<void> = new EventEmitter<void>();
protected isEditing = signal(false);
protected address = signal<AddressResponse | null>(null);
addressForm = new FormGroup({
firstName: new FormControl("", { validators: Validators.required }),
lastName: new FormControl("", { validators: Validators.required }),
streetAddress: new FormControl("", { validators: Validators.required }),
firstName: new FormControl("", {
validators: [Validators.required, Validators.pattern("^[a-zA-Z]\\S+$")],
}),
lastName: new FormControl("", {
validators: [Validators.required, Validators.pattern("^[a-zA-Z]\\S+$")],
}),
street: new FormControl("", { validators: Validators.required }),
city: new FormControl("", { validators: Validators.required }),
state: new FormControl("", { validators: Validators.required }),
pinCode: new FormControl("", { validators: Validators.required }),
pinCode: new FormControl("", {
validators: [Validators.required, Validators.pattern("^[0-9]{6}$")],
}),
});
submitForm() {
if (this.addressForm.invalid) {
this.addressForm.markAllAsTouched();
return;
}
const emittedData = this.addressForm.getRawValue() as AddressRequest;
this.addressForm.reset();
if (this.isEditing()) {
const mergedData = { ...this.address(), ...emittedData };
console.log(mergedData);
this.updateAddress.emit(mergedData as unknown as AddressResponse);
} else {
this.submitAddress.emit(emittedData);
}
}
cancelEditing() {
this.addressForm.reset();
this.editingCanceled.emit();
}
}

View File

@ -1,10 +1,20 @@
@if (!isEditing()) {
<div class="flex justify-between card">
<div class="flex space-x-4 items-center">
<input type="radio" name="address" />
<p class="text-gray-600 font-medium">Kushal Saha</p>
<p class="text-gray-400 text-sm">48 St, Park Avenue, New Towm, 700021</p>
<p class="text-gray-600 font-medium">{{[address.firstName, address.lastName] | fullname}}</p>
<p class="text-gray-400 text-sm">
{{`${address.street}, ${address.city}, ${address.pinCode}`}}
</p>
</div>
<div>
<button class="btn btn-ghost text-sm px-2">Edit</button>
<button (click)="editForm()" class="btn btn-ghost text-sm px-2">Edit</button>
</div>
</div>
} @else{
<app-address-form
[initialData]="address"
(editingCanceled)="cancelEditing()"
(updateAddress)="updateAddress($event)"
/>
}

View File

@ -1,9 +1,30 @@
import { Component } from "@angular/core";
import { Component, EventEmitter, Input, Output, signal } from "@angular/core";
import { AddressResponse } from "@app/features/checkout/services/address-service";
import { FullnamePipe } from "@shared/pipes/fullname-pipe";
import { AddressForm } from "@app/features/checkout/components/address-form/address-form";
@Component({
selector: "app-address-select",
imports: [],
imports: [FullnamePipe, AddressForm],
templateUrl: "./address-select.html",
styleUrl: "./address-select.css",
})
export class AddressSelect {}
export class AddressSelect {
@Input() address!: AddressResponse;
@Output() addressUpdated: EventEmitter<AddressResponse> = new EventEmitter<AddressResponse>();
protected isEditing = signal(false);
editForm() {
this.isEditing.set(true);
}
cancelEditing() {
this.isEditing.set(false);
}
updateAddress(address: AddressResponse) {
this.isEditing.set(false);
this.addressUpdated.emit(address);
}
}

View File

@ -1,6 +1,20 @@
import { inject, Injectable } from "@angular/core";
import { HttpClient } from "@angular/common/http";
import { API_URL } from "@core/tokens/api-url-tokens";
import { PaginatedResponse } from "@core/models/paginated.model";
export interface AddressRequest {
firstName: string;
lastName: string;
street: string;
city: string;
state: string;
pinCode: string;
}
export interface AddressResponse extends AddressRequest {
id: number;
}
@Injectable({
providedIn: "root",
@ -10,6 +24,20 @@ export class AddressService {
apiUrl = inject(API_URL);
fetchAddresses(userId: number) {
return this.http.get(`${this.apiUrl}/user/${userId}/addresses`);
return this.http.get<PaginatedResponse<AddressResponse>>(
`${this.apiUrl}/user/${userId}/addresses`,
);
}
createAddress(userId: number, data: AddressRequest) {
return this.http.post<AddressResponse>(`${this.apiUrl}/user/${userId}/addresses`, data);
}
updateAddress(addressId: number, data: AddressRequest) {
return this.http.patch<AddressResponse>(`${this.apiUrl}/addresses/${addressId}`, data);
}
deleteAddress(userId: number, addressId: number) {
return this.http.delete<AddressResponse>(
`${this.apiUrl}/user/${userId}/addresses/${addressId}`,
);
}
}

View File

@ -0,0 +1,8 @@
import { FullnamePipe } from "./fullname-pipe";
describe("FullnamePipe", () => {
it("create an instance", () => {
const pipe = new FullnamePipe();
expect(pipe).toBeTruthy();
});
});

View File

@ -0,0 +1,13 @@
import { Pipe, PipeTransform } from "@angular/core";
import { TitleCasePipe } from "@angular/common";
@Pipe({
name: "fullname",
})
export class FullnamePipe implements PipeTransform {
titlecase = new TitleCasePipe();
transform(values: string[]): unknown {
return this.titlecase.transform(values.join(" "));
}
}