3

I have created some custom components, each of which implements ControlValueAccessor, in order that they may function as controls within a FormGroup. Usually, the only thing left to do would be to add the formControlName directive to each custom component in HTML.

However, I'm appending these components to the form at runtime using this technique.

My problem is that I cannot register these components with a containing FormGroup because I cannot declare the formControlName directive (or any directive, for that matter) with a dynamically added control.

Has anyone else discovered how this might be possible? At the moment, I'm wrapping each control inside another component so that I can use the formControlName directive but this is just too ugly and labor intensive.


Below is a stripped down example of what I've already implemented as a standalone Angular2 app, which shows a component (CustomComponent) being programatically added on startup. In order to bind CustomComponent to the FormGroup, I've had to create CustomContainerComponent which I'd prefer to avoid.

import {
  Component, ReflectiveInjector, ViewContainerRef, Compiler, NgModule, ModuleWithComponentFactories,
  OnInit, ViewChild, forwardRef
} from '@angular/core';
import {BrowserModule} from '@angular/platform-browser';
import {NG_VALUE_ACCESSOR, ControlValueAccessor, FormGroup, FormBuilder, ReactiveFormsModule} from '@angular/forms';


export class AbstractValueAccessor<T> implements ControlValueAccessor {
  _value: T;
  get value(): T {
    return this._value;
  };

  set value(v: T) {
    if (v !== this._value) {
      this._value = v;
      this.onChange(v);
    }
  }

  writeValue(value: T) {
    this._value = value;
    this.onChange(value);
  }

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

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

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

}

export function MakeProvider(type: any) {
  return {
    provide: NG_VALUE_ACCESSOR,
    useExisting: forwardRef(() => type),
    multi: true
  };
}

@Component({
  selector: 'app-custom',
  template: `<input type="text" [value]="value">`,
  providers: [MakeProvider(CustomComponent)]
})
class CustomComponent extends AbstractValueAccessor<string> {

}

@Component({
  selector: 'app-custom-container',
  template: `<div [formGroup]="formGroup"><app-custom formControlName="comp"></app-custom></div>`
})
class CustomContainerComponent {
  formGroup: FormGroup;
}

@NgModule({
  imports: [BrowserModule, ReactiveFormsModule],
  declarations: [CustomComponent, CustomContainerComponent]
})
class DynamicModule {
}

@Component({
  selector: 'app-root',
  template: `<h4>Dynamic Components</h4><br>
             <form [formGroup]="formGroup">
               <div #dynamicContentPlaceholder></div>
             </form>`
})
export class AppComponent implements OnInit {

  @ViewChild('dynamicContentPlaceholder', {read: ViewContainerRef})
  public readonly vcRef: ViewContainerRef;

  factory: ModuleWithComponentFactories<DynamicModule>;
  formGroup: FormGroup;

  constructor(private compiler: Compiler, private formBuilder: FormBuilder) {
  }

  ngOnInit() {
    this.compiler.compileModuleAndAllComponentsAsync(DynamicModule)
      .then((moduleWithComponentFactories: ModuleWithComponentFactories<DynamicModule>) => {
        this.factory = moduleWithComponentFactories;
        const compFactory = this.factory.componentFactories.find(x => x.selector === 'app-custom-container');
        const injector = ReflectiveInjector.fromResolvedProviders([], this.vcRef.parentInjector);
        let cmp = this.vcRef.createComponent(compFactory, this.vcRef.length, injector, []);
        (<CustomContainerComponent>cmp.instance).formGroup = this.formGroup;
      });

    this.formGroup = this.formBuilder.group({
      'comp': ['hello world']
    })
  }
}

@NgModule({
  imports: [BrowserModule, ReactiveFormsModule],
  declarations: [AppComponent],
  bootstrap: [AppComponent]
})
export class AppModule {
}
Community
  • 1
  • 1
Stephen Paul
  • 37,253
  • 15
  • 92
  • 74
  • 1
    no code no cry... Add what you tried here – smnbbrv Oct 24 '16 at 06:27
  • 1
    You're quite correct. I will add some code – Stephen Paul Oct 24 '16 at 07:11
  • thanks for update. What is the end use of your dynamically created components? Do you want to have a dynamic form with dynamic controls only or you want to have an ability to use the controls one-by-one? – smnbbrv Oct 24 '16 at 09:02
  • The end use is to create a series of form controls based on a JSON schema obtained from the server. The schema will have references to each components selector (I just made a small update to onInit() method of AppComponent). My form only contains dynamic controls. – Stephen Paul Oct 24 '16 at 10:36

1 Answers1

2

If I do not misunderstand your needs...

Weird thing is that you attach your form group inside of the AppComponent. Instead you could create the full form dynamically. In order to do that you need to implement two functions:

  1. One that generates the template from JSON config
  2. Another one that generates the form from JSON config

Then simply build a big form container component that contains all your inputs. I would recommend using a service here:

@Injectable()
export class ModuleFactory {

  public createComponent(jsonConfig: SomeInterface) {
    @Component({
      template: `
        <div ngForm="form">${ /* and here generate the template containing all inputs */ }</div>
      `
    })
    class FormContainerComponent {
      public form: FormGroup = /* generate a form here */;
    };

    return FormContainerComponent;
  }

  public createModule(component: any) {
    @NgModule({
      imports: [
        CommonModule,
        ReactiveFormsModule
      ],
      declarations: [ component ]
    })
    class MyModule {};

    return MyModule;
  }

}

This is of course just a simplified version, but it works fine for my project not only for the plain form but for nested form groups / arrays / controls.

Once you inject the service you can use these two functions to generate the module and the component.

smnbbrv
  • 23,502
  • 9
  • 78
  • 109