-1

We build a dashboard-like interface and frequently receive generic objects with optional fields, such as the event object below. event may not be defined at the time the template is loaded, and it may or may not have a daysRemaining ?: number parameter; if it is set, it may be 0.

To ensure that a 0 value is actually printed in the above scenario, we use this pattern:

<div *ngIf="event?.daysRemaining?.toString().length > 0">
  {{event.daysRemaining}} days
</div>

(The .toString() is necessary because Property 'length' does not exist on type 'number'.)

Our object tree can be much deeper than the above example. Where we can wrap common a parent tree in an <ng-container>'s *ngIf, we do, but we rarely can.

There must be a more elegant way to do this, especially with regard to needing to call .toString() on virtually every optional number.

This seems more efficient:

<div>
  {{event?.daysRemaining + ' days'}}
</div>

But has the drawback that if we need to optionally add things to the <div> (such a color: red when daysRemaining < 0), we still need all the checks.

msanford
  • 11,803
  • 11
  • 66
  • 93
  • where is the component code? update it to post – Aravind Jun 02 '17 at 19:54
  • @Aravind There is nothing useful in the Component except a service call to set `event`, which is why I didn't include it. I'm trying to solve a generic problem by demonstrating what I think is an anti-pattern we're using, rather than focusing on _this specific case_. If there is a suggestion to do better by doing more checks in the Component, by all means, share some, but we're currently doing none. – msanford Jun 02 '17 at 19:59
  • what error you are getting.. your post is confusing. – Aravind Jun 02 '17 at 20:01

2 Answers2

1

You can check the condition in the component using a method.

<div *ngIf="daysRemaining(event)">
  {{event.daysRemaining}} days
</div>

Typescript method will be as

daysRemaining(event){
    if(event && event.daysRemaining.toString().length > 0){
        return true;
    } else {
        return false;
    }
}
msanford
  • 11,803
  • 11
  • 66
  • 93
Aravind
  • 40,391
  • 16
  • 91
  • 110
1

In the component class I would use something like:

get daysRemaining(){
    return this.event && (this.event.daysRemaining || this.event.daysRemaining === 0) ?
             this.event.daysRemaining + ' days' : undefined;
}

and in the template:

<div>
  {{daysRemaining}}
</div>

If you think you migth be doing this a lot, you can create a Pipe:

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

@Pipe({name: 'printIfDefined'})
export class PrintIfDefinedPipe implements PipeTransform {

    transform(value:number, suffix?:string): number {
      let suffix = (suffix ? ' ' + suffix : '');
      return (value || value === 0) ? value + suffix : undefined;
    }

}

and in the template:

<div>
      {{event?.daysRemaining | printIfDefined: 'days' }} 
</div>

In both alternatives the div is still injected on de DOM though as it has no content it shouldn't affect your app (Unless you have css wit fixed widths or something like that).

If DOM space occupied bothers you, another alternative migth be a directive:

import { Directive, ElementRef, HostListener, Input } from '@angular/core';

@Directive({
  selector: '[print-if-defined]'
})
export class PrintIfDefinedDirective {

  constructor(private el: ElementRef) { }

  @Input('suffix') 
  suffix:string;

  @Input('value') 
  set value(nm:number) {
    this.el.nativeElement.style.display = nm || nm === 0 ? "" : "none";
    this.el.nativeElement.textContent =  nm + (this.suffix ? ' ' + this.suffix : '');
  }

}

and use it like

<div print-if-defined [value]="event?.daysRemaining" suffix="days"></div>

You can see all of them running in: https://plnkr.co/edit/gFeKJilYGdt5ttENjKUG?p=preview

stp18
  • 311
  • 2
  • 9