Trying to make a dinamic form

Hi there, I’ve been using Ionic for a long time now, but I got caught with version changes while I wasn’t having much time to study. Here is my problem, I am creating a dynamic form by reading a JSON to generate the fields names and the inputs names, with this code here:

<form [formGroup]="form">
		
		<div *ngFor="let details of dataDetails; index as i">
		
			<ion-list>
				
				<ion-button expand="block" fill="outline" (click)="showDetails(i)">{{ details[0] }}</ion-button>

				<div *ngFor="let field of dataFields; index as j">
					<ion-item [hidden]=details[1] *ngIf="checkDetails(i, j)">  
						<ion-label position="floating">{{ field[1] }}</ion-label>
						<ion-input formControlName="{{form.control.j}}"></ion-input>
					</ion-item>
				</div>

			</ion-list>

		</div>

	</form>

and everything is working fine except formControlName … Can someone give me a hint to read the data?

PS: It’s impossible to make a static form with 44 fields that can be changed from time to time …

This strikes me as a unusual structure, because the inner loop doesn’t seem to depend on the outer loop, so you’ve basically got a cross-product of two arrays. Is that really what you want?

It would help to see the schema of the underlying JSON:

interface Detail {
  // ???
}
interface Field {
 // ???
}
interface FormFood {
 fields: Field[]; // ???
 details: Detail[]; // ???
 // ???
}

I can’t show you the real JSON, but here goes more or less it’s structure:

data: {
  "client-info":{
    "name": "",
    "id": ""
  },
  "client-local":{
    "street": "",
    "neighborhood": ""
  }
}

So, I’m using dataFields to collect just the fields that I want to display in the form. And dataDetails as ‘blocks’ of content to display in the html when pressed. Someone gave me the tought that I could make a JSON from this page then send it to my DB, but again I found myself at “How to collect the inputs…”

I don’t care about the contents. Please don’t tell me you consider the structure of the data to be a trade secret, or there’s really nothing more I can do here. I would like to see a formal definition in TypeScript interface form like I started to do above, culminating in a single type that if we were to call JSON.stringify on, we would end up with an object like the one you are dealing with in your app.

I’m sure this makes perfect sense to somebody who already understands your problem domain, but unfortunately it’s totally opaque to me.

Forms intended to fill complex objects work best when there is a precise 1-to-1 mapping of backing properties to template elements: when the backing type has a string, the template has an input bound to a form control. When the backing type is an array, the template has an ngFor loop. Your current cross-product template layout does not have this structure, so it is hard to comprehend.

To put it another way, I would like you to think about (not actually do, necessarily, but organize things so that you could) making this form into a separate component with a single @Input property (let’s call it “client”) that includes everything needed to make the form. What I need to see is the type of that Client object.

If that exercise is hard to do, then I think we’ve isolated the problem. Once the exercise is doable, then it should become clear how to write the template.

I’m sorry…

dataDetails are itens that show the form fields when clicked.
dataFields are the form fields.

Basically, when the page is loaded, with the JSON example I gave earlie, it should have 2 blocks, “client-Info” and “client-location”, and when clicked they should show their labels “name” and “id” and inputs, and “street” and “neighborhood” and inputs, respectively.

I don’t think I really understand what do you wanna say with “… making this form into a separate component with a single @Input…”

I kind of understant that the way I generate the page it’s not ideal… But it was the only I thought to handle a form so huge (44 field inputs…) and that can change in a near future…

This would go faster if we could just communicate in TypeScript, I think.

Are you saying that we have this structure?

interface ClientInfo {
  id: string;
  name: string;
}

interface ClientLocation {
  street: string;
  neighborhood: string;
}

// note: I am deliberately not using "client-info" and "client-location" here, 
// because allowing kebab-case to get this far is going to continue to make
// the template irritating to write. I would rename these properties to more
// Angular-friendly names very early in your pipeline.
interface Client {
  info: ClientInfo;
  location: ClientLocation;
}

type Clients = Client[];
@Component({selector: "client-editor"})
class ClientEditor {
  @Input() client: Client;
}
<client-editor [client]="client"></client-editor>

If I didn’t do it right here, can you write a Client interface so that this component could conceivably be written? Also importantly, do you want it to take a Client or a Client[]?

I think I understand now. About the TypeScript structure, it should be like this one you did. The Client interface using your analogy, should be something like that:

interface Client {
  info: ClientInfo;
  localtion: ClientLocation;
  waterInformation: ClientWaterInformation;
  sewerInformation: ClientSewerInformation;
}

The problem is that the structure is huge. Each variable of Client is another interface, each one having 4 or 5 variables, and having to redo it programmatically (in future changes) would cause a lot of chaos for future programmers (assuming that I won’t work in the project in the future…).

Because of that I tried to use a JSON as the basis for the form. How would I fill in the interface fields with data coming from JSON?

PS: Am I making myself clear?

Here’s how I would do this. This assumes that there is only one layer of nesting, and that all inputs are text. Both of those assumptions could be relaxed without too much additional effort. I’ve used a more complex version of this basic design to implement a markdown renderer in Ionic.

interface Field {
  name: string;
  label?: string;
  control?: FormControl;
}

interface Block {
  name: string;
  label?: string;
  fields: Field[];
  control?: FormGroup;
}

type Schema = Block[];

export class HomePage {
  fg: FormGroup;
  schema: Schema = [
    {
      name: 'info',
      label: '基本情報',
      fields: [{name: 'id'}, {name: 'name', label: '名前'}]
    },
    {
      name: 'location',
      fields: [{name: 'street'}, {name: 'neighborhood'}]
    }
  ];

  constructor() {
    this.fg = new FormGroup({});
    this.schema.forEach(block => {
      this.fg.addControl(block.name, block.control = this.groupify(block.fields));
    });
  }

  private groupify(fields: Field[]): FormGroup {
    let rv = new FormGroup({});
    fields.forEach(field => {
      rv.addControl(field.name, field.control = new FormControl());
    });
    return rv;
  }
}
<ion-content>
    <ion-list>
        <ng-container *ngFor="let block of schema">
            <ion-item-divider>{{block.label || block.name}}</ion-item-divider>
            <ion-item *ngFor="let field of block.fields">
                <ion-label [innerText]="field.label || field.name"></ion-label>
                <ion-input [formControl]="field.control"></ion-input>
            </ion-item>
        </ng-container>
    </ion-list>

    <div><pre><code>
        {{fg.value | json}}
    </code></pre>
    </div>
</ion-content>

As you can see, the optional label property for fields and blocks can be used to display human-readable labels (localized if desired) aside from the name property used internally, and here is how to gracefully fall back to the name if there is no label. The automatically-updating <div> at the bottom shows what you should be able to send directly to a backend API with virtually no extra work.

The Schema object could be serialized/deserialized from JSON.

1 Like

I don’t even try it, but I know this is what I want. Thanks for you help and the time you spent on this, I really appreciate that.

I could do the whole scheme and most importantly, I could understand everything. But unfortunately I couldn’t fully implement it. I’m having a problem calling the groupify function. Apparently it doesn’t iterate over the values I store in the schema, but it can iterate through static values (which I use for testing).

fetchData(){
  	fetch("./assets/forms/form.json").then(result => result.json())
  	.then(data => {

      Object.entries(data).forEach(([key, value]) => {
        // create object with the JSON values
        let values = Object.assign({}, value);
        // auxiliar to carry all the fields
        let field: Field[] = [];
        // iterate through values to get a field object
        Object.entries(values).forEach(([key, value]) => {
          field.push({name: key});
        });
        // add a new block to the schema
        this.schema.push(Object.assign({}, {
          name: key,
          show: true,
          fields: field
        }));
      });
  	});
  }

I’m pretty sure that has somthing to do with my function to fetch data from the JSON… Could you point to me where I’m going wrong? Sorry if my code isn’t good, I don’t have good practices with JS.

Here my console.log(this.schema)

Print|211x315

for the JSON:

{
	"Client": {
        "matricula": "",
        "name": "",
        "age": ""
    },
    "Car": {
        "model": "",
        "color": ""
    }
}

If you can reformat the JSON, I would do that, because it would allow you to use the previously-posted code. For example, here is a Q&D mogrifier:

> let raw = {
	"Client": {
        "matricula": "",
        "name": "",
        "age": ""
    },
    "Car": {
        "model": "",
        "color": ""
    }
};
> Object.keys(raw).map(rk => { return { name: rk, fields: Object.keys(raw[rk]) } });
[
  { name: 'Client', fields: [ 'matricula', 'name', 'age' ] },
  { name: 'Car', fields: [ 'model', 'color' ] }
]

If modifying the JSON ahead of time isn’t an option, then you could use this same operator to do the transformation on the fly:

fetch("form.json")
  .then(r => r.json())
  .then(raw => Object.keys(raw).map(rk => { return { name: rk, fields: Object.keys(raw[rk]) } }))
  .then(schema => schema.forEach(block => {
      this.fg.addControl(block.name, block.control = this.groupify(block.fields));
    }));

I don’t think I understood what you said. If I got what you said, my problem isn’t in the fields. My print wasn’t good… Let me show this:

Here you can see the first static block with its fields, and in the fields, its controls. But in the “Cliente” block you see the the fields, but it does not show its controls. I’ve put some console.log in groupify function, but it show only one time (probably in the static block “info”).

Assuming you haven’t modified anything else, I think the problem is in the timing. The calls to groupify happen in the constructor, as I wrote it, which means that schema must already exist by then.

You need to move the building of the form controls to after the schema is built:

fetch()
  .then(result => schemaifyResult())
  .then(schema => {
    schema.forEach(block => {
      this.fg.addControl(block.name, block.control = this.groupify(block.fields));
    });
  ));