#Material - resusable inputs without mat-form-field

81 messages · Page 1 of 1 (latest)

rough nova
#

I have a very simple setup. Apologies...no stackblitz for this as it's quite difficult to set up material on Stackblitz without a terminal.

I have the following:

// app.component.html
<form>
  <mat-form-field>
    <app-simple-input></app-simple-input>
  </mat-form-field>
</form>
// simple-input.component.html
<input matInput type="text" [formControl]="control" />
import { Component } from '@angular/core';
import { FormControl } from '@angular/forms';

@Component({
  selector: 'app-simple-input',
  templateUrl: './simple-input.component.html',
  styleUrls: ['./simple-input.component.scss'],
})
export class SimpleInputComponent {
  control = new FormControl();
}

This setup does not appear to work, as I get the following error:

#

I want to build my reusable form fields this way as it allows consumers to provide their own mat-labels, mat-hints, mat-errors, etc...

The only other way I know where this is possible is by implementing MatFormFieldControl but this has a lot of boilerplate and also breaks type safety on the reactive form controls.

If I have my reusable component include the mat-form-field within its template, then I'm closing things off to the consumer and my reusable component needs to proxy everything that material provides (e.g. label, appearance, errors).

inland sparrow
#

You can fork a Stackblitz project from Material documentation as a starter.
Can you provide examples about both solutions. Here I don't see the benefits of your attempt.
For reusability, I'd directly use formly library.

rough nova
#

Oh good call on forking from the component docs. Completely forgot about that

#

Formly is a nuclear option and I'd rather not go that route since we already have a rather large component library which is built fully on material. I'm looking to improve our implementation patterns which are very closed off and require a lot of maintenance as requirements pop up.

e.g. lets say we have a Country autocomplete. It is a mat-autocomplete. So the typical implementation would be something like this:

//country-autocomplete.component.html
<mat-form-field>
  <mat-label>Country</mat-label>
  <input matInput 
         type="text" 
         [formControl]="control"
         [matAutocomplete]="auto">
  <mat-autocomplete #auto="matAutocomplete">
    <mat-option *ngFor="let country of countries$ | async" [value]="country">
      <img src="country.flagUrl"> {{ country.name }}
    </mat-option>
  <mat-autocomplete>
  <mat-error *ngIf="control.errors?.invalid">Please select a valid country</mat-error>
</mat-form-field>
#

Now the problem with this:

We have to export every single little property we want to be able to provide to the mat form field. Appearance, label, color, hint, etc...

#

It also closes off validation. The parent can't easily add errors to this mat form field. We could set up some content projection here but its a bit hacky.

#

One consumer might want to extend the default validation by only allowing a subset of countries. So now instead of making our component composable and easily extendible, we have to specifically account for this and make a change to the component to allow to modify the validation.

#

You could argue that the component shouldn't provide its own validation, but it's done in certain cases to enforce policies. But if that's a very bad practice, I'll concede that point and suggest that we only provide the ValidatorFn to consumers to supply on their own.

#

I have asked a similar question many times. This is the most common type of reusable component used within an enterprise setting. After all, 90% of what we do is forms.

#

Invariable, formly gets recommended. So are ReactiveForms and Material simply not suited for enterprise? There's a frustrating lack of documentation on how to use them in anything but the most trivial of cases. =\

#

Now, what I was hoping to do was something like this. Basically remove the mat-form-field wrapper. That way, a consumer can use all of mat-form-field properties and content-projection slots. I could take things further by making countries$ an injectible service so that the filtered data can be further filtered by a consumer.

// country-autocomplete.component.html
<input matInput 
       type="text" 
       [formControl]="control"
       [matAutocomplete]="auto">
<mat-autocomplete #auto="matAutocomplete">
  <mat-option *ngFor="let country of countries$ | async" [value]="country">
    <img src="country.flagUrl"> {{ country.name }}
  </mat-option>
<mat-autocomplete>

// my-form.component.html
<mat-form-field>
  <mat-label>Country</mat-label>
  <country-autocomplete>  
  <mat-error *ngIf="control.errors?.invalid">Please select a valid country</mat-error>
</mat-form-field>
#

This is possible by implementing MatFormFieldControl but it is incredibly cumbersome to do so for simple components that are simple styled autocompletes like this. Since implementing MatFormFieldControl also uses ControlValueAccessor, we lose all typing for our reactive form as well, so now we're left with a worse DX and we need to add a lot of runtime validation.

rough nova
#

Seems like it might simply be impossible due to the fact that angular will do this:

// actual html
<mat-form-field>
  <mat-label>Country</mat-label>
  //country-autocomplete
  <country-autocomplete>  
    <input matInput 
       type="text" 
       [formControl]="control"
       [matAutocomplete]="auto">
    <mat-autocomplete #auto="matAutocomplete">
      <mat-option *ngFor="let country of countries$ | async" [value]="country">
        <img src="country.flagUrl"> {{ country.name }}
      </mat-option>
    <mat-autocomplete>
  <country-autocomplete>
  <mat-error *ngIf="control.errors?.invalid">Please select a valid country</mat-error>
</mat-form-field>

And mat-form-field fails to find the input with its associated formcontrol

#

It's just unfortunate that the basic use case of creating an extendable autocomplete field with some basic styling in Angular is a good 100+ lines of code when implementing MatFormFieldControl. Whereas I could write the same thing in vue with vuetify like this:

// country-autocomplete.vue
<script setup lang="ts">
  const props = defineProps<{country: country}>();

  const countries = ['Canada', 'USA', 'Mexico'];
</script>

<template>
  <v-autocomplete v-model="country" :items="countries">
  <template #item="data">
    <img src="data.item.flagUrl"> {{ data.item.name }}
  <item>
  <v-autocomplete>
</template>

Usage:

<country-autocomplete label="Country" :rules="myValidatorFn" />
#

Maybe Formly truly is the only option. But formly doesn't use material (even though it provides material-like fields). And it doesn't have any documentation on creating completely custom fields (e.g. a country dropdown with custom styling on the dropdown items), only custom field types / validation.

#

And formly really doesn't seem to be built to create a component library. The documentation seems to indicate that it's really geared towards app specific forms

inland sparrow
#

Given consumers might change the label, the hint, the errors and the data source. What's left about a reusable component?
Here given the requirements, the consumers should just consume Angular Material directly rather than a custom wrapper component

rough nova
#

It’s more about keeping the component open and composable. In an ideal world the component would provide a sensible default implementation but still allow the flexibility to modify things without having to make changes to the underlying component. It’s difficult for the write of a component to predict how a component will be reused so I was just trying to find an easier and more sensible pattern for new components to adopt. I just think that what I wanted is not possible. If angular allowed mounting a component without its host component and if it had a way to provide fallback content projection slots it might be possible.

I guess the best thing we can do is just enforce certain standards. i.e. proxy things like label, hints, errors through Input properties. Proxy material inputs like Appearance. Allow the consumer to pass in a filter function to further filter autocomplete items…or turn the autocomplete filter into a service that is provided in the component module and can be extended.

gloomy parcel
#

Cant you just project mat-label etc?

#

<ng-content select="mat-label"></ng-content>

Inside your component, you would have the ng-content inside the mat form field still.

#

So People can do

<Your-component>
<mat-label>title</mat-label>
...
</Your-component>

#

It gets complicated to use a fallback if not provided. Not sure it's impossible, but been too long to recall how and if that's possible.

#

Not gonna lie, this was easier in angularjs, which supported replace: true, to avoid rendering the host component. But that's often more a bad idea than a good.

#

A more complicated solution, but a nicer api to use, would be to have your own components for the label.

#

but that would mean you need to communicate from the custom label component to your parent component (the one that holds the mat form field) and have it setup the html based on that.

#

Would only do that if you realy need loads of customization.

#

Benefit also is that it decouples the consuming code from material. but that's probably unneccesary.

#

Users have no idea u use material, they just use your label component, inside your component.

#

So I believe what you want to achieve is possible, but it's not neccesarily easier than proxying all inputs (well it,s more work for you, less work for people using the component)

#

So in your case, you could replace:

<ng-template><ng-content></ng-content></ng-template>

with

<ng-template><mat-label><ng-content></ng-content></mat-label></ng-template>

And rename hello-child to your-label

#

That way, it will inject mat-label in the parent (hello) component, without a wrapping host. Allowing you to nest it directly under mat-form-field if you change the hello html to:

template: `<mat-form-field><ng-container *ngTemplateOutlet="nameTemplate"></ng-container></mat-form-field>`
#

Loads of work, not always worth it. But just sharing it to show that things are possible.

#

All of the above is complicated, but also implementation details the consumer of your compobents do not have to worry about.

#

Users would use:

<your-form-field>
  <your-label>Foo</your-label>
</your-form-field>

and it would render

<your-form-field>
  <mat-form-field>
    <mat-label>Foo</mat-label>
  </mat-form-field>
</your-form-field>
#

Sorry for the wall of text, I have done this before and figured it was worth elaborating a bit 😂

#

(feel free to ping me if this makes no sense, I wrote most of it on mobile (well, not the stackblitz), so it may not be well explained 🙈

#

All that text, and I forgot to ping you @rough nova ☝️ Pinging to ensure you dont miss this novel.

rough nova
#

Thanks heh. I’ll take a look and play around with it once my brain is functioning heh.

gloomy parcel
#

If you don't care about having your own label component, drop the hello-child and the ng-container in hello. And use:

template: `<mat-form-field><ng-content select="mat-label"></mat-form-field>`

in the hello HTML

#

Didn't test that, but last time I used angular I think that worked.

#

Users would then use

<your-form-field>
  <mat-label>Foo</mat-label>
</your-form-field>

But that feels slightly off for me.

#

Regarding formly. I think your question has nothing to do with forms, but is purely about components.

#

In this case, it's components that are concerned about forms, but thats irrelevant to the solution.

#

Reactive Forms and Template Forms are awesome, and fine for enterprise. I only ever used formly once, I think it's great. But I see no reason to constantly refer people to use third party things when native things can be more than fine for the question at hand

rough nova
#

It has somewhat to do with forms since people in my area really like having components provide validation but that feels wrong to me. Really at this point I think I just want to provide a style guide and blueprint for creating reusable form components

gloomy parcel
#

You can translate it to any kind of components.

rough nova
#

Well there is one thing that’s form specific-ish. One pattern I’ve noticed is people like to take an Input formControl and do things with it (I.e addValidator), or take in an Input formGroup and add formControls to it in the child component. I think it’s fair to say that’s violating 1 way data flow and I should discourage those patterns.

gloomy parcel
#

I would ignore that pattern if you talk about reusability.

rough nova
#

Oh ok great love the discussion on this

gloomy parcel
#

it works, but it limits reusability.

gloomy parcel
rough nova
#

One thing that bothers me with CVA is the lack of support for typing the form control.

gloomy parcel
#

So cant reply to that.

rough nova
#

On top of the boilerplate of CVA we also need to worry about runtime validation. So it ends up being a lot of code

gloomy parcel
#

It's one less way to reuse it.

#

But again, I see reasons it does justify it. Rules exist to be broken.

rough nova
#

CVA seems like it should mainly be used if:
1 - you want to support all manners of passing in form controls (ngModel, formControlName, etc)
2 - if your form control is a complex object that has to be bound to multiple inputs

#

There is another option actually that is less boilerplate than CVA if you only want to account for case 1. You can inject ngControl

#

Then you get access to the form control and you don’t need to worry about implementing writeValue and registering change listeners.

gloomy parcel
rough nova
#

I believe it does

#

Only thing is it’s poorly documented. Though CVA is also poorly documented.

gloomy parcel
#

Documented, yes. But there are great articles out there.

#

CVA is not that complicated, but ye it's like 80% the same code over and over again. But I take that for the flexibility any time.

rough nova
gloomy parcel
#

But tbh, what you are trying to achieve is beyond basics, so I feel like it justifies a more complicated solution.

#

Anyway, loads of food for thought, and all kinds of ways to achieve what you need. Most depends on requirements, but I tend to try and avoid passing formControl if we talk about reusability.

rough nova
#

I think I just want to come up with the simplest solution for both consumers and the writers of the reusable component. I think I’ll just have to play around with it to find a nice middle ground.

gloomy parcel
#

Its not because I prefer reactive forms with form control, that others cant like formControlName, or ngModel even.

rough nova
#

If this was an external lib I’d agree fully. But since it’s internal and we’re pretty strict with typescript, I like the idea of only allowing typed formcontrols going forward.

gloomy parcel
gloomy parcel
#

Anyway, merry christmas and good luck with it!

rough nova
#

Unfortunately it doesn’t. When you import ReactiveFormsModule, it allows any of the formcontrol attributes to be added to any element. There’s no type safety at the template