Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

formControlName could not be used with component transclusion #13761

Open
asfernandes opened this issue Jan 3, 2017 · 47 comments
Open

formControlName could not be used with component transclusion #13761

asfernandes opened this issue Jan 3, 2017 · 47 comments
Labels
area: forms feature: under consideration Feature request for which voting has completed and the request is now under consideration feature Issue that requests a new feature freq2: medium state: Needs Design
Milestone

Comments

@asfernandes
Copy link

[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.

@DzmitryShylovich
Copy link
Contributor

pls add a repro

@asfernandes
Copy link
Author

@rusev
Copy link

rusev commented Jan 4, 2017

@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.

@gschuager
Copy link

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?

@rusev
Copy link

rusev commented Mar 20, 2017

@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 😞.

@brachi-wernick
Copy link

brachi-wernick commented Jul 5, 2017

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: #5126

@kara
Copy link
Contributor

kara commented Dec 8, 2017

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.

@artem-v-shamsutdinov
Copy link

artem-v-shamsutdinov commented Dec 18, 2017

+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.

@benneq
Copy link

benneq commented Mar 25, 2018

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

@e-davidson
Copy link

Any update?

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

@SerkanSipahi
Copy link

@benneq when removing changeDetection: ChangeDetectionStrategy.OnPush the same error still there.

@hecht-software
Copy link

@Russoturisto we have the same requirement. We are wrapping material form components. See my solution on SO which works very well for us:
https://stackoverflow.com/a/51890273/395879

@steve-todorov
Copy link

@kara , @DzmitryShylovich, @petebacondarwin ping? Are there any plans to fix / improve this issue?

@petebacondarwin
Copy link
Member

@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.

@brachi-wernick
Copy link

I see this as a good time to investigate it. This fix can be related to rendering.
Sometimes, when rewriting a module you have a good opportunity to enhance it's ability.

@steve-todorov
Copy link

steve-todorov commented Dec 4, 2018

@petebacondarwin many thanks for replying! Looking forward to Ivy as well! I'm sure you guys will do an awesome job! :)

@Igneous01
Copy link

We are also wrapping material components to reduce boiler plate and are blocked on this issue as well. Will be hoping this is looked at some point as well.

@kevupton
Copy link

Here is a solution:

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

Just add it to a div as well

@stguitar
Copy link

Just sharing that @rusev solution worked great for me, after changing slightly for new apis: https://next.plnkr.co/edit/IfXXzwQk1uhUJTmL

For whatever reason (probably just what other stuff i have going on with this app) I was not able to get @gschuager approach to work for me, but suspect its just the composition of my forms

im assuming this is still the 'common approach' to this problem

@lazarljubenovic
Copy link
Contributor

The sanest and most straight-forward/obvious workaround for this was to pass in the reference to the form along with the key in the ng-template.

So instead of

        <ng-template
          [ngTemplateOutlet]="tpl"
          [ngTemplateOutletContext]="{
                                       key: 'path.to.key'
                                     }"
        ></ng-template>

pass in the form as well

        <ng-template
          [ngTemplateOutlet]="tpl"
          [ngTemplateOutletContext]="{
                                       key: 'path.to.key',
                                       form: form
                                     }"
        ></ng-template>

Inside the template, instead of [formControlName]="key", do [formControl]="form.get(key)".

The formControlName is just a shortcut for exactly that, when you have a regular, common form with fields inside. For this advanced use-case, I don't think it's a big deal that the shorthand way to assign a control cannot be used.

@dxm99
Copy link

dxm99 commented Sep 6, 2019

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

@benneq
Copy link

benneq commented Sep 6, 2019 via email

@dxm99
Copy link

dxm99 commented Sep 7, 2019

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

@adam-marshall
Copy link

adam-marshall commented Sep 11, 2019

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?

@adam-marshall
Copy link

Any ideas?

In case anyone cares, I managed to get rid of the repetition part by using templating in combination with content projection, both together (couldn't use content projection alone as you end up with multiple children, one for each switch case, couldn't use templating alone as you can't grab HTML from a separate file).

@undo76
Copy link

undo76 commented Dec 27, 2019

@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 {

@nasreddineskandrani
Copy link

nasreddineskandrani commented Dec 28, 2019

@asfernandes you can transclude a component that has a reference to a formGroup as @input and add this reference to your root FormGroup or manage it with a different one => It will work.

example look at the app.component here:
https://stackblitz.com/edit/angular-example-nested-form-control-with-ng-content?file=src%2Fapp%2Fapp.component.html

edit: 31-12-2019
After cleaning my example i figured my solution comeback to the solution mentionned before in the thread by @gschuager #13761 (comment)

@asfernandes
Copy link
Author

I have switched to React long long time ago.

@nasreddineskandrani
Copy link

nasreddineskandrani commented Dec 28, 2019

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).

@asfernandes
Copy link
Author

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

@lazarljubenovic
Copy link
Contributor

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?

@undo76
Copy link

undo76 commented Dec 30, 2019

Thanks @nasreddineskandrani, but my problem is that my input wrapper is sometimes included under a nested FormGroup, therefore ControlContainer sometimes is a FormGroupDirective and sometimes a FormGroupName. In order to make it work I have to declare something like this:

@Component({
    selector: 'app-field',
    templateUrl: './field.component.html',
    viewProviders: [
        {
            provide: ControlContainer,
            useFactory: (controlContainer: ControlContainer) =>
                controlContainer,
            deps: [[new SkipSelf(), ControlContainer]],
        },
    ],
})

It feels wrong to have to use to a factory to provide the ControlContainer. I also don't understand why I need to expose the ControlContainer in viewProviders. If I expose it in providers it doesn't work.

@nasreddineskandrani
Copy link

nasreddineskandrani commented Dec 31, 2019

@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 #31963


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

@undo76
Copy link

undo76 commented Dec 31, 2019

@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

@nasreddineskandrani
Copy link

nasreddineskandrani commented Dec 31, 2019

@undo76 thanks for the example. I understand what you did. It's a really nice solution but if i am not wrong this is a different problem/case from the initial one that @asfernandes opened the issue for?

@undo76
Copy link

undo76 commented Dec 31, 2019

@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.

@nasreddineskandrani
Copy link

nasreddineskandrani commented Dec 31, 2019

@undo76 oki +1 . In my opinion, what will be cool is to isolate your case in another angular issue. So devs and angular core team can evaluate your solution and give you feedback or add a new feature/fix to angular to do it with a simpler way. Also it'll help the readers of this issue to stay focused on the different answers to the initial issue (that multiple devs proposed already on top).

@RenanRossiDias
Copy link

RenanRossiDias commented Sep 6, 2020

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
Copy link
Contributor

lazarljubenovic commented Sep 7, 2020

@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.

@renanmontebelo
Copy link
Contributor

@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 :-)

@lazarljubenovic
Copy link
Contributor

Whopes, sorry for the ping. 😅 @RenanRossiDias ☝️

@zaquas77
Copy link

Somebody have some news about it? I have try with the last angular 11, but not yet work :(

@nasreddineskandrani
Copy link

nasreddineskandrani commented Oct 17, 2020

@lazarljubenovic
your last answer gives:

this.fb.group({
    group: this.fb.group({
      foo: 'foo',
      bar: 'bar',
    }),

when in his approach he ends up with:

this.fb.group({
      foo: 'foo',
      bar: 'bar',
    }),
  })

two form group vs one form group no?

@seeiuu
this is my example i built and posted previously in this github issue. You can use it as a workaround
https://stackblitz.com/edit/angular-example-nested-form-control-with-ng-content?file=src%2Fapp%2Fapp.component.html

@lazarljubenovic
Copy link
Contributor

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

@nasreddineskandrani
Copy link

@lazarljubenovic your example in stackoverflow has an extra formgroup but it was about sub-form and not projection. My bad!

@angular-robot angular-robot bot added the feature: under consideration Feature request for which voting has completed and the request is now under consideration label Jun 4, 2021
@michaelurban
Copy link

I'm running into this while creating reusable forms. Any movement?

@DutchPrince
Copy link

+1

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
area: forms feature: under consideration Feature request for which voting has completed and the request is now under consideration feature Issue that requests a new feature freq2: medium state: Needs Design
Projects
None yet
Development

No branches or pull requests