Implementing Angular2 forms – Beyond basics

In the first part of this article, we described the basics of the form support of Angular2. In this second part, we will deal with more advanced concepts.

Improving forms

As you can see, the form we created in the last section can be improved to be more concise and efficient to write. Here we will describe some techniques that leverage the power of Angular 2 components and directives to achieve this goal.

Bootstrap-ifying the form

Some classes need to be applied to forms and elements to display them using the Bootstrap 3 library. They need to be set on every element to be able to leverage its form support. Attribute directives of Angular 2 could provide us a great support to apply these under the hood to all these elements. As a matter of fact, they aim to change the appearance or behavior of an element.

In our case, we need to add the form-control class to the <input> / <select> / <textarea>HTML tags and form-horizontal to the form one. For this purpose, just create two directives with selectors that match these tags and leverage the ElementRef and Renderer classes to add the classes.

import {Directive, ElementRef, Renderer, Input} from 'Angular 2/core';

@Directive({
    selector: 'input:not([noBootstrap]), textarea:not([noBootstrap])
})
export class BootstrapInputDirective {
  constructor(el: ElementRef, renderer: Renderer) {
    renderer.setElementClass(el.nativeElement, 'form-control', true);
  }
}

@Directive({
  selector: 'form:not([noBootstrap])'
})
export class BootstrapFormDirective {
  constructor(el: ElementRef, renderer: Renderer) {
    renderer.setElementClass(el.nativeElement, 'form-horizontal', true);
  }
}

Notice that there is no impact in the component templates that use forms.

Form component for fields

As you can see there is a lot of code duplication within the form because of the use of Bootstrap. As a matter of fact, it requires a specific structure and dedicated CSS classes. In order to simplify this code, a good idea would be to modularize all this stuff into a component.

That said, some parts like the definition of inputs remain specific. Of course, we want to handle field validation within the component. Let’s first define the structure of this component. We need to provide the value of the label and the field state. We simply move the HTML block for a field into the component template.

@Component({
  selector: 'field',
  template: `
    <div class="form-group form-group-sm" [ngClass]="{'has-error':state && !state.valid}">
      <label for="for" class="col-sm-3 control-label">{{label}}</label>
      <div class="col-sm-8">
        <!-- Input, textarea or select -->
        <span *ngIf="state && !state.valid" class="help-block text-danger">
          <span *ngIf="state.errors.required">The field is required</span>
        </span>
      </div>
    </div>
  `
})
export class FormFieldComponent {
  @Input()
  label: string;

  @Input()
  state: Control;
}

As you can see, the expressions regarding field validation remain the same but do not rely on thestate attribute provided to the component.

We use the template attribute here to see both template and component in a single snippet. You should externalize the HTML code into a file referenced using the templateUrl attribute.

The last thing to implement is to include the specific part, the form element itself. Angular 2 provides the ng-content component for this purpose. It allows you to put into the component template the HTML content provided when using the component. The template will now look like this:

<div class="form-group form-group-sm" [ngClass]="{'has-error':state && !state.valid}">
  <label for="for"
      class="col-sm-3 control-label”>{{label}}</label>
  <div class="col-sm-8">
    <ng-content ></ng-content>
    <span *ngIf="state && !state.valid" class="help-block text-danger">
      <span *ngIf="state.errors.required">The field is required</span>
    </span>
  </div>
</div>

We are now ready to refactor our form using this component. That way, our input automatically takes part of a Bootstrap-based form and errors are automatically displayed according to specified validations.

<form [ngFormModel]="companyForm">
  <field label="Name" [state]="companyForm.controls.name">
    <input [ngFormControl]="companyForm.controls.name" [(ngModel)]="company.name"/>
  </field>
</form>

You can see that there is some duplication here at the level of the state attribute. We need to specify it on both field component and form elements. A good approach would be to let the field component deduce the controller of the element it wraps. Angular 2 provides an elegant way to do this thanks to the ContentChild decorator. You can reference components and directives within the ng-content block.

In our case, we need to find out a directive of kind NgFormControl. To achieve this, simply add a property in the component decorated with @ContentChild, as described below. There is no need now to keep the state input.

export class FormFieldComponent {
  @Input()
  label: string;

  @Input()
  feedback: boolean;

  @ContentChild(NgFormControl) state;

  (...)
}

We can now refactor our form to remove the state parameter when using the field element:

<form [ngFormModel]="companyForm">
  <field label="Name">
    <input [ngFormControl]="companyForm.controls.name" [(ngModel)]="company.name"/>
  </field>
</form>

For our use case, we also need to implement an additional component to manage labels of the company.

Specific form component

In our company form, we want to be able to specify labels to identify the company sectors, like “IT”, “computer”, “maintenance”, “software”. Implementing a complex field will fit our needs and modularize its processing in a single entity.

We want to leverage the two way binding support of Angular 2 at this level to let the labels field of the company to be automatically updated when a label is added or removed.

Let’s create the structure of our component to make it accept a values attribute and fire avaluesChange event. With such names, the values field can leverage the [(values)]expression to use two way binding.

@Component({ 			
  selector: labels', 			
  template: ` 			
    (...)
  ` 			
}) 			
export class LabelsComponent { 			
  @Input() 			
  values:string[]; 			

  @Output() 			
  valuesChange: EventEmitter; 			

  constructor() {
    this.valuesChange = new EventEmitter(); 			
  } 			

  (...)
}

We can notice the use of the @Input annotation for parameters and @Ouput for custom events of the component.

The component builds a list of Bootstrap labels using the values attribute. For each element, we attach an event to delete the label from the list. Besides this list, a dedicated input is added for the label creation. Here is a simple version of the component template.

@Component({
  selector: 'labels',
  template: `
    <div *ngIf="values">
      <span *ngFor="#value of values" style="font-size:14px"
          class="label label-default" (click)="removeValue(tag)">
        {{value}} <span class="glyphicon glyphicon-remove" aria-hidden="true"></span>
      </span>
      <span> | </span>
      <span style="display:inline-block;">
        <input [(ngModel)]="valueToAdd" style="width: 50px; font-size: 14px;" class="custom"/>
        <em class="glyphicon glyphicon-ok" aria-hidden="true" (click)="addValue(valueToAdd)"></em>
      </span>
    </div>
  `
})

All these events are attached to internal removeValue and addValue methods of the component. Each time the labels are updated the valuesChange event must be fired to update external bindings.

export class LabelsComponent implements OnInit {
  (...)

  removeValue(value:string) {
    var index = this.values.indexOf(label, 0);
    if (index != undefined) {
      this.values.splice(index, 1);
      this.valuesChange.emit(this.values);
    }
  }

  addValue(value:string) {
    this.values.push(this.valueToAdd);
    this.valuesChange.emit(this.values);
    this.valueToAdd = '';
  }
}

This component can be simply referenced within the form component and used in its associated template.

<form [ngFormModel]="companyForm">
  (...)
  <field label="Tags">
    <labels [(values)]="company.tags"></labels>
  </field>
  (...)
</form>

This approach provides an interesting way to implement a custom form component but its main drawback is that this component doesn’t take part of the form controller. Angular 2 makes it possible to improve this component by leveraging the ngModel feature.

NgModel-compatible component

The labels component we implemented is fine since it allows you to bring some advanced form elements. Its main drawback is that it is not taken into account in the form validation. What happens if I want to ensure that the list of tags isn’t empty?

We previously discussed that Angular 2 provides two way binding and validation for native form elements like inputs, selects and textarea. Angular 2 is extendable and allows you to bring this feature into custom components. That way such components can take part in the validation of the form. This means that if the validation fails for the component, the whole form will be invalid.

Let’s start to adapt the custom component implemented in the previous section. At this level, the only thing to do is to the input property labels since corresponding value will now be provided by ngModel itself. A new method must be added to allow you to set this value directly.

export class LabelsComponent {
  (...)

  writeLabelsValue(labels:string[]) {
    this.labels = labels;
  }

  (...)
}
NgModel

The value accessor corresponds to a directive that will be attached to the custom component itself. This way we will be able to detect the labelsChange event and trigger the registeredonChange callback. For information the writeValue method is called when the ngModel is updated programmatically. This makes it possible to call the host (i.e. the component attached on) to set this new value.

@Directive({
  selector: 'labels',
  host: {'(labelsChange)': 'onChange($event)'}
})
export class LabelsValueAccessor implements ControlValueAccessor {
  onChange = (_) => {};
  onTouched = () => {};

  constructor(private host: LabelsComponent) {

  }

  writeValue(value: any): void {
    this.host.writeLabelsValue(value);
  }

  registerOnChange(fn: (_: any) => void): void { this.onChange = fn; }
  registerOnTouched(fn: () => void): void { this.onTouched = fn; }
}

The remaining task involves registering this class within the providers of the component to make the class take part in the ngModel processing.

const CUSTOM_VALUE_ACCESSOR = new Provider(
    NG_VALUE_ACCESSOR, {useExisting: forwardRef(() => LabelsValueAccessor), multi: true});

@Directive({
  (...)
  providers: [CUSTOM_VALUE_ACCESSOR]
})
export class LabelsValueAccessor implements ControlValueAccessor {
  (...)
}

We can now use both ngModel and ngFormControl directives on our custom component.

<field label="Tags">
  <labels [ngFormControl]="companyForm.controls.tags"
               [(ngModel)]="company.tags"></labels>
</field>

Asynchronous validation for fields

We previously saw a simple custom validator to validate zip codes. Validations can be a bit more advanced and require an exchange with the server. Imagine we want to ensure that the name of the company is unique. For this, we will trigger a HTTP request that looks for a company with the same name. If an empty result is received, we know that the name is unique.

In this case, we need to exclude the current company from the check, otherwise it would appear in the results.

We will leverage the CompaniesService. So the first thing is to implement the request.

findCompanyByName(companyName:string) {
  var headers = new Headers();
  this.createAuthorizationHeader(headers);

  return this.http.get(
    `https://Angular 2.apispark.net/v1/companies/?name=${companyName}`, {
      headers: headers
  }).map(res => res.json());
}

Now we need to be able to reference the service from the validation function. There is no dependency injection at this level but we can leverage closures. As a matter of fact, we have access to the service instance from the component where we register validators. So we will implement a function that will accept the service as parameter and create the actual validation function. This function will have access to the service when called during the validation process.

export function createUniqueNameValidator(service:CompaniesService) {
  return function(control) {
    (...)
  }
}

We can now implement the asynchronous validation function. Such a function leverages promises to notify Angular 2 of the success or failure of the validation. For that reason, the validation function must return this promise. In all cases, the promise must be resolved: withnull in the first case and with an object containing the key of the validator in the other case. The following code describes the whole code of this validator.

export function createUniqueNameValidator(
	      service:CompaniesService,component:DetailsComponent) {
  return function(control) {
    return new Promise((resolve, reject) => {
      service.findCompanyByName(control.value).subscribe(
        data => {
          if (data.length === 0 || (data.length === 1 &&
                component.company.id === data[0].id)) {
            resolve(null);
          } else {
            resolve({uniqueName: true});
          }
        },
        err => {
          resolve({uniqueName: true});
        }
      });
    });
  };
}

We could stop here but we want to offer a great user experience. So we need to improve our form a little bit to display a hint about the asynchronous validation. For this we can improve ourfield component.

Let’s start by adding a new feedback property to specify the component required to use Bootstrap feedback for the input. We also add three methods to detect the status of the feedback:

  • isFeedbackValid: the asynchronous validation is successful
  • isFeedbackNotValid: the asynchronous validation failed
  • isFeedbackPending: the asynchronous validation is in progress

These methods leverage the control associated with the component by checking the valid andpending properties.

@Component({
  (...)
})
export class FormFieldComponent {
  (...)

  @Input()
  feedback: boolean;

  (...)
  isFeedbackValid() {
    return this.state && this.feedback &&
       !this.state.control.pending && this.state.valid;
  }

  isFeedbackNotValid() {
    return this.state && this.feedback &&
       !this.state.control.pending && !this.state.valid;
  }

  isFeedbackPending() {
    return this.state && this.feedback && this.state.control.pending;
  }
}

The component template can now be updated to take these states into account to display the correct icons within the associated input element.

@Component({
  selector: 'field',
  template: `
    <div
         [ngClass]="{ (...), 'has-feedback':feedback}">
      <label *ngIf="label" for="for"
         class="col-sm-2 col-md-2 control-label">{{label}}</label>

      <div class="col-sm-8 col-md-8"
           [ngClass]="{'col-sm-8': label, 'col-md-8': label}">
        <ng-content ></ng-content>
        <span *ngIf="isFeedbackValid()" 
              class="glyphicon glyphicon-ok form-control-feedback text-success"
              aria-hidden="true"></span>
        <span *ngIf="isFeedbackNotValid()"
              class="glyphicon glyphicon-remove form-control-feedback"
              aria-hidden="true"></span>
        <span *ngIf="isFeedbackPending()"
              class="glyphicon glyphicon-refresh glyphicon-refresh-animate text-muted form-control-feedback"
              aria-hidden="true"></span>
        (...)
      </div>
    </div>
  `,
  styles: [
    `.glyphicon-refresh-animate {
      -animation: spin .7s infinite linear;
      -webkit-animation: spin2 .7s infinite linear;
    }`,
    `@-webkit-keyframes spin2 {
      from { -webkit-transform: rotate(0deg);}
      to { -webkit-transform: rotate(360deg);}
    }`
  ]
})
export class FormFieldComponent {
  (...)
}

We also defined some CSS styles for the component to have an spinning icon when the validation is in progress. Here is the result:

validation-async-successful

validation-async-pending

validation-async-failure

That’s all for this second part. In the next and last part, we will focus on the form submission.

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
使用Ansible实施DevOps 2有助于实现更高效的软件开发和运维流程。Ansible是一款开源的自动化工具,它提供了一种简单而强大的方式来自动化配置管理、应用部署和任务协调。 首先,使用Ansible可以实现基础设施即代码(Infrastructure as Code)。通过编写Ansible Playbooks,可以将整个基础设施的配置进行版本控制,并将其文档化。这样,团队成员可以更容易地理解和复制该基础设施。使用Ansible还可以轻松地在不同环境(例如开发、测试和生产)之间进行配置代码的复用,减少了配置的冗余和手动操作的错误。 其次,Ansible可以实现自动化部署。使用Ansible Playbooks,可以减少手动操作,从而降低了人为错误的风险。通过定义各个部署步骤和依赖关系,可以确保每个部署流程的一致性和可靠性。此外,Ansible还支持滚动更新,在保持应用的可用性的同时逐步更新。 此外,Ansible具有良好的扩展性和灵活性。能够与各种不同的工具和平台集成,例如CI/CD工具、云平台和容器管理器(如Docker和Kubernetes)。这使得在DevOps中使用Ansible更加方便,可以根据需要定制个性化的自动化流程。 最后,Ansible还提供了丰富的日志和报告功能,可以帮助团队追踪和分析各个任务的执行情况,以及及时发现和解决问题。这些日志和报告可以与监控和告警系统集成,以最大限度地减少对人工干预的需求,提高故障排查和修复的效率。 总而言之,使用Ansible实施DevOps 2可以帮助团队提高软件开发和运维效率,减少人为错误,并实现更好的可维护性和可扩展性。
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值