Here is the label code:
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;
}
and here is the radio button code:
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; }
}
Notice that the
@Directive({
selector: '[ngbButton][type=radio]',
host: {
'[checked]': 'checked',
'[disabled]': 'disabled',
'[name]': 'nameAttr',
'(change)': 'onChange()',
'(focus)': 'focused = true',
'(blur)': 'focused = false'
}
})
has no providers section, but the constructor has a NgbRadioGroup and NgbButtonLabel. Further more, when using the directives, leaving off the ngbButtonLabel like this:
<div [(ngModel)]="model" ngbRadioGroup>
<label>
<input ngbButton type="radio" name="radio" [value]="values[0]"/> {{ values[0] }}
</label>
</div>
causes a No provider for NgbButtonLabel! error. What piece of declaration am I missing? Here is a link to their full repository: https://github.com/ng-bootstrap/ng-bootstrap
ng-bootstrap package expects that the element
<input ngbButton type="radio" ...>
, on which you provided NgbRadio
directive, will have parent element on which you provided NgbButtonLabel
directive.
So your template should looks like:
<label ngbButtonLabel> <======== add ngbButtonLabel attribute
<input ngbButton type="radio" name="radio" [value]="values[0]"/> {{ values[0] }}
</label>
To understand why this is so you need to know how angular gets dependencies from hierarchical tree of elements.
Let's say we have the following template in our root component:
app.component.html
<div dirA>
<comp-b dirB>
<span dirC>
<i dirD></i>
</span>
</comp-b>
</div>
and the following set of directives:
@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) {}
}
Note: private dirB: DirB
is like private _label: NgbButtonLabel
in your case
Angular compiler creates view factory for our template:
Note: i used new preserveWhitespaces: false
option on component so we don't see textDef
in the factory.
Then angular creates ViewDefinition from this factory and also instantiates providers for host elements.
Main thing you should know is that each directive provides its own token:
So providers here could look as follows:
<div dirA> [DirA]
<comp-b dirB> [ComponentB, DirB]
<span dirC> [DirC]
<i dirD></i> [DirD]
</span>
</comp-b>
</div>
The following rule is providers that we are declaring within directive metadata(providers
array) will also be added to host element 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>
Now angular is trying to get provider for DirB
directive
@Directive({ selector: 'dirD' })
export class DirD {
constructor(private dirB: DirB) {}
}
Angular dependency resolution mechanism starts with <i dirD></i>
node and goes up to <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>
So angular will find DirB
provider on <comp-b dirB>
host element. We might think that angular will make three steps up to get DirB
provider BUT
Indeed angular uses prototypical inheritance to define providers on elements.
This way our tree will look like:
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>
As we can see actually angular uses only one step to find DirB
provider from <i dirD></i>
host element.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With