角度材料2具有ng值访问器的自定义组件

时间:2017-10-30 16:29:08

标签: angular angular-material2 angular4-forms

我正在研究棱角4.4 +材质beta12自定义组件,但无法弄清楚我的实现中出了什么问题

我正在尝试实现以下自定义输入
enter image description here

任务:

  1. 设置值为formControl,一旦我从服务器获取数据(data.productTeam是数据 - 可以在代码中看到)
  2. 在编辑时,应使用值更新formcontrol(例如:P12DT2H231M)
  3. 的问题:

    1. 我无法将默认值绑定到formcontrol。
    2. 没有ngDefaultControl(表单控件的值访问器没有名称:'productTeam'错误发生)
    3. dashboard.component.js

      this.CRForm = this.fb.group({
            productTeam: [data.productTeam || '']
      });

      在Dashboard.html中

      <mat-form-field  floatPlaceholder="always" >
              <app-mat-custom-form-field #custref formControlName="productTeam" placeholder="P12D" ></app-mat-custom-form-field>
          <!--<app-mat-custom-form-field #custref formControlName="productTeam" placeholder="P12D" ngDefaultControl></app-mat-custom-form-field> -->
            </mat-form-field>
       {{custref.value}} -- gives value eg:[P12DT1H2M] and only if ngDefaultControl
       {{CRForm['controls']['productTeam']['value']}} --not giving any

      垫定制外形field.ts

      import {
        Component,
        OnInit,
        OnDestroy,
        Input,
        HostBinding,
        Optional,
        Renderer2,
        Self,
        forwardRef,
        ElementRef
      } from '@angular/core';
      import {
        MatFormFieldControl
      } from '@angular/material';
      import {
        ControlValueAccessor,
        FormGroup,
        FormBuilder,
        NgControl,
        NG_VALUE_ACCESSOR
      } from '@angular/forms';
      import {
        coerceBooleanProperty
      } from '@angular/cdk/coercion';
      import {
        FocusMonitor
      } from '@angular/cdk/a11y';
      import {
        Subject
      } from 'rxjs/Subject';
      
      class Duration {
        constructor(public days: number, public hours: number, public minutes:
          number) {}
        getDuration() {
          return 'P' + (this.days || 0) + 'DT' + (this.hours || 0) + 'H' +
            (this.minutes || 0) + 'M';
        }
        setDuration() {}
      }
      @Component({
        selector: 'app-mat-custom-form-field',
        templateUrl: './mat-custom-form-field.component.html',
        styleUrls: ['./mat-custom-form-field.component.scss'],
        providers: [{
            provide: MatFormFieldControl,
            useExisting: MatCustomFormFieldComponent
          },
          {
            provide: NG_VALUE_ACCESSOR,
            useExisting: forwardRef(() => MatCustomFormFieldComponent),
            multi: true
          }
        ]
      })
      export class MatCustomFormFieldComponent implements OnInit,
      MatFormFieldControl < Duration > , ControlValueAccessor, OnDestroy {
        parts: FormGroup;
        focused = false;
        stateChanges = new Subject < void > ();
        errorState = false;
        controlType = 'my-tel-input';
        private _disabled = false;
        private _required = false;
        private _placeholder: string;
        static nextId = 0;
        @Input()
        get required() {
          return this._required;
        }
        set required(req) {
          this._required = coerceBooleanProperty(req);
          this.stateChanges.next();
        }
        @Input()
        get disabled() {
          return this._disabled;
        }
        set disabled(dis) {
          this._disabled = coerceBooleanProperty(dis);
          this.stateChanges.next();
        }
        /* code for placeholder property */
        @Input()
        get placeholder() {
          return this._placeholder;
        }
        set placeholder(plh) {
          this._placeholder = plh;
          this.stateChanges.next();
        }
        @Input()
        get value(): Duration | null {
          let n = this.parts.value;
          if (n.days && n.hours && n.minutes) {
            return new Duration(n.days, n.hours, n.minutes);
          }
          return null;
        }
      
        set value(duration: Duration | null) {
          duration = duration || new Duration(0, 0, 0);
          this.parts.setValue({
            days: duration.days,
            hours: duration.hours,
            minutes: duration.minutes
          });
          this.writeValue('P' + (duration.days || 0) + 'DT' + (duration.hours || 0) +
            'H' + (duration.minutes || 0) + 'M');
          this.stateChanges.next();
        }
        onContainerClick(event: MouseEvent) {
          if ((event.target as Element).tagName.toLowerCase() != 'input') {
            this.elRef.nativeElement.querySelector('input').focus();
          }
        }
      
        /* code to get id and set id*/
      
        @HostBinding() id = `mat-custom-form-
          field-${MatCustomFormFieldComponent.nextId++}`;
      
        @HostBinding('class.floating')
        get shouldPlaceholderFloat() {
          return this.focused || !this.empty;
        }
      
        @HostBinding('attr.aria-describedby') describedBy = '';
      
        setDescribedByIds(ids: string[]) {
          this.describedBy = ids.join(' ');
        }
      
        constructor(fb: FormBuilder, private fm: FocusMonitor, private elRef:
          ElementRef,
          renderer: Renderer2, public ngControl: NgControl, ) {
          fm.monitor(elRef.nativeElement, renderer, true).subscribe(origin => {
            this.focused = !!origin;
            this.stateChanges.next();
          });
          ngControl.valueAccessor = this;
          this.parts = fb.group({
            'days': '',
            'hours': '',
            'minutes': '',
          });
        }
      
        ngOnInit() {}
      
        ngOnDestroy() {
          this.stateChanges.complete();
          this.fm.stopMonitoring(this.elRef.nativeElement);
        }
        get empty() {
          let n = this.parts.value;
          return !n.area && !n.exchange && !n.subscriber;
        }
        private propagateChange = (_: any) => {};
      
        public writeValue(a: any) {
          if (a !== undefined) {
            this.parts.setValue({
              days: a.substring(a.lastIndexOf("P") + 1, a.lastIndexOf("D")),
              hours: a.substring(a.lastIndexOf("T") + 1, a.lastIndexOf("H")),
              minutes: a.substring(a.lastIndexOf("H") + 1, a.lastIndexOf("M"))
            });
          }
        };
        public registerOnChange(fn: any) {
          this.propagateChange = fn;
        }
      
        // not used, used for touch input
        public registerOnTouched() {}
        // change events from the textarea
      }

      垫定制外形field.html

      < div[formGroup]="parts">
        < input class="area" formControlName="days" size="3">
          < span> & ndash; < /span>
          < input class="exchange" formControlName="hours" size="3">
          < span> & ndash; < /span>
          < input class="subscriber" formControlName="minutes" size="3">
        < /div>

2 个答案:

答案 0 :(得分:4)

首先,我修改了你的写入值fn,因为它在null的情况下对我不起作用:

public writeValue(a: string) {
    if (a && a !== '') {
      this.parts.setValue({
        days: a.substring(a.lastIndexOf('P') + 1, a.lastIndexOf('D')),
        hours: a.substring(a.lastIndexOf('T') + 1, a.lastIndexOf('H')),
        minutes: a.substring(a.lastIndexOf('H') + 1, a.lastIndexOf('M'))
      });
    }
  }

自定义组件模板保持不变。 我以这样的示例形式使用此组件:

测试表格

&#13;
&#13;
<div>
  <form #form="ngForm" [formGroup]="productForm">
    <mat-form-field>
      <product-team-input formControlName="productTeam" placeholder="P12D" ></product-team-input>
    </mat-form-field>
  </form>
  {{ form.value | json }}
</div>
     
&#13;
&#13;
&#13;

Simple AppComponent设置控件的默认值(求解点1),还包含一个简单的单击方法,可以模拟从服务器加载数据时的情况。

 @Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.css']
})
export class AppComponent {
  title = 'app';

  data: string;
  productForm: FormGroup;

  constructor(private fb: FormBuilder) {
    this.productForm = this.fb.group({
      productTeam: [null] // can be value like P12DT2H231M as well
    });
  }
  onClick() {
    this.productForm.controls['productTeam'].patchValue('P12DT2H231M');
  }
}

使用此设置,您已经可以使用您的组件,并且将设置默认值,但您还没有收到任何更改。

为了接收父表单中的更改,您需要使用在组件中注册的propagateChange回调来传播它们(以解决第2点)。 因此,组件代码的主要更改是订阅组件内部表单组的更改,您将从中将其传播到上一级:

this.parts = fb.group({
  'days': '',
  'hours': '',
  'minutes': '',
});

this.subs.push(this.parts.valueChanges.subscribe((value: Duration) => {
  this.propagateChange(value);
}));

我还将在这里留下product-team-field.component.ts和Duration类的完整代码以防万一:

<强> duration.ts

class Duration {
      constructor(public days: number, public hours: number, public minutes:
        number) {
         }

    toString() {
      return 'P' + (this.days || 0) + 'DT' + (this.hours || 0) +
      'H' + (this.minutes || 0) + 'M';
    }

}

<强>产品团队field.component.ts     

@Component({
  selector: 'product-team-input',
  templateUrl: './product-team-field.component.html',
  styleUrls: ['./product-team-field.component.css'],
  providers: [{
    provide: MatFormFieldControl,
    useExisting: ProductTeamControl
  },
  {
    provide: NG_VALUE_ACCESSOR,
    useExisting: forwardRef(() => ProductTeamControl),
    multi: true
  }]
})
export class ProductTeamControl implements OnInit, OnDestroy, ControlValueAccessor, MatFormFieldControl<Duration> {
  static nextId = 0;
  ngControl = null;
  parts: FormGroup;
  focused = false;
  stateChanges = new Subject<void>();
  errorState = false;
  controlType = 'product-team-input';
  private _disabled = false;
  private _required = false;
  private _placeholder: string;

  @Input()
  get required() {
    return this._required;
  }
  set required(req) {
    this._required = coerceBooleanProperty(req);
    this.stateChanges.next();
  }
  @Input()
  get disabled() {
    return this._disabled;
  }
  set disabled(dis) {
    this._disabled = coerceBooleanProperty(dis);
    this.stateChanges.next();
  }
  @Input()
  get placeholder() {
    return this._placeholder;
  }
  set placeholder(plh) {
    this._placeholder = plh;
    this.stateChanges.next();
  }
  @Input()
  get value(): Duration | null {
    const n = this.parts.value;
    if (n.days && n.hours && n.minutes) {
      return new Duration(n.days, n.hours, n.minutes);
    }
    return null;
  }

  set value(duration: Duration | null) {
    duration = duration || new Duration(0, 0, 0);
    this.writeValue(duration.toString());
    this.stateChanges.next();
  }
  onContainerClick(event: MouseEvent) {
    if ((event.target as Element).tagName.toLowerCase() !== 'input') {
      this.elRef.nativeElement.querySelector('input').focus();
    }
  }

  @HostBinding() id = `${this.controlType}-${ProductTeamControl.nextId++}`;

  @HostBinding('class.floating')
  get shouldPlaceholderFloat() {
    return this.focused || !this.empty;
  }

  @HostBinding('attr.aria-describedby') describedBy = '';

  setDescribedByIds(ids: string[]) {
    this.describedBy = ids.join(' ');
  }

  private subs: Subscription[] = [];

  constructor(
    private fb: FormBuilder,
    private fm: FocusMonitor,
    private elRef: ElementRef,
    renderer: Renderer2) {

    this.subs.push(fm.monitor(elRef.nativeElement, renderer, true).subscribe(origin => {
      this.focused = !!origin;
      this.stateChanges.next();
    }));

    this.parts = fb.group({
      'days': '',
      'hours': '',
      'minutes': '',
    });

    this.subs.push(this.parts.valueChanges.subscribe((value: Duration) => {
      this.propagateChange(value);
    }));
  }

  ngOnInit() { }

  ngOnDestroy() {
    this.stateChanges.complete();
    this.subs.forEach(s => s.unsubscribe());
    this.fm.stopMonitoring(this.elRef.nativeElement);
  }
  get empty() {
    const n = this.parts.value;
    return !n.area && !n.exchange && !n.subscriber;
  }

  private propagateChange = (_: any) => { };

  public writeValue(a: string) {
    if (a && a !== '') {
      this.parts.setValue({
        days: a.substring(a.lastIndexOf('P') + 1, a.lastIndexOf('D')),
        hours: a.substring(a.lastIndexOf('T') + 1, a.lastIndexOf('H')),
        minutes: a.substring(a.lastIndexOf('H') + 1, a.lastIndexOf('M'))
      });
    }
  }
  public registerOnChange(fn: any) {
    this.propagateChange = fn;
  }

  public registerOnTouched(fn: any): void {
    return;
  }

  public setDisabledState?(isDisabled: boolean): void {
    this.disabled = isDisabled;
  }
}

答案 1 :(得分:0)

那些不使用表单构建器或反应式表单的人,请在输入字段中使用“ ngDefaultControl”作为属性。