Reputation: 21134
I have a @Component
which uses the @Attribute
decorator:
@Component(...)
export class ButtonComponent {
@Input()
disabled: boolean;
constructor(@Attribute('type') public type: ButtonType = 'default') {}
}
How can I correctly set its value when dynamically creating it?
const factory = this.factoryResolver.resolveComponentFactory(ButtonComponent);
Just by setting its instance field?
instance.type = 'primary';
But what about change detection? @Attribute
isn't checked after creation.
Upvotes: 2
Views: 847
Reputation: 3043
I think you'd need a combination of @Input()
for change detection and @HostBinding('attr.type')
for keeping the native attribute up to date
@Component({
selector: 'button[app-button]',
template: '<ng-content></ng-content><span> type: {{ type }}</span>',
styleUrls: ['./button.component.css'],
//inputs: ['type'],
host: {
//'[attr.type]': 'type'
}
})
export class ButtonComponent implements OnInit {
@Input()
@HostBinding('attr.type')
public type: string;
constructor(
@Attribute('type') type?: string
) {
// if created in template set initial
if (!!type) {
this.type = type;
}
}
ngOnInit() {
console.log(this)
}
}
then
public ngOnInit(): void {
const buttonFactory = this.resolver.resolveComponentFactory(ButtonComponent);
const buttonRef = this.view.createComponent(buttonFactory, undefined, this.injector);
buttonRef.instance.type = 'submit';
}
In the stackblitz you can see ngOnInit
and the native element in the debug tools has type submit:
https://stackblitz.com/edit/angular-attribute-decorator-and-componentfactoryresolver
Upvotes: 1
Reputation: 54771
How can I correctly set its value when dynamically creating it?
You can't.
The @Attribute()
decorator is a special feature used only by the AOT compiler. When the template is compiled into TypeScript the factory function for the component (from the perspective of the parent template) passes the attribute value to the component constructor. There has to be a parent DOM element that contains the DOM element for the new child component, and the attributes are read from that element.
The code written by the AOT is different from the component factories you use to dynamically create components, and since the dependency injector is by-passed for @Attribute()
values and there is no data binding. The execution time for using attributes in the constructor is much faster. I think that's something people often overlook and it's good to know.
The only thing you can do is support both attributes and dependency injector in the component, and use the value from either in the constructor.
Here is an example of a button component that takes an attribute or DI value.
export const MESSAGE_TOKEN: InjectionToken<string> = new InjectionToken<string>('MESSAGE_TOKEN');
@Component({
selector: 'app-button',
template: '<button>{{message}}</button>'
})
export class ButtonComponent {
public message: string;
public constructor(@Attribute('message') attrMessage: string,
@Inject(MESSAGE_TOKEN) @Optional() tokenMessage: string) {
this.message = attrMessage || tokenMessage;
}
}
You would use it normally in the template as a <app-button message="Hello World></app-button>
, but you would use the MESSAGE_TOKEN
when you had to create it dynamically.
@Component({
selector: 'app-root',
template: ``
})
export class AppComponent implements OnInit {
public constructor(private _view: ViewContainerRef,
private _resolver: ComponentFactoryResolver,
private _injector: Injector) {
}
public ngOnInit(): void {
const button = this._resolver.resolveComponentFactory(ButtonComponent);
const injector = Injector.create({
providers: [
{provide: MESSAGE_TOKEN, useValue: 'Hello World'}
],
parent: this._injector
});
this._view.createComponent(button, undefined, injector);
}
}
When I first looked into this question. I assumed that the @Attribute()
used the dependency injector and was an alternative for the @Inject()
, but upon reviewing the source code in Angular I discovered this was coded into how templates are rendered. It makes sense to me, because this would have little overhead for performance.
Now, this is in reference to the current render engine in Angular. I have not had time to review the source code for Ivy, but how it handles attributes in that render appears to be done differently. I just haven't had to time to review this part of it.
If anyone else can figure out how to get @Attribute()
to work with component factories, then please let me know in the comments.
Upvotes: 2