Updated on

Updating a resource from Angular is a round trip in two halves. We GET the owner by id, copy it into a reactive form with patchValue(), let the user edit it, and then PUT the whole object back to the same address.

The form itself is the one we built in the previous part. What is new here is loading it, addressing a single resource by id in the URL, and handling a response that has no body.

The server side of this is the update action we wrote in part 6, where the POST, PUT and DELETE actions are built. The Angular side starts where Angular Reactive Form Validation and POST Requests left off.

For the complete navigation and all the basic instructions of the Angular series, check out: Introduction of the Angular series.

To download the source code for this article, you can visit the PutRequests folder in our GitHub repository. The source code for the whole Angular series is here.
The server application this client calls is the one from our ASP.NET Core Web API series. Its code is in the UsingRepositoryForWriteRequests folder in our GitHub repository. We start it with the http launch profile, so it listens on http://localhost:5000, the address our environment file already points at.

How Do We Route to the Update Component?

Prior to any update action, we need to create our component files.

So, let’s create them with the Angular CLI command that writes the component, its template and its stylesheet under src/app/owner/owner-update:

ng g component owner/owner-update --skip-tests

There is no module to register the new component in. A standalone component names what it needs in its own imports array, so the CLI writes those three files and nothing else in the application changes.

Now, to establish the route to this component, we have to modify the routes array in the owner.routes.ts file:

export const routes: Routes = [
  { path: 'list', component: OwnerList },
  { path: 'details/:id', component: OwnerDetails },
  { path: 'create', component: OwnerCreate },
  { path: 'update/:id', component: OwnerUpdate }
];

The :id segment is what lets this route address one owner. The router puts the clicked owner’s id into the URL, and the component reads it back out of the activated route.

Now we are going to change our owner-list.html and the owner-list.ts files, to enable navigation between the OwnerList and the OwnerUpdate components:

<td><button type="button" id="update" class="btn btn-success"
(click)="redirectToUpdatePage(owner.id)">Update</button></td>

And the .ts file:

public redirectToUpdatePage = (id: string) => {
  const updateUrl: string = `/owner/update/${id}`;
  this.router.navigate([updateUrl]);
}

At this point, we have our routing defined, and we can move forward to handle the PUT request.

How Do We Pre-fill an Angular Update Form?

The update form is the create form with one difference: it opens already filled in.

The FormGroup, the three FormControl declarations, the validators and the formControlName bindings are all the ones we built in the previous part, unchanged. Rather than reprint them, we point at what actually differs.

The template changes in exactly one place. The submit binding becomes (ngSubmit)="updateOwner(ownerForm.value)" instead of calling the creation handler.

Filling the form is the component’s job, and it happens through patchValue(). Once the GET response arrives, we hand the whole owner object to the form and Angular copies each property into the control with the matching name.

patchValue() ignores keys that have no matching control, so an object carrying an id the form never shows causes no complaint. setValue() is the strict alternative, and it throws when the shapes do not match exactly.

Let’s add the wrappers code in the owner-update.html file:

<div class="container-fluid">
  <form [formGroup]="ownerForm" autocomplete="off" novalidate (ngSubmit)="updateOwner(ownerForm.value)">
    <div class="card card-body bg-light mb-2 mt-2">

    </div>
  </form>
</div>

We already know from the previous post that the formGroup is going to contain all of the controls inside its value. This value is exactly what we send as a parameter to the updateOwner action.

Everything that belongs between the card card-body div tags is markup we have already written: the three labelled controls with their formControlName attributes, the bsDatepicker input, the error rows and the Save and Cancel buttons. Take that block from the create form we built in the previous part, or copy the whole file out of the repository.

With the template in place, the rest of the work is in the component file, which is where the form gets filled and the request goes out.

How Do We Send a PUT Request From an Angular Component?

Sending the update takes three steps, once the form is valid.

First we build a plain object shaped the way the API expects. OwnerForUpdate carries a name, a date of birth and an address, and no identifier, because the identifier travels in the URL rather than in the body.

Then we build that URL from the id we loaded the owner with. PUT addresses one specific resource, so the id belongs in the path and nowhere else.

Then we call the repository and subscribe. The success branch takes no parameter at all, because our API answers a successful PUT with 204 No Content and there is no body to read. That is why the code reads next: () => rather than naming a response object, which is the one line here that surprises people coming from the create form.

Failures need no branch here: the interceptor we registered earlier sees every one of them.

The two arrows worth comparing below are steps 3 and 7: the GET response carries the whole owner, and the PUT response carries nothing at all.

Sequence of the update round trip: GET the owner, patchValue into the form, PUT the full object, receive 204 No Content.

Let’s start with all the imports inside the owner-update.ts file:

import { DatePipe } from '@angular/common';
import { Component, OnInit, inject, signal } from '@angular/core';
import { FormControl, FormGroup, ReactiveFormsModule, Validators } from '@angular/forms';
import { ActivatedRoute, Router } from '@angular/router';
import { BsDatepickerDirective, BsDatepickerInputDirective } from 'ngx-bootstrap/datepicker';
import { BsModalRef, BsModalService, ModalOptions } from 'ngx-bootstrap/modal';

import { Owner } from '../../_interfaces/owner.model';
import { OwnerForUpdate } from '../../_interfaces/ownerForUpdate.model';
import { OwnerRepositoryService } from '../../shared/services/owner-repository.service';
import { SuccessModal } from '../../shared/modals/success-modal/success-modal';

Then the component’s own metadata, which is where a standalone component declares what its template uses:

@Component({
  imports: [ReactiveFormsModule, BsDatepickerDirective, BsDatepickerInputDirective],
  providers: [DatePipe],
  selector: 'app-owner-update',
  styleUrl: './owner-update.css',
  templateUrl: './owner-update.html',
})

The datepicker needs two directives, not one. BsDatepickerDirective opens the calendar on the date input, and BsDatepickerInputDirective is the one that writes the picked value into the dateOfBirth control. Leave the second one out and the control is never written, which on this page would look like patchValue() failing rather than like a missing import.

DatePipe is provided on the component instead of application-wide, because only this component and the create component from the previous part ever inject it.

Next, let’s add the required properties and inject the required services:

owner = signal<Owner | undefined>(undefined);
ownerForm!: FormGroup;
bsModalRef?: BsModalRef;

private repository = inject(OwnerRepositoryService);
private router = inject(Router);
private activeRoute = inject(ActivatedRoute);
private datePipe = inject(DatePipe);
private modal = inject(BsModalService);

The owner we load lives in a signal, the shape part 10 introduced, and it starts as undefined because the component holds nothing until the response arrives.

To continue, we have to modify the ngOnInit function and add one additional function:

ngOnInit(): void {
  this.ownerForm = new FormGroup({
    name: new FormControl('', [Validators.required, Validators.maxLength(60)]),
    dateOfBirth: new FormControl('', [Validators.required]),
    address: new FormControl('', [Validators.required, Validators.maxLength(100)])
  });

  this.getOwnerById();
}

private getOwnerById = () => {
  const ownerId: string = this.activeRoute.snapshot.params['id'];
  const ownerByIdUri: string = `api/owner/${ownerId}`;

  this.repository.getOwner(ownerByIdUri)
  .subscribe({
    next: (own: Owner) => {
      const owner: Owner = { ...own,
        dateOfBirth: new Date(own.dateOfBirth)
      };

      this.owner.set(owner);
      this.ownerForm.patchValue(owner);
    }
  })
}

In the ngOnInit function, we instantiate the ownerForm with all the form controls and add the validation rules. Then we call the getOwnerById function to fetch the owner with the exact id from the server.

Inside that function, we pull the id out of the activated route, build the API URI from it, and send the GET request. The response goes into a local owner constant, which then feeds two things: set, which stores it in the signal for the PUT to read later, and patchValue(), which puts its values on the screen.

One thing to pay attention to is the dateOfBirth value. The API sends a date as a string and the datepicker input expects a Date, so we build one with new Date(own.dateOfBirth). The string is ISO 8601, which the Date constructor reads the same way whatever the locale.

The validateControl and hasError helpers that drive the error messages are the ones from the validation section of the previous part, unchanged, so we can copy both of them straight across.

Now, before we send the PUT request, we are going to add one more interface:

export interface OwnerForUpdate {
  name: string;
  dateOfBirth: string;
  address: string;
}

It has the same three fields as OwnerForCreation, and it is a separate interface on purpose. The two happen to agree today, but they answer to different actions on the server, and either one can gain a field without dragging the other along.

And also modify the updateOwner function inside the owner-repository.service file, so it takes the new type:

  public updateOwner(route: string, owner: OwnerForUpdate) {
    return this.http.put(this.createCompleteRoute(route, this.envUrl.urlAddress), owner);
  }

Finally, we are going to return to our component file, and execute the update action:

public updateOwner = (ownerFormValue: any) => {
  if (this.ownerForm.valid)
    this.executeOwnerUpdate(ownerFormValue);
}

private executeOwnerUpdate = (ownerFormValue: any) => {
  const ownerForUpd: OwnerForUpdate = {
    name: ownerFormValue.name,
    dateOfBirth: this.datePipe.transform(ownerFormValue.dateOfBirth, 'yyyy-MM-dd') ?? '',
    address: ownerFormValue.address
  }

  const apiUri: string = `api/owner/${this.owner()?.id}`;

  this.repository.updateOwner(apiUri, ownerForUpd)
  .subscribe({
    next: () => {
      const config: ModalOptions = {
        initialState: {
          modalHeaderText: 'Success Message',
          modalBodyText: 'Owner updated successfully',
          okButtonText: 'OK'
        }
      };

      this.bsModalRef = this.modal.show(SuccessModal, config);
      this.bsModalRef.content?.redirectOnOk.subscribe(() => this.redirectToOwnerList());
    }
  })
}

This is pretty much the same logic as the createOwner function, with two differences. The URL carries the id, read back out of the signal with this.owner()?.id, and the next callback declares no parameter, because the server sends a 204 and there is nothing to read.

The outgoing date goes the other way through DatePipe: the form holds a Date, and transform(..., 'yyyy-MM-dd') turns it into the string the API’s model binder accepts.

Nothing in this component reacts to a failed request. The error interceptor from Angular Error Handling: HTTP Errors and Error Pages catches every one of them, sends a 404 or a 500 to its error page, and shows the error modal for anything else.

Of course, we also need that redirectToOwnerList function:

public redirectToOwnerList = () => {
  this.router.navigate(['/owner/list']);
}

Now we can try it. With the API running, open the owner list, click Update on a row, change the name and save. The request comes back 204 No Content, the modal reads “Owner updated successfully”, and clicking OK sends us back to the list, which fetches the owners again and shows the new value.

When Should We Use PUT Instead of PATCH?

PUT replaces a resource. PATCH changes part of one.

A PUT body is the complete new state of the resource at that address. Whatever the body leaves out, the server is entitled to treat as absent, so sending only a changed name can blank the other fields. Our form submits all three properties on every save, which is why PUT is the honest verb here.

A PATCH body describes a change instead: which fields to set, and to what. It suits a screen that edits one field on its own, and it costs less over the wire on a large resource.

PUT is idempotent. Sending the same request twice leaves the resource in the state one request would have left it in, so a client retrying after a timeout cannot do harm. POST is not idempotent, which is why creating twice makes two owners.

MethodWhat the request body meansIdempotentTypical success responseReach for it when
POSTData for the server to process however it choosesNo201 Created with a Location headerThe server decides the new resource's URL
PUTThe complete new state of the resource at this URLYes204 No Content, or 200 OK with the resourceThe client already knows the resource's URL
PATCHA description of the changes to applyNot required to be204 No Content, or 200 OK with the resourceOnly part of the resource is changing

Our API has no PATCH action, so there is nothing to call here yet. If you want to add one, how PATCH requests are built covers the document format and the server side of it.

A .NET application sends the same requests, and sending the same requests from a .NET client shows what changes when the caller is HttpClient rather than a browser.

Conclusion

In this article, we have learned how to pre-fill a reactive form with patchValue(), how to address a single owner by putting the id in the URL, and how to send a PUT request whose response carries no body at all. We have also seen when PUT is the right verb and when a PATCH would fit the screen better.

In the next part of the series, Angular DELETE Request to an ASP.NET Core Web API, we are going to write the delete part of the project, and slowly wrap the coding part of the series up.

Tested with Angular 22.1.5, Angular CLI 22.1.7, ngx-bootstrap 22.0.0, Bootstrap 5.3.8, TypeScript 6.0.3 and Node 24.