Wednesday, June 17, 2020

5 USAGE IDEAS FOR ANGULAR PIPES

5 usage ideas for Angular pipes thumbnail

Pipes are a useful feature in Angular. They are a simple way to transform values in an Angular template. There are some built in pipes, but you can also build your own pipes.

A pipe takes in a value or values and then returns a value. This is great for simple transformations on data but it can also be used in other unique ways. This post will highlight a few useful or unique use cases that we found for pipes.

The ratings on each of the points indicate how much I believe this particular use case abuses the pipe framework.

1. Return Default Values

The functionality behind a Default Pipe is pretty self explanatory—if a value is falsy, use a default value instead. The implementation is also very simple.

@Pipe({name: 'default', pure: true})
export class DefaultPipe {
   transform(value: any, defaultValue: any): any {
       return value || defaultValue;
   }
}

The default pipe uses Angular’s ability to pass multiple values into the pipe to get both the value and the default value.

An example of using the default pipe in Angular would be:

<lucid-icon [name]="folder.icon | default:'Folder'"></lucid-icon>

In the example above, either the folder has an icon that is used or the Folder icon is used as a default. You can play with the example code using the Default Pipe in the Plunker below.

Hackiness RatingDescription
This use is along the lines of what pipes are intended to do.

2. Debounce Input

Debounce Pipe is much more technically interesting than the Default Pipe. The basic premise of the idea is that the passed-in value can change frequently, but the actual value returned from the pipe will not change until the value has remained changed for a certain period of time. This can be very useful when listening to user input but not wanting to update the user interface until after the user has finished typing.

You can see the implementation below:

@Pipe({name: 'debounce', pure: false})
export class DebouncePipe {
   private currentValue: any = null;
   private transformValue: any = null;
   private timeoutHandle: number = -1;

   constructor(
       private changeDetector: ChangeDetectorRef,
       private zone: NgZone,
   ) {
   }

   transform(value: any, debounceTime?: number): any {
       if (this.currentValue == null) {
           this.currentValue = value;
           return value;
       }
       if (this.currentValue === value) {
           // there is no value that needs debouncing at this point
           clearTimeout(this.timeoutHandle);
           return value;
       }
       if (this.transformValue !== value) {
           // there is a new value that needs to be debounced
           this.transformValue = value;
           clearTimeout(this.timeoutHandle);
           this.timeoutHandle = setTimeout(() => {
               this.zone.run(() => {
                   this.currentValue = this.transformValue;
                   this.transformValue = null;
                   this.changeDetector.markForCheck();
               });
           }, typeof debounceTime == 'number' ? debounceTime : 500);
       }
       return this.currentValue;
   }
}

The Debounce Pipe takes in a value, and then if the value has changed since the last time the debounce completed (or it is null), it will wait either the time passed in as the second value or 500 ms if a time wasn’t provided, and then apply the new value.

An example of using it in Angular would be:

<div
    *ngIf="hasInputError(contentOption) | debounce"
    class="error-message"
>
    {{errorMessage(contentOption)}}
</div>

You can play with the example code using the Debounce Pipe in the Plunker below.

Hackiness RatingDescription
This pipe doesn’t generally follow the idea of taking in a value and returning a result value. Instead, it delays returning in the latest passed-in value, which means it has to be an impure pipe because it returns different values for the same input depending on the time. However, generally it works well and is not too abusive of the framework.

3. Get the Position of an Element

Angular has a handy feature that lets you assign an element or component to a variable and then reference that variable within that template. The Element Position Pipe takes advantage of that feature and allows you to pass an element to the pipe and have it return the position. This can be a useful feature for deciding where to position a pop-up or some other element.

Here is the implementation:

@Pipe({name: 'elementPosition', pure: true})
export class ElementPosition {
   transform(value: HTMLElement, xLerp: number, yLerp: number): Point|null {
       if (value != null) {
           const boundingRect = value.getBoundingClientRect();
           return {
               x: boundingRect.left + xLerp * boundingRect.width,
               y: boundingRect.top + yLerp * boundingRect.height,
           };
       } else {
           return null;
       }
   }
}

The two numbers that are passed in with the element are used to decide where on the element the position should be. The first value is used in the x position and is multiplied by the width. So if you want to get the position of the right side of the element, you would pass in 1 for the first parameter, which would mean x + 1 * width. The second parameter is the same but for the y value and the height.

An example of using the Element Position Pipe in Angular would be:

<div
   #titleElement
   (click)="expanded = !expanded"
>
   {{title}}
</div>
<ng-container popup [visible]="expanded">
   <lucid-menu
       *popupContent
       [items]="menuOptions"
       [position]="titleElement | elementPosition:0:1"
   ></lucid-menu>
</ng-container>

 

In this simplified snippet, the menu is getting the bottom-left position of the `titleElement` passed into its `[position]` input.

Hackiness RatingDescription
This use is fairly hacky because it doesn’t work in Web Worker or service-side environments. It also returns a position that is relative to the browser window and not the nearest relative or absolute positioned ancestor.

4. Feign Natural Typing

A flashy feature that I have seen throughout the web is animating text as if it was being typed by a user. An intuitive way to implement such a feature in Angular would be using pipes.

Below is a simple implementation of a Natural Typing Pipe:

@Pipe({name: 'naturalType', pure: false})
export class NaturalType {
   private typed: string = '';
   private target: string = '';
   private currentIndex: number = -1;
   private timeoutHandle: number = -1;

   constructor(
       private changeDetector: ChangeDetectorRef,
       private zone: NgZone,
   ) {
   }

   transform(value: string, mintypingSpeed: number = 30): any {
      if (this.target !== value) {
       clearTimeout(this.timeoutHandle);
       this.typed = '';
       this.currentIndex = -1;
       this.target = value;
       this.typeNextCharacter(mintypingSpeed);
      }
      return this.typed;
   }
   
   private typeNextCharacter(mintypingSpeed: number) {
    this.currentIndex++;
    this.typed = this.target.substr(0, this.currentIndex);
    this.changeDetector.markForCheck();
    if (this.typed !== this.target) {
      const time = Math.round(Math.random() * 70) + mintypingSpeed;
      this.timeoutHandle = setTimeout(()=> {
        this.zone.run(() => this.typeNextCharacter(mintypingSpeed));
      },time);
    }
   }
}

Usage of the pipe is pleasantly simple—simply pass a string through the pipe.

{{value | naturalType}}

You can play with the example code using the Natural Typing Pipe in the Plunker below.

Hackiness RatingDescription
In my opinion, this is actually a fairly elegant way to implement this feature. However, it does have to be an impure pipe because the value that it returns is based on the input and time, not just the input value.

5. Track User Input

Analytics and recently used lists are two features that come to mind when I think of tracking user input. It seems possible to do that with a pipe. So why not?

First, we will need a tracking service.

@Injectable()
export class TrackingService {
  private wordsUsed: Set = new Set();
  
  public addWordUsed(word: string) {
    this.wordsUsed.add(word);
  }
  
  public getWords(): string[] {
    return Array.from(this.wordsUsed);
  }
 
}

Then we will need a pipe to feed the service.

@Pipe({name: 'track', pure: true})
export class TrackingPipe {
   constructor(
       private trackingService: TrackingService,
   ) {
   }

   transform(value: string): string {
       this.trackingService.addWordUsed(value);
       return value;
   }
}

Consuming the feed is then simple with Angular.

@Component({
  ...
  template: `
    ... 
    <div *ngFor="let word of getWords()">{{word}}</div>
  `,
})
export class AppComponent {
  ...
  constructor(private trackingService: TrackingService) {
    
  }
  
  public getWords(): string[] {
    return this.trackingService.getWords();
  }
}

After running, it doesn’t render the list immediately. Checking the console we have this error: ERROR Error: ExpressionChangedAfterItHasBeenCheckedError: Expression has changed after it was checked. This error makes sense. When the change detector runs, it transforms the value using the tracking pipe which modifies the list. Since the value changed during change detection, the change detection is then out of date and has to run again. If it changes during change detection … ∞. Since we were smart ?, we put the debounce pipe that we built earlier in front of the tracker pipe ({{inputValue | debounce:200 | track}}), so we know that the value isn’t going to actually change every change detection and cause an infinite loop. So we can just make our logging run after an async callback, and then everything should work.

@Pipe({name: 'track', pure: true})
export class TrackingPipe {
   constructor(
       private trackingService: TrackingService,
       private changeDetector: ChangeDetectorRef,
       private zone: NgZone,
   ) {
   }

   transform(value: string): string {
       Promise.resolve().then(() => {
        this.trackingService.addWordUsed(value);
        this.zone.run(() => this.changeDetector.markForCheck()); 
       });
       return value;
   }
}

If we wanted to, we could even log the values over the network and then we wouldn’t have to fake the async! ?

You can play with the example code using the Tracking Pipe in the Plunker below.

Hackiness RatingDescription
This pipe doesn’t actually do any transformations on the value, which breaks the whole paradigm of a pipe. It has to pretend to log values in an asynchronous way in order to not modify component state during change detection. It is stateful. There is no benefit over just injecting the service into the component and logging the values there. But … it was possible and we did it ?.

No comments:

Post a Comment

Free hosting web sites and features -2024

  Interesting  summary about hosting and their offers. I still host my web site https://talash.azurewebsites.net with zero cost on Azure as ...