5 Rookie Mistakes to Avoid with Angular 2

So you've read some blog posts, watched some conference videos, and finally you're ready to get your feet wet with Angular 2.    What are some things you should know starting out?

Here's a compilation of common beginner mistakes to avoid when you're writing your first application. 

Note:  This blog post assumes basic knowledge of Angular 2.  If you're brand new to the framework, here are some good resources to get up to speed:

Mistake #1:  Binding to the native "hidden" property

In Angular 1, if you wanted to toggle the visibility of an element, you would likely use one of Angular's built-in directives, like ng-show or ng-hide :

Angular 1 example
<div ng-show="showGreeting">
Hello, there!
</div>

In Angular 2, template syntax makes it possible to bind to any native property of an element. This is incredibly powerful and opens up a number of possibilities.  One such possibility is to bind to the native hidden property, which, similar to ng-show , sets display to "none".   

Angular 2 [hidden] example (not recommended)
<div [hidden]="!showGreeting">
Hello, there!
</div>

At first glance, binding to the hidden property seems like the closest cousin to Angular 1's ng-show .  However, there is one "!important" difference.  

ng-show and ng-hide both manage visibility by toggling an "ng-hide" CSS class on the element,  which simply sets the display property to "none" when applied.  Crucially, Angular controls this style and postscripts it with "!important" to ensure it always overrides any other display styles set on that element.

On the other hand, the "display: none" styles attached to the native hidden property are implemented by the browser.  Most browsers do so without an "!important" postscript.  Consequently, the style carries low specificity and can be easy to override accidentally.  It's as simple as adding any display style to the element you're trying to hide.  In other words, if you set "display: flex" on the element in your stylesheet, it will outrank the style set by  hidden and the element will always be visible  (see example ) .

For this reason, it's often better to toggle an element's presence using *ngIf.

Angular 2 *ngIf example (recommended)
<div *ngIf="showGreeting">
Hello, there!
</div>

Unlike the hidden property, Angular's *ngIf directive is not subject to style specificity constraints.  It's always safe to use regardless of your stylesheet.   However, it's worth noting that it's not functionally equivalent.  Rather than toggling the display property, it works by adding and removing template elements from the DOM.

An alternative would be to set a global style in your application that configures "display: none !important" for all hidden attributes.  You might ask why the framework doesn't handle this by default.  The answer is that we can't assume this global style is the best choice for every application.  Because it could potentially break apps that rely on the normal specificity of hidden , we leave it up to developers to decide what is right for their use case.

Mistake #2:  Calling DOM APIs directly

There are very few circumstances where manipulating the DOM directly is necessary.   Angular 2 provides a set of powerful, high-level APIs like queries that one can use instead.  Leveraging these APIs confers a few distinct advantages:

  • It's possible to unit test your application without touching the DOM, which removes complexity from your testing and helps your tests run faster.

  • It decouples your code from the browser, allowing you to run your application in any rendering context, such as web workers or outside of the browser completely (for example, on the server or in Electron).

When you manipulate the DOM manually, you miss out on these advantages and ultimately end up writing less expressive code.

Coming from an Angular 1 paradigm (or no Angular background), there are a few situations where you may be tempted to work with the DOM directly.  Let's review a few of these situations to demonstrate how to refactor them to use queries.

Scenario 1:  You need a reference to an element in your component's template


Imagine you have a text input in your component's template and you want it to auto-focus when the component loads. 

You may already know that @ViewChild/@ViewChildren queries can provide access to component instances nested within your component's template.  But in this case, you need a reference to an HTML element that is not attached to a particular component instance. Your first thought may be to inject the component's ElementRef thusly:

Working with the ElementRef directly (not recommended)
@Component({
 selector: 'my-comp',
 template: `
   <input type="text" />
   <div> Some other content </div>
 `
})
export class MyComp {
 constructor(el : ElementRef ) {
   el.nativeElement.querySelector('input').focus();
 }
}

However, this type of workaround isn't necessary.  

Solution: ViewChild + local template variable

What developers often don't realize is that it's also possible to query by local variable in addition to component type.  Since you control your component's view, you can add a local variable to the input tag (e.g. "ref-myInput" or "#myInput") and pass the variable name into the @ViewChild query as a string. Then, once the view is initialized, you can use the renderer to invoke the focus method on that input.

Working with ViewChild and local variable (recommended)
@Component({
 selector: 'my-comp',
 template: `
   <input #myInput type="text" />
   <div> Some other content </div>
 `
})
export class MyComp implements AfterViewInit {
 @ViewChild('myInput') input: ElementRef;

 constructor(private renderer: Renderer) {}

 ngAfterViewInit() {
   this.renderer.invokeElementMethod(this.input.nativeElement,    
   'focus');
 }
}

Scenario 2:  You need a reference to an element a user projects into your component

What if you need a reference to an element that isn't in your component's template?   As an example, let's imagine you have a list component that accepts custom list items through content projection, and you'd like to track the number of list items. 

You can use a @ContentChildren query to search your component's "content" (i.e. the nodes projected into the component), but because the content is arbitrary, it's not possible to label the nodes with local variables yourself (as in the last example). 

One option is to ask your users to label each of their list items with a pre-agreed-upon variable, like "#list-item". In that case, the approach resembles the last example:

ContentChildren and local variable (not recommended)
// user code
<my-list>
  <li *ngFor =" let item of items " #list-item > {{item}} </ li >
</my-list>

// component code
@Component({
 selector: 'my-list',
 template: `
   <ul>
     <ng-content></ng-content>
   </ul>
 `
})
export class MyList implements AfterContentInit {
 @ContentChildren('list-item') items: QueryList<ElementRef>;

 ngAfterContentInit() {
    // do something with list items
 }
}

However, this solution isn't ideal because it requires users to write some extra boilerplate. You may prefer an API with regular <li> tags and no attributes.  How can we make this work?

Solution: ContentChildren + directive with li selector

One great solution is to take advantage of the selector in the @Directive decorator. You simply define a directive that selects for <li> elements, then use a @ContentChildren query to filter all <li> elements down to only those that are content children of the component.

ContentChildren and directive (recommended)
// user code
<my-list>
  <li *ngFor="let item of items"> {{item}} </li>
</my-list>

@Directive({ selector: 'li' })
export class ListItem {}

// component code
@Component({
 selector: 'my-list'
})
export class MyList implements AfterContentInit {
 @ContentChildren(ListItem) items: QueryList<ListItem>;

 ngAfterContentInit() {
    // do something with list items
 }
}


Note:  It seems like it might work to select for only <li> elements within <my-list> tags (e.g. "my-list li"), but it's important to note that parent-child selectors aren't yet supported.  If you want to limit the results to children of your component, using queries to filter is the best way.

Mistake #3:  Checking for query results in the constructor

When first playing around with queries, it's easy to fall into this trap:

Logging query in constructor (broken)
@Component({...})
export class MyComp {
 @ViewChild(SomeDir) someDir: SomeDir;

 constructor() {
   console.log(this.someDir);        // undefined
 }
}

When the console logs "undefined", you may assume the query isn't working or you constructed it incorrectly.  In fact, you are just checking for the results too early in the component's lifecycle.  It's key to remember that query results are not yet available when the constructor executes.  

Luckily, Angular's new lifecycle hooks make it easy to puzzle out when you should check for each type of query.    

  • If you're conducting a view query, the results will become available after the view is initialized.  Use the handy ngAfterViewInit lifecycle hook.

  • If you're conducting a content query, the results become available after the content is initialized.  Use the ngAfterContentInit lifecycle hook.

So we can fix our above code thusly:

Logging query in ngAfterViewInit hook (recommended)
@Component({...})
export class MyComp implements AfterViewInit {
 @ViewChild (SomeDir ) someDir : SomeDir ;

 ngAfterViewInit() {
   console.log(this.someDir);        // SomeDir {...}
 }
}

Mistake #4:  Using ngOnChanges to detect query list changes

In Angular 1, if you wanted to be notified when a value changed, you'd have to set a $scope.$watch and manually check for changes each digest cycle.   In Angular 2, the ngOnChanges hook greatly simplifies this process. Once you define an ngOnChanges method in your component class, it will be called whenever the component's inputs change.  This comes in very handy.

However, the ngOnChanges method executes only when the component's inputs change -- specifically, those items you have included in your inputs array or explicitly labeled with an  @Input decorator.   It will not be called when items are added or removed from @ViewChildren or @ContentChildren query lists.

If you want to be notified of changes in a query list, don't use ngOnChanges. I nstead subscribe to the query list's built-in observable, its "changes" property.  As long as you do so in the proper lifecycle hook, not the constructor, you will be notified whenever an item is added or removed.

For example, the code might look something like this:

Using 'changes' observable to subscribe to query list changes (recommended)
@Component({ selector: 'my-list' })
export class MyList implements AfterContentInit {
 @ContentChildren(ListItem) items : QueryList < ListItem >;

 ngAfterContentInit() {
   this.items.changes.subscribe(() => {
       // will be called every time an item is added/removed
   });
 }
}

If you're new to observables, you can learn more about them here .

Mistake #5: Constructing ngFor incorrectly

In Angular 2, we introduce the concept of "structural directives" that add or remove elements from the DOM based upon expressions. Unlike other directives, structural directives must be used with a template element, a template attribute, or an asterisk.   Given this new syntax, it tends to be a target for beginner mistakes.

Can you spot the common errors below?

Incorrect ngFor code
// a:
<div *ngFor="#item in items">
  <p> {{ item }} </p>
</div>

// b:
<template *ngFor let-item [ngForOf]="items">
   <p > {{ item }} </ p >
</template>

// c:
<div *ngFor="let item of items; trackBy=myTrackBy; let i=index">
  <p>{{i}}: {{item}} </p>
</div>

Let's correct the above errors, one-by-one.

5a:  Using outdated syntax
// incorrect
<div *ngFor="#item in items">
  <p> {{ item }} </p>
</div>

There are actually two errors here. The first is a common trap that developers can fall into if they have Angular 1 experience.  In Angular 1, the equivalent repeater would read ng-repeat="item in items" .

Angular 2 switches from "in" to "of" in order to resemble the ES6 for-of loop.   It may help to remember that ngFor's actual de-sugared input property is ngForOf, not  ngForIn .

The second error is the "#". In older versions of Angular 2, the "#" used to create a local template variable within the  ngFor , but as of beta.17,  ngFor uses the "let" prefix instead.  

A good rule of thumb is to use the "let" prefix if you want to make a variable available within ngFor's template, and to use the "#" or "ref" prefix if you want to get a reference to a particular element outside of an ngFor (like #myInput in Mistake 2).


// correct
<div *ngFor="let item of items">
  <p> {{ item }} </p>
</div>

5b: Mixing sugared and de-sugared template syntax
// incorrect
<template *ngFor let-item [ngForOf]="items">
   <p > {{ item }} </ p >
</template>

It's unnecessary to include both a template tag and an asterisk - and in fact, using both won't work.  Once you prefix a directive with an asterisk, Angular treats it as a template attribute, rather than a normal directive.  Specifically, the parser takes the string value of ngFor, prefixes it with the directive name, and then parses it as a template attribute.  In other words, something like 

<div *ngFor="let item of items">

is treated just like

<div template="ngFor let item of items">

Functionally then, when you use both, it's like writing:

<template template="ngFor" let-item [ngForOf]="items">

Consequently, when the template attribute is parsed, all the value contains is "ngFor".  Without a source collection or a local variable for "item" in that string, it cannot be processed correctly and nothing happens.

On the other side, the template tag no longer has an ngFor directive attached, so the code will throw an error.  Without an ngFor directive, the ngForOf property binding has no component class to bind to.

The error can be remedied either by removing the asterisk, or converting completely to the short version of the syntax.


// correct
<template ngFor let-item [ngForOf]="items">
   <p > {{ item }} </ p >
</template>

// correct
<p *ngFor="let item of items">
  {{ item }}
</p>

5c: Using the wrong operator in * syntax
// incorrect
<div *ngFor="let item of items; trackBy=myTrackBy; let i=index">
  <p>{{i}}: {{item}} </p>
</div>

To explain what's going wrong here, let's start by rewriting the code in long-form template syntax:


// correct
<template ngFor let-item [ngForOf]="items" [ngForTrackBy]="myTrackBy" let-i="index">
  <p> {{i}}: {{item}} </p>
</template>

In this form, it's easier to understand the structure of the directive.  To break it down:

  • We are passing two pieces of information into ngFor using input properties:
    • Our source collection (items) is bound to the ngForOf property
    • Our custom track-by function (myTrackBy) is bound to the ngForTrackBy property
  • We are declaring two local template variables using the "let" prefix: "i" and "item".  The ngFor directive sets these variables as it iterates over the items in the list.  
    • "i" is set to the zero-based index of the items list
    • "item" is set to the current element in the list at index "i"

When we shorten the code to use asterisk syntax, we have to follow certain rules that the parser will understand:

  • All configuration must happen within the string value of the *ngFor attribute
  • We set local variables using an = operator
  • We set input properties using a : operator
  • We strip the directive name prefix from input properties ("ngForOf" → "of")
  • We separate statements with semicolons

Following these rules, here is the result:


// correct
<p *ngFor="let item; of:items; trackBy:myTrackBy; let i=index">
  {{i}}: {{item}}
</p>

Semicolons and colons are actually optional because they are ignored by the parser.  They're used mainly for readability.  Thus, we can clean up our code to flow a bit more naturally:


// correct
<p *ngFor="let item of items; trackBy:myTrackBy; let i=index">
  {{i}}: {{item}}
</p>

Conclusion

I hope you found this explanation of common gotchas helpful.  Happy coding!


原文链接: http://angularjs.blogspot.com/2016/04/5-rookie-mistakes-to-avoid-with-angular.html

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值