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" /> <app-go-back route="/" text="Home" />
<div class="grid grid-cols-3 gap-x-10"> <div class="grid grid-cols-3 gap-x-10">
<div class="col-span-2 flex flex-col space-y-4"> <div class="col-span-2 flex flex-col space-y-4">
<app-address-select /> @for (address of addresses(); track address.id) {
<app-address-select /> <app-address-select [address]="address" (addressUpdated)="updateAddress($event)" />
<app-address-select /> }
<app-address-form /> <app-address-form (submitAddress)="createNewAddress($event)" />
</div> </div>
<div> <div>
<app-order-summery /> <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 { AddressForm } from "../components/address-form/address-form";
import { GoBack } from "@app/shared/components/go-back/go-back"; import { GoBack } from "@app/shared/components/go-back/go-back";
import { AddressSelect } from "../components/address-select/address-select"; import { AddressSelect } from "../components/address-select/address-select";
import { OrderSummery } from "../components/order-summery/order-summery"; 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({ @Component({
selector: "app-address", selector: "app-address",
@ -13,6 +20,41 @@ import { AddressService } from "@app/features/checkout/services/address-service"
}) })
export class Address implements OnInit { export class Address implements OnInit {
addressService = inject(AddressService); 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> <details class="card p-0!" title="Click to add a new address">
<summary class="p-4"> <summary class="p-6">
<label for="currentAddress" class="font-medium text-gray-600 ml-2">Add new address</label> <label for="currentAddress" class="font-medium text-gray-600 ml-2"
>{{isEditing() ? 'Update address' : 'Add new address'}}</label
>
</summary> </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="flex space-x-4 w-full">
<fieldset class="fieldset w-full"> <fieldset class="fieldset w-full">
<legend class="fieldset-legend">First Name</legend> <legend class="fieldset-legend">First Name</legend>
@ -17,13 +23,8 @@
</fieldset> </fieldset>
<fieldset class="fieldset w-full"> <fieldset class="fieldset w-full">
<legend class="fieldset-legend">Street Address</legend> <legend class="fieldset-legend">Street Address</legend>
<input <input type="text" class="input" formControlName="street" placeholder="Your street address" />
type="text" <app-error fieldName="Street address" [control]="addressForm.get('street')" />
class="input"
formControlName="streetAddress"
placeholder="Your street address"
/>
<app-error fieldName="Street address" [control]="addressForm.get('streetAddress')" />
</fieldset> </fieldset>
<fieldset class="flex space-x-4 w-full"> <fieldset class="flex space-x-4 w-full">
<fieldset class="fieldset w-full"> <fieldset class="fieldset w-full">
@ -43,8 +44,12 @@
</fieldset> </fieldset>
</fieldset> </fieldset>
<div class="ml-auto flex space-x-4"> <div class="ml-auto flex space-x-4">
<button type="button" class="btn btn-ghost px-3 text-sm">Cancel</button> <button type="button" (click)="cancelEditing()" class="btn btn-ghost px-3 text-sm">
<button class="btn btn-primary px-3 text-sm">Use this address</button> Cancel
</button>
<button class="btn btn-primary px-3 text-sm">
{{isEditing() ? 'Update this address' : 'Use this address'}}
</button>
</div> </div>
</form> </form>
</details> </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 { FormControl, FormGroup, ReactiveFormsModule, Validators } from "@angular/forms";
import { Error } from "@app/shared/components/error/error"; import { Error } from "@app/shared/components/error/error";
import { AddressRequest, AddressResponse } from "@app/features/checkout/services/address-service";
@Component({ @Component({
selector: "app-address-form", selector: "app-address-form",
@ -9,12 +10,55 @@ import { Error } from "@app/shared/components/error/error";
styleUrl: "./address-form.css", styleUrl: "./address-form.css",
}) })
export class AddressForm { 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({ addressForm = new FormGroup({
firstName: new FormControl("", { validators: Validators.required }), firstName: new FormControl("", {
lastName: new FormControl("", { validators: Validators.required }), validators: [Validators.required, Validators.pattern("^[a-zA-Z]\\S+$")],
streetAddress: new FormControl("", { validators: Validators.required }), }),
lastName: new FormControl("", {
validators: [Validators.required, Validators.pattern("^[a-zA-Z]\\S+$")],
}),
street: new FormControl("", { validators: Validators.required }),
city: new FormControl("", { validators: Validators.required }), city: new FormControl("", { validators: Validators.required }),
state: 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 justify-between card">
<div class="flex space-x-4 items-center"> <div class="flex space-x-4 items-center">
<input type="radio" name="address" /> <input type="radio" name="address" />
<p class="text-gray-600 font-medium">Kushal Saha</p> <p class="text-gray-600 font-medium">{{[address.firstName, address.lastName] | fullname}}</p>
<p class="text-gray-400 text-sm">48 St, Park Avenue, New Towm, 700021</p> <p class="text-gray-400 text-sm">
{{`${address.street}, ${address.city}, ${address.pinCode}`}}
</p>
</div> </div>
<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>
</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({ @Component({
selector: "app-address-select", selector: "app-address-select",
imports: [], imports: [FullnamePipe, AddressForm],
templateUrl: "./address-select.html", templateUrl: "./address-select.html",
styleUrl: "./address-select.css", 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 { inject, Injectable } from "@angular/core";
import { HttpClient } from "@angular/common/http"; import { HttpClient } from "@angular/common/http";
import { API_URL } from "@core/tokens/api-url-tokens"; 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({ @Injectable({
providedIn: "root", providedIn: "root",
@ -10,6 +24,20 @@ export class AddressService {
apiUrl = inject(API_URL); apiUrl = inject(API_URL);
fetchAddresses(userId: number) { 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(" "));
}
}