Editing a Single Photo

Now that the user can search and save photos, it’s time to add the final major component: editing a saved photo. The first requirement is simple—get a single photo from the API. While it’s possible to reuse the getSavedPhotos method and just filter down to the requested ID, it’s more elegant to have a function just for this purpose, and adding a method won’t complicate things too much. Add a getSinglePhoto method to PhotosService:

 getSinglePhoto(photoId) {
 return​ ​this​.http.​get​<IPhoto>(​this​.api + ​'/getSinglePhoto/'​ + photoId);
 }

Next, you need to tap into that new method. Open up edit-photo.component.ts and add the following (import as needed):

 constructor​(
private​ currentRoute: ActivatedRoute,
private​ photosService: PhotosService
 ) {}

Injecting an ActivatedRoute results in an object that represents the current route. We’ll use this to grab information about the route itself.

This is the photo service created earlier. In this case, we’ll use it to fetch a single photo, rather than make a search.

Once everything’s injected, it’s time to figure out what photo the user wants to edit. The currentRoute object has a property paramMap—an observable that returns the latest details about the route parameters we defined in the route definition. Since this is an observable, rather than a fixed set of properties, Angular can handle changes to the route parameters without reloading the entire component. The application runs faster and our users are happy. Everyone wins!

Whenever the photoId in the route params changes, we want to load details about that photo. If the params change before the previous details have finished loading, Rx should ditch the previous request and only focus on the current one. This is the job of switchMap, first discussed in Chapter 4, Advanced Async. You’ll need to import switchMap and ParamMap, as well as declare the photo$ property on the EditPhotoComponent class.

 ngOnInit() {
 this​.photo$ = ​this​.currentRoute.paramMap
  .pipe(
  switchMap((params: ParamMap) =>
 this​.photosService.getSinglePhoto(params.​get​(​'photoId'​))
  )
  );
 }

None of this triggers until the UI actually cares about the results. To handle that, we’ll unwrap the observable with the async pipe. However, quite a few elements on this page will need the information that’s passed through photo$. If each element adds an async pipe, that means a new subscription (and therefore, a new request) each time. Fortunately, Angular provides a solution to that problem: the *ngIf directive.

Using ngIf

Sometimes, we want to hold off on rendering a section of a page until something has finished loading. In this case, the entire page depends on the photo details being available. ngIf is a directive that can be attached to an element that only inserts that element (and all of its children) into the DOM after the condition passed to it evaluates to true. It will remove the element if the condition ever becomes false again (and so on). In our case, the condition starts at false and only moves to true once.

Add an *ngIf to the page that encompasses everything that wants to use the latest photo data. This prevents the page from trying to render before it’s ready. Using as aliases the result of the photo$ observable, making it available as a variable to every child element without requiring that each element use the async pipe. The square brackets around the src attribute tell Angular to take whatever value is passed in (in this case, the URL of the photo) and set it as the src attribute:

 <div ​*​ngIf=​"photo$ | async as photo"​>
  <div class=​"row"​>
  <div class=​"col-xs-2 col-xs-offset-5"​>
  <img class=​"photo-detail img-rounded"​ ​[​src​]="​photo​.​url​"​>
  </div>
  </div>
 </div>

Now that the page loads the photo details correctly, it’s time to add interactivity with a simplified tag manager.

Tagging and Saving Photos

Time to add some interactivity to the page. Using as to alias photo only scopes that variable to the view. Not to worry, this is a standard pattern in Angular: load something page-wide with as, then pass that into methods on the component whenever you want to do something with that value.

Speaking of methods, it’s time to add tagging to the edit page. In the view, a few div elements are using Bootstrap classes to keep things centered and looking nice. The key attribute here is [(ngModel)]="tagInput". The ngModel attribute is a special attribute provided by Angular that matches the value of an element on the page with the value contained by a property of the component. Much like the src attribute from before, the square brackets indicate that whatever value is contained by the tagInput property of our component is also bound to the element itself. The parens work in the opposite direction—as the value of the input element changes, so does the component’s value. Clicking the “Add Tag” button at the end calls the addTag method on the component (which you’ll add right after this). The value of photo is passed through.

 <div class=​"row"​>
  <div class=​"col-xs-4 col-xs-offset-4"​>
  <div class=​"input-group"​>
  <input ​[(​ngModel​)]="​tagInput​"​ type=​"text"​ class=​"form-control"
  placeholder=​"Input Tag"​>
  <span class=​"input-group-btn"​>
  <button class=​"btn btn-default"​ type=​"button"
 (​click​)="​addTag​(​photo​)"​>Add Tag</button>
  </span>
  </div>
  </div>
 </div>

The addTag method is nothing surprising. It just adds a tag to the photo object passed in, and resets the form. Changing the value of the component’s property lets Angular know that we also want to change the value on the page itself through the square brackets on ngModel.

 addTag(photo: IPhoto) {
  photo.tags.push(​this​.tagInput);
 this​.tagInput = ​''​;
 }

Now that the user can add tags, we need to display them on the page. You’ll use *ngIf again to prevent displaying the tag list before there are any tags to show, and *ngFor to iterate over all of the tags that have been added so far.

 <div class=​"row"​ ​*​ngIf=​"photo.tags.length"​>
  <div class=​"col-xs-4 col-xs-offset-4"​>
  Tags:
  <ul>
  <li ​*​ngFor=​"let tag of photo.tags"​>{{ tag }}</li>
  </ul>
  </div>
 </div>

After the user has added tags to their heart’s delight, they need to save the photo. First, the photo service needs a new method that saves the details of a single photo:

 savePhoto(photo: IPhoto) {
 return​ ​this​.http.put(​this​.api + ​'/updatePhoto'​, photo);
 }

Once that’s in, add a button to the page to call the new method:

 <div class=​"row"​>
  <div class=​"col-xs-4 col-xs-offset-4"​>
  <button class=​"btn btn-success"
 (​click​)="​photos​.​savePhoto​(​photo​)"​>Save</button>
  </div>
 </div>

In this case, the view can call directly through to the service, because the function will only be called on every click event, not every time something changes.

There’s one final change to make to this page. Right now, there’s a new call to save a photo every time the user clicks the Save button, even when there’s already an active request. There’s now a saving property to the component. You’ve learned that you can bind that property to an attribute with square brackets. Bind the saving property to the disabled attribute of the Save button with [disabled]="saving". With this, the main functionality of your website is complete. Let’s add some analytics to track how people use the site and plug in a universal HTTP error handler.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
18.118.24.30