Way to inject angular FormBuilder service to dynamic component

1.4k views Asked by At

I'm trying to inject the FormBuilder service to a dynamic component this way:

Template:

...
<div #vc></div>
...

Component:


@ViewChild('vc', { read: ViewContainerRef }) _container: ViewContainerRef;

...

  constructor(private fb: FormBuilder,
    private componentFactoryResolver: ComponentFactoryResolver,
    private _compiler: Compiler, private _injector: Injector,
    private _m: NgModuleRef<any>) {
  }

...

ngAfterViewInit() {
    let  allPms: any[] = null;
    let template = '';

    // construct template on the fly

    const wTypes = this._f.w_type;

    for (const plug of this._plugs) {
      if (plug.name === wTypes) {
        allPms = plug.params;
      }
    }

    for (const pm of allPms) {
      if (pm.type === 'str') {
        template = template + `
        <div class="form-group row">
          <label class="col-sm-3 col-form-label"><strong>` + pm.name + `</strong></label>
          <div class="col-sm-8">
            <input class="form-control" name="` + pm.name + `" type="text"
              formControlName="` + pm.name + `">
          </div>
        </div>
        `;
      }
    }

    // add field for each pm

    let injector1 = Injector.create([
      {
        provide: 'FormBuilder',
        useValue: FormBuilder
      }
    ]);

    const tmpCmp = Component({ template: template, styles: [`label {
      width: 128px;
      margin: 0px 8px;
    }`] })(class {
      constructor(private fb: FormBuilder) {
      }
    });
    const tmpModule = NgModule({ declarations: [tmpCmp] })(class {
    });

    this._compiler.compileModuleAndAllComponentsAsync(tmpModule)
      .then((factories) => {
        const f = factories.componentFactories[0];
        this.cmpRef = f.create(injector1, [], null, this._m);
        this.cmpRef.instance.name = 'B component';
        this._container.insert(this.cmpRef.hostView);
      })
  }

Doing so, I got this error:

  ERROR Error: Can't resolve all parameters for class_1: (?).
    at syntaxError (compiler.js:1021)
    at CompileMetadataResolver.push../node_modules/@angular/compiler/fesm5/compiler.js.CompileMetadataResolver._getDependenciesMetadata (compiler.js:10922)
    at CompileMetadataResolver.push../node_modules/@angular/compiler/fesm5/compiler.js.CompileMetadataResolver._getTypeMetadata (compiler.js:10815)
    at CompileMetadataResolver.push../node_modules/@angular/compiler/fesm5/compiler.js.CompileMetadataResolver.getNonNormalizedDirectiveMetadata (compiler.js:10434)
    at CompileMetadataResolver.push../node_modules/@angular/compiler/fesm5/compiler.js.CompileMetadataResolver.loadDirectiveMetadata (compiler.js:10296)
    at compiler.js:23883
    at Array.forEach (<anonymous>)
    at compiler.js:23882
    at Array.forEach (<anonymous>)
    at JitCompiler.push../node_modules/@angular/compiler/fesm5/compiler.js.JitCompiler._loadModules (compiler.js:23879)
View_testComponent_17 @ testComponent.html:72
push../node_modules/@angular/core/fesm5/core.js.DebugContext_.logError @ core.js:11306
push../node_modules/@angular/core/fesm5/core.js.ErrorHandler.handleError @ core.js:1719
(anonymous) @ core.js:4578
./node_modules/zone.js/dist/zone.js.ZoneDelegate.invoke @ zone.js:391
./node_modules/zone.js/dist/zone.js.Zone.run @ zone.js:150
push../node_modules/@angular/core/fesm5/core.js.NgZone.runOutsideAngular @ core.js:3779
push../node_modules/@angular/core/fesm5/core.js.ApplicationRef.tick @ core.js:4578
(anonymous) @ core.js:4462
./node_modules/zone.js/dist/zone.js.ZoneDelegate.invoke @ zone.js:391
onInvoke @ core.js:3820

Trying to add a custom injector did not help:

 let injector1 = Injector.create([
  {
    provide: 'FormBuilder',
    useValue: FormBuilder
  }
]);

Here is the Stackblitz that reproduces the issue:

angular-dynamic-components-example

enter image description here

Is there any way to resolve this issue ?

1

There are 1 answers

0
yurzui On BEST ANSWER

The ? sign in errors like Can't resolve all parameters for class_1: (?) means that Angular can't resolve type of parameter passed to constructor. In other words, reflector can't recognize that private fb: FormBuilder parameter has FormBuilder type because type dissappers after TypeScript compilation.

In order to tell TS compiler that it should keep this type you need to rewrite this class definition to version with decorator like:

@Component({
  template: template,
  styles: [
    `
      label {
        width: 128px;
        margin: 0px 8px;
      }
    `
  ]
})
class tmpCmp {
  constructor(private fb: FormBuilder) {}
}

Forked Stackblitz

This will be compiled to:

tmpCmp = __decorate([
    core_1.Component({
        template: template,
        styles: [
            `
label {
width: 128px;
margin: 0px 8px;
}
`
        ]
    }),
    __metadata("design:paramtypes", [typeof (_a = typeof forms_1.FormBuilder !== "undefined" && forms_1.FormBuilder) === "function" ? _a : Object])
], tmpCmp);

where you can notice __metadata("design:paramtypes" part which is responsible for providing information to Angular reflector.

There are other ways of solving it.

Static parameters

const tmpCmp = Component({
  ...
})(
  class {
    constructor(private fb: FormBuilder) {}

    static parameters = [ FormBuilder ]
  }
);

Forked Stackblitz

Static ctorParameters method

const tmpCmp = Component({
 ...
})(
  class {
    constructor(private fb: FormBuilder) {}

    static ctorParameters = () => [{ type: FormBuilder} ]
  }
);

Forked Stackblitz