Angular 2. Как ng-bootstrap предоставляет NgbRadioGroup и NgbButtonLabel своей директиве NgbRadio?

Вот код этикетки:

import {Directive} from '@angular/core';

@Directive({
  selector: '[ngbButtonLabel]',
  host:
      {'[class.btn]': 'true', '[class.active]': 'active', '[class.disabled]': 'disabled', '[class.focus]': 'focused'}
})
export class NgbButtonLabel {
  active: boolean;
  disabled: boolean;
  focused: boolean;
}

а вот код радиокнопки:

import {Directive, forwardRef, Input, Renderer2, ElementRef, OnDestroy} from '@angular/core';
import {ControlValueAccessor, NG_VALUE_ACCESSOR} from '@angular/forms';

import {NgbButtonLabel} from './label';

const NGB_RADIO_VALUE_ACCESSOR = {
  provide: NG_VALUE_ACCESSOR,
  useExisting: forwardRef(() => NgbRadioGroup),
  multi: true
};

let nextId = 0;

/**
 * Easily create Bootstrap-style radio buttons. A value of a selected button is bound to a variable
 * specified via ngModel.
 */
@Directive({
  selector: '[ngbRadioGroup]',
  host: {'data-toggle': 'buttons', 'role': 'group'},
  providers: [NGB_RADIO_VALUE_ACCESSOR]
})
export class NgbRadioGroup implements ControlValueAccessor {
  private _radios: Set<NgbRadio> = new Set<NgbRadio>();
  private _value = null;
  private _disabled: boolean;

  get disabled() { return this._disabled; }
  set disabled(isDisabled: boolean) { this.setDisabledState(isDisabled); }

  /**
   * The name of the group. Unless enclosed inputs specify a name, this name is used as the name of the
   * enclosed inputs. If not specified, a name is generated automatically.
   */
  @Input() name = `ngb-radio-${nextId++}`;

  onChange = (_: any) => {};
  onTouched = () => {};

  onRadioChange(radio: NgbRadio) {
    this.writeValue(radio.value);
    this.onChange(radio.value);
  }

  onRadioValueUpdate() { this._updateRadiosValue(); }

  register(radio: NgbRadio) { this._radios.add(radio); }

  registerOnChange(fn: (value: any) => any): void { this.onChange = fn; }

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

  setDisabledState(isDisabled: boolean): void {
    this._disabled = isDisabled;
    this._updateRadiosDisabled();
  }

  unregister(radio: NgbRadio) { this._radios.delete(radio); }

  writeValue(value) {
    this._value = value;
    this._updateRadiosValue();
  }

  private _updateRadiosValue() { this._radios.forEach((radio) => radio.updateValue(this._value)); }
  private _updateRadiosDisabled() { this._radios.forEach((radio) => radio.updateDisabled()); }
}


/**
 * Marks an input of type "radio" as part of the NgbRadioGroup.
 */
@Directive({
  selector: '[ngbButton][type=radio]',
  host: {
    '[checked]': 'checked',
    '[disabled]': 'disabled',
    '[name]': 'nameAttr',
    '(change)': 'onChange()',
    '(focus)': 'focused = true',
    '(blur)': 'focused = false'
  }
})
export class NgbRadio implements OnDestroy {
  private _checked: boolean;
  private _disabled: boolean;
  private _value: any = null;

  /**
   * The name of the input. All inputs of a group should have the same name. If not specified,
   * the name of the enclosing group is used.
   */
  @Input() name: string;

  /**
   * You can specify model value of a given radio by binding to the value property.
   */
  @Input('value')
  set value(value: any) {
    this._value = value;
    const stringValue = value ? value.toString() : '';
    this._renderer.setProperty(this._element.nativeElement, 'value', stringValue);
    this._group.onRadioValueUpdate();
  }

  /**
   * A flag indicating if a given radio button is disabled.
   */
  @Input('disabled')
  set disabled(isDisabled: boolean) {
    this._disabled = isDisabled !== false;
    this.updateDisabled();
  }

  set focused(isFocused: boolean) {
    if (this._label) {
      this._label.focused = isFocused;
    }
  }

  get checked() { return this._checked; }

  get disabled() { return this._group.disabled || this._disabled; }

  get value() { return this._value; }

  get nameAttr() { return this.name || this._group.name; }

  constructor(
      private _group: NgbRadioGroup, private _label: NgbButtonLabel, private _renderer: Renderer2,
      private _element: ElementRef) {
    this._group.register(this);
  }

  ngOnDestroy() { this._group.unregister(this); }

  onChange() { this._group.onRadioChange(this); }

  updateValue(value) {
    this._checked = this.value === value;
    this._label.active = this._checked;
  }

  updateDisabled() { this._label.disabled = this.disabled; }
}

Обратите внимание, что

@Directive({
  selector: '[ngbButton][type=radio]',
  host: {
    '[checked]': 'checked',
    '[disabled]': 'disabled',
    '[name]': 'nameAttr',
    '(change)': 'onChange()',
    '(focus)': 'focused = true',
    '(blur)': 'focused = false'
  }
})

не имеет раздела провайдеров, но в конструкторе есть NgbRadioGroup и NgbButtonLabel. Более того, при использовании директив, оставляя ngbButtonLabel следующим образом:

<div [(ngModel)]="model" ngbRadioGroup>
  <label>
    <input ngbButton type="radio" name="radio" [value]="values[0]"/> {{ values[0] }}
  </label>
</div>

вызывает отсутствие поставщика для NgbButtonLabel! ошибка. Какую часть декларации я пропустил? Вот ссылка на их полный репозиторий: https://github.com/ng-bootstrap/ng-bootstrap


person Tyzone34    schedule 20.09.2017    source источник


Ответы (1)


Пакет ng-bootstrap ожидает, что элемент

<input ngbButton type="radio" ...>

, для которого вы указали директиву NgbRadio, будет иметь родительский элемент, для которого вы указали директиву NgbButtonLabel.

Таким образом, ваш шаблон должен выглядеть так:

<label ngbButtonLabel> <======== add ngbButtonLabel attribute
  <input ngbButton type="radio" name="radio" [value]="values[0]"/> {{ values[0] }}
</label>

Чтобы понять, почему это так, вам нужно знать как angular получает зависимости от иерархического дерева элементов.

Допустим, у нас есть следующий шаблон в нашем корневом компоненте:

app.component.html

<div dirA>
  <comp-b dirB>
    <span dirC>
      <i dirD></i>
    </span>
  </comp-b>
</div>

и следующий набор директив:

@Directive({
  selector: '[dirA]',
  providers: [{ provide: 'A', useValue: 'dirA provider' }]
})
export class DirA {}

@Component({
  selector: 'comp-b',
  template: '<ng-content></ng-content>',
  providers: [{ provide: 'B', useValue: 'comp-b provider'}]
})
export class ComponentB {}

@Directive({ selector: 'dirB' })
export class DirB {}

@Directive({ selector: 'dirC' })
export class DirC {}

@Directive({ selector: 'dirD' })
export class DirD {
  constructor(private dirB: DirB) {}
}

Примечание. private dirB: DirB в вашем случае похоже на private _label: NgbButtonLabel.

Компилятор Angular создает фабрику представлений для нашего шаблона:

введите описание изображения здесь

Примечание. я использовал новую опцию preserveWhitespaces: false для компонента, поэтому мы не видим textDef на заводе.

Затем angular создает ViewDefinition из этой фабрики, а также создает экземпляры поставщиков для элементов хоста.

Куда компилятор angular берет провайдеров?

Главное, что вы должны знать, это то, что каждая директива предоставляет собственный токен:

Таким образом, провайдеры здесь могут выглядеть следующим образом:

<div dirA>               [DirA]
  <comp-b dirB>          [ComponentB, DirB]
    <span dirC>          [DirC] 
      <i dirD></i>       [DirD]
    </span>
  </comp-b>
</div>

Следующее правило заключается в том, что поставщики, которые мы объявляем в метаданных директивы (массив providers), также будут добавлены к поставщикам элементов хоста:

<div dirA>               [DirA, { provide: 'A', useValue: 'dirA provider' }]
  <comp-b dirB>          [ComponentB, DirB, { provide: 'B', useValue: 'comp-b provider'}]
    <span dirC>          [DirC] 
      <i dirD></i>       [DirD]
    </span>
  </comp-b>
</div>

Теперь angular пытается получить провайдера для директивы DirB

@Directive({ selector: 'dirD' })
export class DirD {
  constructor(private dirB: DirB) {}
}

Механизм разрешения зависимостей Angular начинается с узла <i dirD></i> и доходит до узла <div dirA>:

              null or throw error
                    /\
                 @NgModule
                    /\
                  my-app
<div dirA>          /\     [DirA, { provide: 'A', useValue: 'dirA provider' }]
  <comp-b dirB>     /\     [ComponentB, DirB, { provide: 'B', useValue: 'comp-b provider'}]
    <span dirC>     /\     [DirC]   
      <i dirD></i>  /\     [DirD]  
    </span>
  </comp-b>
</div>

Итак, angular найдет DirB провайдера на <comp-b dirB> хост-элементе. Мы могли бы подумать, что angular сделает три шага, чтобы получить поставщика DirB, НО действительно angular использует прототипическое наследование для определения поставщиков для элементов.

введите описание изображения здесь

Таким образом, наше дерево будет выглядеть так:

              null or throw error
                    /\
                 @NgModule
                    /\
                  my-app
<div dirA>          /\     [
                             DirA, { provide: 'A', useValue: 'dirA provider' }
                           ]
  <comp-b dirB>     /\     [
                             ComponentB, 
                             DirB, { provide: 'B', useValue: 'comp-b provider'}, 
                             DirA, { provide: 'A', useValue: 'dirA provider' }
                           ]
    <span dirC>     /\     [
                             DirC, ComponentB, 
                             DirB, { provide: 'B', useValue: 'comp-b provider'}, 
                             DirA, { provide: 'A', useValue: 'dirA provider' }
                           ]  
      <i dirD></i>  /\     [
                             DirD, DirC, ComponentB, 
                             DirB, { provide: 'B', useValue: 'comp-b provider'}, 
                             DirA, { provide: 'A', useValue: 'dirA provider' }
                           ]  
    </span>
  </comp-b>
</div>

Как мы видим, на самом деле angular использует только один шаг, чтобы найти DirB провайдера из <i dirD></i> элемента хоста.

person yurzui    schedule 23.09.2017