angular: formControlName could not be used with component transclusion

[x] bug report => search github for a similar issue or PR before submitting

Version: 2.4.1

A component with selector parent-component has this:

<form [formGroup]="myFormGroup">
	<ng-content></ng-content>
</div>

And it is inserted in another template with:

<parent-component>
    <input formControlName="myControl">
</parent-component>

Angular says the control is used without a form.

About this issue

  • Original URL
  • State: open
  • Created 7 years ago
  • Reactions: 55
  • Comments: 46 (9 by maintainers)

Most upvoted comments

I have switched to React long long time ago.

@asfernandes This unfortunate behavior is due to the fact that the form-wizard content is processed as part of the app, not as part of the form-wizard template. Thus formControlName directive will not find FormGroup (or ControlContainer to be more precise) to register.

What I’m usually doing in such case is using template+formControl instead of ng-content. Here is an example - https://plnkr.co/edit/lVqEIYcY9zFJCeoYdvwL?p=preview

This is the best solution I’ve come so far.

Here is a solution:

<CustomForm [formGroup]="group">
  <div [formGroup]="group">
     .... elements here
  </div>
</CustomForm>

Just add it to a div as well

I came up with this other approach of duplicating the formGroup directive on the container component and on the form and it seems to work ok: https://plnkr.co/edit/77wcSk0dnDKuJ2IyBgZd?p=preview Do you see any problem with this?

Good for you if you like it. I am the author of: https://www.wearefrontend.com Remember some devs are doing open source in both side react and angular to (HELP) the community (YOU)… Think about it before giving an answer like this one (some respect to this people)… your answer has nothing good with the context we are in (trying to solve the issue (YOU) flagged).

viewProviders have solved my problem with wrapper component for inputs in reactive forms. Example:

@Component({
    selector: 'app-input',
    templateUrl: './input.component.html',
    styleUrls: ['./input.component.scss'],
    viewProviders: [
        {
            provide: ControlContainer,
            useExisting: FormGroupDirective
        }    
    ]
})

usage: <app-input name="lastName">Last name</app-input>

“name” is used as formControlName in wrapper component

+1 We are wrapping Angular Material components in our own components to reduce boilerplate and this issue forces us to use the hack specified by gschuager.

There’s nothing insulting in saying that someone is using a different framework now. The issue was opened 3 years ago and you tagged him directly with the solution. He was kind enough to reply and tell you that he won’t be able to check it out because he uses React now. Then for no reason you decided to call him out in a patronizing tone.

What did you expect him to do? Open 3 years old code and decide to rewrite the whole the app to Angular?

As others have mentioned, the problem here is that child form directives like formControlName are looking to register with a parent form, and here there is no parent form above it in the component tree that can be injected. The FormGroupDirective instance lives inside the component’s view, which at best is a sibling.

It’s possible to make this work with a mix of viewProviders and a template-outlet, but it’s hacky and we should have an easier way to implement components like this. This needs a design doc.

For everyone having this problem:

Had the same difficulty as @artem-v-shamsutdinov while attempting to wrap angular material components into my own app components.

Solved this by adding a @Input prop at my input component to carry the current formGroup instance, and adding it to my input container tag. In my case, this container was <mat-form-field>, but i think it would also work with <div>. Just like this:

login.component.html:

<form [formGroup]="loginForm">
    <cv-classic-input [...] [parentForm]="loginForm"></cv-classic-input>
</form>

cv-classic-input.component.html:

<mat-form-field [...] [formGroup]="parentForm">
    <mat-label>{{label}}</mat-label>
    <input [...] [formControlName]="name">
</mat-form-field>

cv-classic-component.ts:

[...]
export class CvClassicInputComponent implements OnInit {
  @Input() parentForm: FormGroup
}
[...]

Hope this helped. Credits to: https://medium.com/@joshblf/using-child-components-in-angular-forms-d44e60036664

@lazarljubenovic His answer seems to say “i switched to react so it’s not my problem anymore”. I don’t like this kind of mindset so i just said what i think about it. It’s just my opinion you can agree or disagree with my answer knowing that i took the time to try to answer him (providing an online example) when i am currently working in a project with “react”…

note: what lead me here -> in our spare time (with my friends), we are comparing angular forms with react ones and we are trying to solve issues in angular forms listed in https://github.com/angular/angular/issues/31963


@undo76 if possible can you share a stackblitz (online) with your case/solution?

That I flagged in “Jan 3, 2017”. I do open source development as well, and my comment was not insulting.

@adam-marshall

I found a workaround using a factory to make it work with formGroupNames and FormGroupDirectives in Angular 7. It feels hacky, but it is the best solution I have found so far.

function factory(
    formGroupDirective: FormGroupDirective,
    formGroupName: FormGroupName
) {
    return formGroupName || formGroupDirective;
}

@Component({
    selector: 'app-field',
    templateUrl: './field.component.html',
    viewProviders: [
        {
            provide: ControlContainer,
            useFactory: factory,
            deps: [
                [new Optional(), FormGroupDirective],
                [new Optional(), FormGroupName],
            ],
        },
    ],
})
export class FieldComponent {

Yes, that is just shorter naming convention for my directives. In the component i have @Input() name: string; and in the template ... <input matInput formControlName="{{name}}..."

Component attaches itself to the parent formGroup and it works as expected. Validation, blur, dirty, submit, valueChange… whole nine yards

I dig into the angular source code in FormGroupName and FormControlName. FormControlName tries to inject FormGroupName , and can’t find it, because it is its parent only by transclusion(ng-content), not a “real” parent. The reason it is not working depends in the way angular proceed transclusion, angular ‘lost’ actually my parent. I have here a simple plnkr that demonstrates how I lost my parent component. what I want to show there that I have issue with transclusion not just with forms.

found a related issue with good menu example: https://github.com/angular/angular/issues/5126

@renanmontebelo You don’t need a custom input at all, you can simply inject it. See here for details: https://stackoverflow.com/a/46025197/2131286

Also the issue is about projection, not sub-components.

@lazarljubenovic I believe you tagged the wrong Renan 😃

@steve-todorov - at the moment the Angular team are all hands to the pumps on getting the new Ivy rendering engine and associated parts working. I would not expect this issue to be addressed until that is complete, at the earliest. @kara might have more info.

Does anyone have a new solution / workaround for this?

When duplicating the formGroup directive, then changeDetection: ChangeDetectionStrategy.OnPush will break some functionality of the component.

Here’s a small Stack Blitz example: https://stackblitz.com/edit/angular-material2-issue-zupfgz?file=app/app.component.ts

If you click the “submit”, then the errors for the first input will be shown, but the second input doesn’t get triggered

I’m not sure where you got the extra group from.

@renanmontebelo You don’t need a custom input at all, you can simply inject it. See here for details: https://stackoverflow.com/a/46025197/2131286

Also the issue is about projection, not sub-components.

@nasreddineskandrani, you are right. My answer was directed to @adam-marshall question about nested named groups.

On the other hand, I don’t think mine is a nice solution. It is not obvious at all why I need to do so for such a simple wrapper.

@undo76 if possible can you share a stackblitz (online) with your case/solution?

What I am trying to do is to create a wrapper for inputs with labels, errors, styles. In order to make it work in nested groups with FormGroupName I have to propagate the current ControlContainer in a factory.

Demo: https://stackblitz.com/edit/angular-kklcs7

I can manage to get viewProviders: [{ provide: ControlContainer, useExisting: FormGroupDirective }] working such that [formGroup] is on the parent component, and [formControlName] is on the child, and all works as expected.

However my big problem is when [formGroupName] is introduced for nested FormGroups and FormArrays… I can’t figure out a way of splitting [formGroupName] out using the viewProviders or any other means.

I’ve tried using ng-content, ng-template, loading components with ComponentFactoryResolver and even nested components which implement ControlValueAccessor, but it seems you need that one, concrete HTML element with [formControlName] on, at the same depth as the [formGroupName].

The closest I’ve got so far is to have the label, input and validation messages of the control as a component which implements ControlValueAccessor (a lot of boilerplate when the control itself is very simple), and then replicate the outer structure of formGroupName for each type of control… not ideal as you can see below:

<ng-container [ngSwitch]="groupNames.length">
    <ng-container *ngSwitchCase="0">
        <div class="form-group">
            <app-control-textarea [question]="question" [arrayIndex]="arrayIndex" [formControlName]="question.name"></app-control-textarea>
        </div>
    </ng-container>
    <ng-container *ngSwitchCase="1">
        <div class="form-group" [formGroupName]="groupNames[0]">
            <app-control-textarea [question]="question" [arrayIndex]="arrayIndex" [formControlName]="question.name"></app-control-textarea>
        </div>
    </ng-container>
    <ng-container *ngSwitchCase="2" [formGroupName]="groupNames[0]">
        <div class="form-group" [formGroupName]="groupNames[1]">
            <app-control-textarea [question]="question" [arrayIndex]="arrayIndex" [formControlName]="question.name"></app-control-textarea>
        </div>
    </ng-container>
    <ng-container *ngSwitchCase="3" [formGroupName]="groupNames[0]">
        <ng-container [formGroupName]="groupNames[1]">
            <div class="form-group" [formGroupName]="groupNames[2]">
                <app-control-textarea [question]="question" [arrayIndex]="arrayIndex" [formControlName]="question.name"></app-control-textarea>
            </div>
        </ng-container>
    </ng-container>
</ng-container>

Any ideas?

Any update?

@kara can you elaberate on how to make it work with viewProviders and a template-outlet?

@gschuager Indeed this is valid solution too. It will work as the FormGroup is the same instance 😉. However I don’t like it/use it as it is too much noise, but still this is just a personal preference.

I was hoping that the angular team will address this behavior somehow as it has another side effect. You can check this issue here. Unfortunately it was closed and no answer since then 😞.