Brian Vander Plaats

Developer

Blog

30 Sep 2016

Angular InMemoryDataService

Reviewing the HTTP Angular Tutorial this morning, I was really confused how the sample “Web API” was working in the sample plunker.

It started out as:

search(term: string): Observable<Hero[]> {
    return this.http
               .get(`app/heroes/?name=${term}`)
               .map((r: Response) => r.json().data as Hero[]);

app/heroes/?name=${term} is clearly the API call, but where is the function that processes it? I assumed that this was in HeroesComponent, since app.routing.ts defines the heroes route.

{
    path: 'heroes',
    component: HeroesComponent
  }

But why would the API be in a component class, that makes no sense! Moreover, the only likely function,

  getHeroes(): void {
    this.heroService
        .getHeroes()
        .then(heroes => this.heroes = heroes);
  }

doesn’t include any parameters, and a console.log() very clearly showed this function is not called during search operations.

I was close to just assuming there was some invisible api I couldn’t see, when I re-read the tutorial and noticed something about the InMemoryDataService:

import { InMemoryDbService } from 'angular-in-memory-web-api';
export class InMemoryDataService implements InMemoryDbService {
  createDb() {
    let heroes = [
      {id: 11, name: 'Mr. Nice'},
      {id: 12, name: 'Narco'},
      {id: 13, name: 'Bombasto'},
      {id: 14, name: 'Celeritas'},
      {id: 15, name: 'Magneta'},
      {id: 16, name: 'RubberMan'},
      {id: 17, name: 'Dynama'},
      {id: 18, name: 'Dr IQ'},
      {id: 19, name: 'Magma'},
      {id: 20, name: 'Tornado'}
    ];
    return {heroes};
  }
}

the return name is {heroes}. What if I changed that to {heroesx}? Sure enough, the app/heroes call failed. I then tried changing the service url in hero.service.ts to

  private heroesUrl = 'app/heroesx'; 

And the service worked again. That’s pretty interesting behavior, but certainly not intuitive…

The next question was how the name filter worked

.get(`app/heroes/?name=${term}`)

As I didn’t see any functions for that. Well it turns out this is also a feature of the InMemoryDataService. You can pass a filter for any property of the json, so this will work as well:

.get(`app/heroes/?id=${term}`)

28 Sep 2016

Angular2 Simple Edit Page

Going over the Angular2 docs on forms, and decided to bring forward my 1.x simple forms demo

It ports over fairly well, but a significant number of changes needed to be made. Honestly it seems like it would take an enormous effort to convert an existing 1.x application to Angular2.

Specific Changes

  • update directives
    • ng-model -> [(ngModel)]
    • ng-class -> [ngClass]
    • ng-submit -> (ngSubmit)
    • ng-show -> [hidden] (could have also used *ngIf)
    • ng-maxlength -> [maxlength]
    • ng-disabled -> [disabled]
  • instead of defining a telephone filter, I created a pipe
  • Create ManufacturerService with a type class and a mock
  • Create generic LookupService for the list of states
  • angular.copy() does not have a direct replacement in Angular2. Object.assign() works well as long as a deep copy is not needed, which isn’t the case here (Manufacturer doesn’t contain collections, just simple types)
  • instead of defining an empty manufacturer JSON object, we can instantiate a new, empty manufacturer. (to be used when the new button is clicked)
  • The currency filter will not work with an empty string. In edit mode, blanking out the <input> causes <span class="displayValue" [ngClass]="pageMode">{{manufacturer.creditLimit | currency }}</span> to raise an exception. The workaround is to set the <input type="number"
    • alternately, one could argue that the view controls should not be bound to the same object the edit controls are bound to
  • I wasn’t able to access the manufacturerForm properly from the simple-edit-form.component.ts.
    • within the template, manufacturerForm.valid is available, but in the component.ts it is undefined
    • ended up using manufacturerForm.checkValidity()
    • this plunker (not mine) shows the forms valid property as accessible in component.ts, but I didn’t get this to work
  • needed to set #manufacturerForm="ngForm" inside the <form>
  • In addition to the id, name attributes, needed to set #nameInput="ngModel" inside the <input>’s using validation.
  • spent a lot of time figuring out why nameInput.errors.required was giving undefined errors. It looks like you always need to check for valid first, as the errors collection is only defined when errors are present: [hidden]="nameInput.valid || !nameInput.errors.required" The || causes a short-circuit evaluation in the expression. If you remove the nameInput.valid from the example below you can repro this issue.

Demo

27 Sep 2016

fakepath

Had a good one today. Someone asked me at work why a file upload wasn’t working on an old ASP intranet site. To be clear, the site doesn’t actually upload a file, it merely stores a network share/path to the file, and presents a file:// as a clickable link on a webpage. Bad design yes, but good enough for it’s purpose.

The site worked in IE, but not in chrome, not too suprising, but I didn’t know what the issue was. I quickly threw up a fiddle, and reproduced the behavior in chrome/firefox. Here’s what the different browsers displayed:

File Input

With the same HTML:

<input id="fileInput" type=file name=attachment maxlength=200 size=40>

I did a little digging, seeing what I could extract from the file, when I noticed something curious. document.getElementById("fileInput").value was returning C:\fakepath\myFile.txt Ummm, what?

Well it turns out that the HTML5 folks really don’t want the browser mucking about with the local file system. This actually goes back several years, according to this post from 2009. It’s not a big deal though, I’ll just re-write that intranet app…

Thankfully it will work on IE, as long as this easy-to-configure setting is in place:

Include Local Directory Path

The funny part is, if this is set to disable, the <input> will show the path as c:\temp\myFile.txt, but the actual value will be c:\fakepath\myFile.txt.

Frankly the better solution would be to simply return the file name without a real or fake path. Firefox does this currently (try it out in the fiddle to see for yourself). I can understand the need to deprecate a feature, but if you are going to break something, don’t return a quasi-real result.

26 Sep 2016

Angular Cookies

Looking at adding cookies to an Angular2 demo the other day, and found out the $cookie service is not implemented natively in Angular2. Doing some quick googling, found a decent implementation: https://github.com/salemdar/angular2-cookie .

To use, you simply need to import the CookieService inside of app.module, making it available to other components.

Interacting with the cookies is pretty simple:

Setting

    this._cookieService.put("cookieSimple", "A Simple Cookie");
    this._cookieService.putObject("cookieObject", {option1:true, option2:1, option3:"default"});
    this._cookieService.put("cookieWithOptions", "Expires in a long time", {expires:"Mon, 30 Jun 2290 00:00:00 GMT"});

Getting

    this.cookieSimpleValue = this._cookieService.get("cookieSimple");
    this.cookieObjectValue = this._cookieService.get("cookieObject"); // or getObject()
    this.cookieWithOptionsValue = this._cookieService.get("cookieWithOptions");

plunker demo:

plnkr.co/edit/k1KCVPupsdiSk2gvkKK1

23 Sep 2016

Angular2 Pipes

Going over the documentation for Angular2 pipes today.

Pipes have changed since angular 1. Most noticeable is the lack of filter/sorting pipes. The reason given for their removal was speed. While they worked well enough for demo applications, real-world usage often caused speed issues. At my company we noticed this with lists only a few hundred items long.

Angular comes with a few standard pipes

  • Date
  • UpperCase
  • LowerCase
  • Currency
  • Percent
{{ myValue | date}}

You pass parameters to the pipe with a :

{{myValue | date:"shortDate"}}

The currency pipe requires multiple parameters, seperated by :

  • symbolDisplay - USD, EUR, etc
  • symbolDisplay - True to show $, False to show USD (false is the default)
{{myValue | currency:"USD":true}}

It’s simple to chain pipes:

{{ myValue | uppercase | lowercase}}

If the built-in pipes do not meet your needs, you will need to process the data in the component, or create a custom pipe. The angular team recommends handling expensive operations i.e. filtering in the component, instead of a pipe.

To create the custom pipe you’ll need to:

  1. Add a new class that will implement the pipe as <pipename>.pipe.ts
  2. Import the new pipe class in app.module.ts, specifying both as an import and a declaration.
  3. You do not need to declare anything in your component class that makes use of the pipe.

Here is a basic pipe implementation:

import { Pipe, PipeTransform } from '@angular/core';

// Take Name e.g. Bill Gates and return Gates, Bill
@Pipe({name: 'LastNameFirst',
       pure: true 
})
export class LastNameFirstPipe implements PipeTransform {
  transform(value: string): string {
    return value.substring(value.indexOf(' ') + 1) + ", " + value.substring(0, value.indexOf(' '));
  }
}

Notice the pure attribute. Pipes in angular can be marked as pure or unpure

Pure pipes:

  • only runs when a pure change is made to the input value.
  • a pure change is when a primitive type is updated, or a reference type is updated
  • a pure change does not include changes within an object e.g. {{customer.name | uppercase}} a change to name would not cause the filter to be re-evaluated, since this is inside the customer object. This is for performance reasons.

Impure Pipes

  • angular runs impure pipes on each component change detection cycle, meaning they run a lot.
  • the above customer.name example would be detected if we created a custom uppercase filter with pure set to false.
  • pipes set as impure should be as performant as possible.

plunker demo:

plnkr.co/edit/G6MNM1Ys99al9svYFM3W