Ngx-formly: Feature: don't load observables of hidden fields (i.e. select boxes)

Created on 2 Oct 2018  路  20Comments  路  Source: ngx-formly/ngx-formly

I'm submitting a ... (check one with "x")

[ ] bug report => search github for a similar issue or PR before submitting
[X] feature request
[ ] support request

Current behavior

Right now, if I have a large form with lots of select boxes that resolve their data via Observables resulting in HTTP calls, then even if the fields are not visible (because of permissions or because of the values in the current model), the data for those fields is loaded anyway.

This is often not necessary because the fields might never actually become visible.

Expected behavior

Something like this Stackblitz here but natively supported by Formly.

Minimal reproduction of the problem with instructions

https://stackblitz.com/edit/angular-x4qjjk?file=src%2Fapp%2Fapp.component.ts

What is the motivation / use case for changing the behavior?

I think it should greatly improve the loading time of large forms.

enhancement

Most helpful comment

This issue has been fixed and released as part of v5.10.0 release. You can now lazy render hidden fields by passing the following config:

    FormlyModule.forRoot({
+      extras: {
+        lazyRender: true,
+      },
    }),

Please let us know, in case you are still encountering a similar issue/problem.
Thank you!

All 20 comments

_just as an update meanwhile_

As of now, I'm handling this by not even adding the formly configuration for that select to formly. This is done via a custom service that "guards" the passed configuration.

Given I have the following Angular component with the method createFormlyConfig().

@Component({})
export class SomeComponent {
   formlyFields: FormlyFieldConfig[];
   ...
   createFormlyConfig() {
      this.formlyFields = [
         {
             key: 'firstname',
             ...
         },
         ...this.myCustomFormlyService.guard(this.permissionService.hasPermission('SEE_AGE'), [
             {
                 key: 'age',
                 ...
             }
         ]
      ]
   }
} 

In this way formly doesn't even see the configuration for the given field as it is not being passed to the configuration. Ofc, this only works for static configuration that doesn't change dynamically based on some model property. In that case the hideExpressions need to be used or alternatively the createFormlyConfig() needs to be called again.

the critical part is done, the component field type is rendered only if hide is false which means the async pipe will be called lazily, still missing some parts to improve such as onInit callback and wrappers components until we figure out a proper way without breaking changes.

@aitboudad awesome. I plan to update my monorepo's formly dep (currently v5-beta.12) to the latest rc in the coming days. So I can properly test the implementation, moreover I have a couple of tests running against some complex forms, so we'll see how that goes 馃槃.

thx meanwhile 馃憤

@aitboudad
@juristr

Question: hidden is used to set display: none. Shouldn't we have two properties: hidden (for not rendering) and "display" (to disable/enable showing)?

Lazily rendering components can bear side-effects, when the field is hidden, but has a defaultValue, which will not be propagated to the model.

In our case the subscription also for hidden (display none) select fields is essential.
In our "Forms" Framework we retrieve FieldConfigs from a backend and on select types set the options field with an async handler. The actual retrieval of options are triggered by definitions in the framework.
As we never know when Formly/Angular subscribes to the async handler, we implemented a synchronizer (semaphore), so that no GET_OPTIONS request is missed. For each options set we add 1. When the subscription succeeded we subtract 1.

    return this.getFlatCommandsBus().pipe(
      takeUntil(handlerClass.getOnDestroy$()),

      startWith(<GuiCommand>{ type: DynamicFormTransitionIntlTypes.SET_SYNCHRONIZER, }),
      tap(command => {
        if (command.type === DynamicFormTransitionIntlTypes.SET_SYNCHRONIZER) {
          setTimeout(() => {
            this.synchronizer$.next(-1);
          }, 0);
        }
      }),
  filter(command => command.type === DynamicFormTransitionTypes.LOAD_OPTIONS && command.context.viewId === viewId && command.keyId === keyId),
      switchmap(.....

Processing of commands, including GET_OPTIONS are delayed as long as (delayWhen semaphore !== 0) .

rxjs replay functions are not of any help, as we need to set a buffer high enough so that no GET_OPTIONS is missed. Also earlier GET_OPTIONS in the buffer could be of a previous form, resulting of side effects.

When sticking to just hidden, can you make this lazy rendering configurable?

When sticking to just hidden, can you make this lazy rendering configurable?

I'm trying to avoid breaking apps :), so let's make it configurable

Question: hidden is used to set display: none. Shouldn't we have two properties: hidden (for not rendering) and "display" (to disable/enable showing)?

You didn't answer my question explicitly.
What about a FieldConfig option "render" (instead of hidden) and keeping hidden for display: none ?

providing more options is not my preferred way to solve this, I think the lazy feature should be the default behavior in the future but for now, passing a lazy attribute for a specific formly-form would enable this behavior or globally through extra options:

  1. through formly-form:
<formly-form lazy ...></formly-form>
  1. through NgModule declaration:
FormlyModule.forRoot({
  extras: { lazy: true },
})

I'm open to any other better approach, so let me know WDYT

I am still figuring out where the journey goes to.

providing more options is not my preferred way to solve this

So FormlyConfig hidden (style.display: none) == similar HTML hidden == will be lazy rendered?

I think the lazy feature should be the default behavior in the future

How will it be implemented ? At the beginning the field is not rendered, when hidden=false, it is rendered, when hidden=true it is de-rendered.

Will style.display: none still be used ? How is it differentiated from hidden used in the lazy rendering context ?

What about descendants of a hidden field, are they instantiated?

There are only 2 differences between the old way and the new lazy rendered:

  1. the field template will not be present on the DOM (it should act like virtual dom), so we don't need to use style.display: none to hide the field element

  2. since the template is not rendered the async calls, for example, will be called only when the field is rendered for the first time.
    when the field is re-rendered we only attach the old element to the DOM (neither destruction or initialization will be recalled)

Note: instantiattion !== rendering

Thanks for the clarification.

So my questioning can be reduced to my concern to have async calls always being subscribed during instantiation. The lazy rendering (DOM) is not one of my concerns.

How will the lazy option be implemented?

  • true (default): lazy rendering and lazy subscription
  • false: no lazy rendering and no lazy subscription

I am only asking for a "no lazy subscription" option.

How will the lazy option be implemented?

  • true: lazy rendering and lazy subscription
  • false (default): no lazy rendering and no lazy subscription

for V5 default is false, for V6 we will decide whether to enable by default or not.

after thinking a little a bit, I'm going to revert https://github.com/ngx-formly/ngx-formly/pull/1445 as I would prefer to not hurry up and leave this for the next major version.

No more features for V5, releasing a stable version should be the priority for now.

Lazy was entirely removed for v5? I've got around 58 fields being rendered with display: none and the form is getting sluggish and now I'm getting validation errors from hidden fields :(

Would try to npm i @ngx-formly/core@next but it points to 5.0.0-rc.12

I'm getting validation errors from hidden fields :(

use hideExpression

the form is getting sluggish

use checkExpressionOn: https://github.com/ngx-formly/ngx-formly/issues/1620#issuecomment-514639091

if you still have an issue/question, fill an issue with reproduction and we'll take care of it.

by hidden I meant through hideExpression and checkExpressionOn is already in use. It's creating 58 fields which aren't often displayed :) I'll have to create a reproduction

also, check the following comment https://github.com/ngx-formly/ngx-formly/issues/1489#issuecomment-479651796 that may help :)

Was a good start to add it throughout to my custom FormArrays but the problem was the FormlyGroup not using *ngIf="!f.hide". After adding that it solved all my problems:

import { Component, HostBinding } from '@angular/core';
import { FieldType } from '@ngx-formly/core';

@Component({
  selector: 'tma-formly-group',
  template: `
    <ng-container *ngFor="let f of field.fieldGroup">
      <formly-field [field]="f" *ngIf="!f.hide"></formly-field>
    </ng-container>
    <ng-content></ng-content>
  `,
})
export class FormlyGroupTypeComponent extends FieldType {
  @HostBinding('class')
  public get klass() {
    return this.field && this.field.fieldGroupClassName || '';
  }

  public defaultOptions = {
    defaultValue: {},
  };
}

Would be awesome to have lazy support throughout by default but I guess that's coming in v6 :)

Any update on this? I'm trying with select field, for example second field will be disabled and not load data while the first field is not selected...

I'm a little concerned with the (likely) new default behavior. I know making the API surface bigger by adding an additional field like "render" is not the best but I too want to rely on the old behavior, but also maybe want to mix the two in some cases.

This issue has been fixed and released as part of v5.10.0 release. You can now lazy render hidden fields by passing the following config:

    FormlyModule.forRoot({
+      extras: {
+        lazyRender: true,
+      },
    }),

Please let us know, in case you are still encountering a similar issue/problem.
Thank you!

Was this page helpful?
0 / 5 - 0 ratings