formio / angular

JSON powered forms for Angular
https://formio.github.io/angular-demo
MIT License
646 stars 472 forks source link

[BUG] Custom Components not triggering inside Sub forms #1099

Open ps91 opened 2 months ago

ps91 commented 2 months ago

Environment

Please provide as many details as you can:

Steps to Reproduce

  1. Create two forms: in one form, add a data grid component and place a custom component in one of the columns. In the second form, reference the first form.

Expected behavior

The custom component should display correctly.

Observed behavior

The custom component does not display, and after debugging, I found that the custom component is never triggered.

lane-formio commented 2 months ago

Could you possibly provide a codesandbox to illustrate what you are experiencing? Also, what version of formio.js are you using?

Aduthraes commented 2 months ago

I was about to open a ticket with the same issue. I'm using:

Custom components work well in forms, subforms, inside datagrids in the main form, but when you add the custom component to a datagrid and reference that form as a subform inside another form, the custom component is not rendered. If you put a breakpoint in the ngOnChanges event of the custom component, you'll see that the breakpoint isn't hit.

Ex: listbox.component.html

<div class="mb-3">
  <p-listbox [options]="items" [(ngModel)]="selected" [optionLabel]="bindLabel" [optionValue]="bindValue"
    [filter]="filter" [multiple]="multipleValues" [checkbox]="checkbox" [disabled]="disabled"
    [listStyle]="{'max-height':maxHeight}" (onChange)="onChange($event)">
  </p-listbox>
</div>

listbox.component.ts

import { Component, EventEmitter, Input, Output, SimpleChanges } from '@angular/core';
import { ApiService } from '@hyperflowX/core';
import { FormioCustomComponent } from '@formio/angular';

@Component({
  selector: 'hfx-listbox',
  templateUrl: './listbox.component.html',
  styleUrls: ['./listbox.component.scss']
})
export class ListboxComponent implements FormioCustomComponent<any> {

  @Input() value: any;
  @Output() valueChange = new EventEmitter<any>();
  @Input() disabled = false;
  @Input() label: string;
  @Input() bindLabel: string;
  @Input() bindValue: string;
  @Input() endpoint: string;
  @Input() method: string;
  @Input() dataPath: string;

  @Input() checkbox: boolean;
  @Input() multipleValues: boolean;
  @Input() filter: boolean;
  @Input() maxHeight: string;

  selected: any;
  items = [];

  constructor(private apiService: ApiService<any>) {
  }

  ngOnChanges(changes: SimpleChanges): void {
    if (changes.endpoint) {
      this.getData();
    }

    if (this.value) {
      if (this.multipleValues) {
        // To avoid the validation error "value must not be an array" the value can't be an array 
        // so the value is saved in an object in the property 'items'
        this.selected = this.value.items || [];
      } else {
        this.selected = this.value;
      }
    }
  }

  onChange(data) {
    this.updateValue(this.selected);
  }

  updateValue(newValue: any) {
    // To avoid the validation error "value must not be an array" the value can't be an array 
    // so the value is saved in an object in the property 'items'
    this.value = this.multipleValues ? { items: newValue } : newValue;
    this.valueChange.emit(this.value);
  }

  getData(data?: any) {
    if (this.endpoint) {
      this.apiService.getData(this.method, this.endpoint, null)
        .subscribe(response => {
          if (response) {
            this.items = this.dataPath && response.data[this.dataPath]
              ? response.data[this.dataPath]
              : response.data;
          }
        });
    } else {
      let itemsVariable = this.dataPath ? data[this.dataPath] : data;

      // Verifica se a propriedade é composta
      if (this.dataPath.includes('.')) {
        const props = this.dataPath.split('.');
        let value = data;
        for (const prop of props) {
          value = value[prop];
        }
        itemsVariable = value;
      }

      if (itemsVariable != null) {
        this.items = [...itemsVariable];
      }
    }
  }
}

listbox.formio.ts

import { Injector } from '@angular/core';
import { FormioCustomComponentInfo, registerCustomFormioComponent, Components } from '@formio/angular';
import { ListboxComponent } from './listbox.component';

const COMPONENT_OPTIONS: FormioCustomComponentInfo = {
  type: 'listbox', // custom type. Formio will identify the field with this type.
  selector: 'hfx-listbox', // custom selector. Angular Elements will create a custom html tag with this selector
  title: 'Listbox', // Title of the component
  group: 'basic', // Build Group
  icon: 'fa fa-dot-circle-o', // Icon
  editForm: customEditForm,
  //  template: 'input', // Optional: define a template for the element. Default: input
  //  changeEvent: 'valueChange', // Optional: define the changeEvent
  //  when the formio updates the value in the state. Default: 'valueChange'
  //  editForm: Components.components.textfield.editForm,
  // Optional: define the editForm of the field. Default: the editForm of a textfield
  //  documentation: '', // Optional: define the documentation of the field
  //  weight: 0, // Optional: define the weight in the builder group
  //  schema: {}, // Optional: define extra default schema for the field
  //  extraValidators: [], // Optional: define extra validators  for the field
  //  emptyValue: null, // Optional: the emptyValue of the field
};

export function registerListboxComponent(injector: Injector) {
  try {
    registerCustomFormioComponent(COMPONENT_OPTIONS, ListboxComponent, injector);
  } catch (error) {
    // already registered
  }
}

export function customEditForm() {
  const form = Components.components.textfield.editForm();

  form.components[0].components.push(
    {
      label: 'CONFIG',
      key: 'selectConfiguration',
      weight: 30,
      components: [
        {
          weight: 10,
          type: 'textfield',
          key: 'customOptions.endpoint',
          label: 'Endpoint Url',
          placeholder: 'Endpoint',
          input: true,
        }, {
          weight: 20,
          type: 'select',
          key: 'customOptions.method',
          label: 'Method',
          input: true,
          dataSrc: 'values',
          data: {
            values: [
              { value: 'get', label: 'Get' },
              { value: 'post', label: 'Post' },
            ],
          },
        }, {
          weight: 30,
          type: 'textfield',
          key: 'customOptions.dataPath',
          label: 'Data Path',
          placeholder: 'Data Path',
          tooltip: 'The property within the source data where iterable items reside. For example: result.items',
          input: true,
        }, {
          weight: 40,
          type: 'textfield',
          key: 'customOptions.bindLabel',
          label: 'Bind Label',
          placeholder: 'Bind Label',
          input: true,
          validate: {
            required: true,
          },
        }, {
          weight: 50,
          type: 'textfield',
          key: 'customOptions.bindValue',
          label: 'Bind Value',
          placeholder: 'Bind Value',
          input: true,
          validate: {
            required: true,
          },
        }, {
          weight: 60,
          type: 'checkbox',
          key: 'customOptions.filter',
          label: 'Filter',
          input: true,
        }, {
          weight: 70,
          type: 'checkbox',
          key: 'customOptions.multipleValues',
          label: 'Multiple values',
          input: true,
        }, {
          weight: 80,
          type: 'checkbox',
          key: 'customOptions.checkbox',
          label: 'Checkbox',
          input: true,
          'conditional': {
            'eq': 'true',
            'when': 'customOptions.multipleValues',
            'show': 'true',
          },
        }, {
          weight: 90,
          type: 'textfield',
          key: 'customOptions.maxHeight',
          label: 'Max height',
          tooltip: 'Max height css style',
          input: true
        }
      ],
    });

  return form;
}

Create a form, add a datagrid and add the component the datagrid. Create a second form and reference the previous form as a subform.

lane-formio commented 1 month ago

If we had a reproducible example from something like stackblitz it would help me get a meaningful review.