ng-dynamic-component
ng-dynamic-component copied to clipboard
Use FormControlDirective
It's currently not possible to apply FormControlDirective to a dynamic component. It fails during the createDirective with the following error:
ERROR Error: StaticInjectorError(AppModule)[Array]:
StaticInjectorError(Platform: core)[Array]:
NullInjectorError: No provider for Array!
at NullInjector.push../node_modules/@angular/core/fesm5/core.js.NullInjector.get (core.js:3228)
at resolveToken (core.js:3473)
at tryResolveToken (core.js:3417)
at StaticInjector.push../node_modules/@angular/core/fesm5/core.js.StaticInjector.get (core.js:3314)
at resolveToken (core.js:3473)
at tryResolveToken (core.js:3417)
at StaticInjector.push../node_modules/@angular/core/fesm5/core.js.StaticInjector.get (core.js:3314)
at resolveNgModuleDep (core.js:19784)
at NgModuleRef_.push../node_modules/@angular/core/fesm5/core.js.NgModuleRef_.get (core.js:20473)
at resolveNgModuleDep (core.js:19784)
Its seems it gets tricked by the peculiar directive constructor and want to resolve to the arrays. I'm not familiar with the reflect method which is at play but it seems it ignore the Injection Tokens in the metadata sur as @Inject(NG_VALIDATORS) see https://github.com/angular/angular/blob/cabf1c71059140a2f7855790a9e8ea79b6745f3a/packages/forms/src/directives/reactive_directives/form_control_directive.ts#L166
Hi, you are right, currently @Inject is not supported in dynamic directives. That is why that error occurs.
It should be possible to implement it without major issues so I will have a look into this soon.
Apparently the global Reflect is going towards deprecation. I wonder what's the right replacement for it.
I was able to make it work without it by having the directive registered as a constructor provider in my module, and creating a childInjector as follows
private createDirective<T>(dirType: Type<T>): T {
const childInjector = Injector.create({
providers: [
{
provide: ViewContainerRef,
useValue: this.hostVcr,
}
],
parent: this.injector,
});
return childInjector.get(dirType);
}
However once done I've hit another problem , it seems the current Input binding does not honor, bindingPropertyName on the @Input decorator ex:
@Input('formControl') form !: FormControl;
I hacked my way around the io service and change updateInputs as follows:
private updateInputs(isFirstChange = false) {
if (isFirstChange) {
this._updateCompFactory();
}
const compInst = this._componentInst;
let inputs = this._inputs;
if (!inputs || !compInst) {
return;
}
inputs = this._resolveInputs(inputs);
const properties = this._compInjector.componentRef.componentType['__prop__metadata__'];
Object.keys(properties).forEach(p => {
const metadata = properties[p] as any[];
const propertyName = metadata.filter(x => !!x.bindingPropertyName);
if (propertyName.length) {
compInst[p] = inputs[propertyName[0].bindingPropertyName];
} else {
compInst[p] = inputs[p]
}
});
// Object.keys(inputs).forEach(p => {(compInst[p] = inputs[p])});
this.notifyOnInputChanges(this._lastInputChanges, isFirstChange);
}
However it seems now that ngOnChanges is not triggered.
Other findings, my hack on the createDirective is not good enough as the injector I'm using is based on the root injector. It has to be the injector from the dynamically created component to get the providers it might register (such as NG_VALUE_ACCESSOR)
@sandorfr great findings. I did not know that directives still have inputs binding information at runtime, that definitely can be used to support inputs renaming.
Regarding the FormControl directive I would advise against using it as dynamic directive because there are limits of what is supported and that directive is not simple one.
You can always wrap your dynamic component into another component and apply any directive from template which will make them static and thus will 100% work.
The main limitation with dynamic directives is injectors: on one hand dynamic component should be able to inject any directive that is attached to it, on the other hand any directive should be able to inject the host component it is attached to it.
This creates circular dependency and without knowing in advance all injectors it is impossible to meet those requirements, unless the injector itself supports dynamic addition of tokens for both component and directives.
I will explore that also but right now it is definitely not possible, that's why I advise you not to use complex dynamic directives.
on the other hand any directive should be able to inject the host component it is attached to it.
By curiosity, In which case would it happen?
For example if your directive has specific selector to work only with specific component (like my-component[myDirective]) then you can easily inject that component inside of your directive and do smth with it like so:
@Directive({ selector: 'my-component[myDirective]'})
class MyDirective {
constructor(@Host() myComponent: MyComponent) {
// do smth with `myComponent` here...
}
}
EDIT: I added @Host decorator to that injection because it will guarantee that you will only get MyComponent from the element directive is attached to. Without it angular will traverse the whole DI chain and you might get that from other places.
I don't get where you see a circular injection pattern in there. It's still uses the normal hierarchy of injectors. it's not a new instance of the component which is created at this stage but the instance which already exists in the parent injector.
The problem is that component will already exist by the time dynamic directive is gonna be created.
And what it means that component's injector was already created without that directive being available in it. And to instantiate dynamic directive component has to exist, so you can see how it is that it's not possible to provide both entities for each other - the chicken egg problem in a nutshell.
So without dynamic injector there is nothing we can do about it.
Sorry by advance if I come out strong, but I'm really trying to help and also understand what has to be done to make it work. If I can come up with a good solution. I'd like to submit a PR.
The problem is that component will already exist by the time dynamic directive is gonna be created.
I don't get why it's a problem. But yes the component is instantiated first
And what it means that component's injector was already created without that directive being available in it.
It's the normal behavior of directive, they are not created in the component but on top of it and "attached"
So without dynamic injector there is nothing we can do about it. I don't get the need for a dynamic injector here.
ViewContainerRef.createComponent returns a ComponentRef<MyComponent> which include a child injector. This injector has the reference to MyComponent, but also any provider defined at the component level (such as NG_VALUE_ACCESSOR which is the corner stone of the FormControlDirective).
This child injector can then be used to instantiate the the Directive and things like you mentioned @Host() myComponent: MyComponent will be honored.
I've experimented a little bit more outside of the library to get a better understanding without the complexity of everything else it provides and the following works.
initializeComponent() {
const factory = this.resolver.resolveComponentFactory(DynamiteComponent);
const ref = this.vc.createComponent(factory);
const instance = ref.injector.get(DynamiteComponent, InjectFlags.Optional);
console.log("component", instance);
const valueAccessor = ref.injector.get(
NG_VALUE_ACCESSOR,
InjectFlags.Optional
);
console.log("valueAccessor", valueAccessor);
const flags: any = InjectFlags.Optional | InjectFlags.Self;
const validators = ref.injector.get(NG_VALIDATORS, null, flags);
console.log("validators", validators);
console.log("flags", flags);
const formControl = new FormControlDirective(
ref.injector.get(NG_VALIDATORS, null, flags) as any,
ref.injector.get(NG_ASYNC_VALIDATORS, null, flags) as any,
ref.injector.get(NG_VALUE_ACCESSOR, null, flags) as any,
null
);
console.log("formControl", formControl);
formControl.form = this.form;
formControl.ngOnChanges({
form: new SimpleChange(undefined, formControl.form, true)
});
this.component = ref;
}

Hello, i have the same error with the NgModel directive
Hi @gund, any update on this issue?
Hey, sorry I did not look into this issue for quite some time.
Again as I said before I do not see how we can resolve this problem as it is recursive - you need to have component created in order to resolve directives, but you also need to have directives to create component...
We have to analyse dependencies of component and directives before their creation in order to prioritise the order of instantiation - and it looks a bit complex to me right now.
Even if you look at the latest code snip from @sandorfr you can see the problem - he first creates the component, and then creates a directive manually from the injector of the new component, but if component will have to access that directive - it will fail as it was not provided...
@sandorfr can use my lib for dynamic components : https://github.com/zircon63/ng-torque, this resolve your problem
I'm closing this issue as it's outdated and no fix is planned for it. If you want to reopen the discussion please feel free to open a new issue.