diff --git a/src/utils/__tests__/formUtil.test.ts b/src/utils/__tests__/formUtil.test.ts index 18f7e8a3..58c3da7e 100644 --- a/src/utils/__tests__/formUtil.test.ts +++ b/src/utils/__tests__/formUtil.test.ts @@ -1,6 +1,14 @@ +import get from "lodash/get"; import { expect } from "chai"; +import { Component } from "types"; -import { getContextualRowData, eachComponentDataAsync, isComponentDataEmpty } from "../formUtil"; +import { + getContextualRowData, + eachComponentDataAsync, + isComponentDataEmpty, + eachComponent, + eachComponentData +} from "../formUtil"; describe('getContextualRowData', () => { it('Should return the data at path without the last element given nested containers', () => { @@ -365,7 +373,7 @@ describe('eachComponentDataAsync', () => { }); }); -describe('isEmpty', () => { +describe('isComponentDataEmpty', () => { it('Should return true for an empty object', () => { const component = { type: 'textfield', @@ -705,3 +713,444 @@ describe('isEmpty', () => { expect(actual).to.equal(expected); }); }); + +describe('eachComponent', () => { + it('Should iterate over each component given a flat components array', () => { + const components = [ + { + type: 'textfield', + key: 'textField', + input: true, + }, + { + type: 'textarea', + key: 'textArea', + input: true, + } + ]; + const rowResults: Map = new Map(); + eachComponent(components, (component: Component, path: string) => { + rowResults.set(path, component); + }); + expect(rowResults.size).to.equal(2); + expect(rowResults.get('textField')).to.deep.equal({ + type: 'textfield', + key: 'textField', + input: true, + }); + expect(rowResults.get('textArea')).to.deep.equal({ + type: 'textarea', + key: 'textArea', + input: true, + }); + }); + + it('Should iterate over each component with correct pathing given a container component', () => { + const components = [ + { + type: 'textfield', + key: 'textField', + input: true, + }, + { + type: 'container', + key: 'container', + input: true, + components: [ + { + type: 'textfield', + key: 'nestedTextField', + input: true, + }, + { + type: 'textarea', + key: 'nestedTextArea', + input: true, + } + ] + } + ]; + const rowResults: Map = new Map(); + eachComponent(components, (component: Component, path: string) => { + rowResults.set(path, component); + }, true); + expect(rowResults.size).to.equal(4); + expect(rowResults.get('textField')).to.deep.equal({ + type: 'textfield', + key: 'textField', + input: true, + }); + expect(rowResults.get('container')).to.deep.equal({ + type: 'container', + key: 'container', + input: true, + components: [ + { + type: 'textfield', + key: 'nestedTextField', + input: true, + }, + { + type: 'textarea', + key: 'nestedTextArea', + input: true, + } + ] + }); + expect(rowResults.get('container.nestedTextField')).to.deep.equal({ + type: 'textfield', + key: 'nestedTextField', + input: true, + }); + expect(rowResults.get('container.nestedTextArea')).to.deep.equal({ + type: 'textarea', + key: 'nestedTextArea', + input: true, + }); + }); + + it('Should iterate over each component with correct pathing given a datagrid component', () => { + const components = [ + { + type: 'textfield', + key: 'textField', + input: true, + }, + { + type: 'datagrid', + key: 'dataGrid', + input: true, + components: [ + { + type: 'textfield', + key: 'nestedTextField', + input: true, + }, + { + type: 'textarea', + key: 'nestedTextArea', + input: true, + } + ] + } + ]; + const rowResults: Map = new Map(); + eachComponent(components, (component: Component, path: string) => { + rowResults.set(path, component); + }, true); + expect(rowResults.size).to.equal(4); + expect(rowResults.get('textField')).to.deep.equal({ + type: 'textfield', + key: 'textField', + input: true, + }); + expect(rowResults.get('dataGrid')).to.deep.equal({ + type: 'datagrid', + key: 'dataGrid', + input: true, + components: [ + { + type: 'textfield', + key: 'nestedTextField', + input: true, + }, + { + type: 'textarea', + key: 'nestedTextArea', + input: true, + } + ] + }); + expect(rowResults.get('dataGrid[0].nestedTextField')).to.deep.equal({ + type: 'textfield', + key: 'nestedTextField', + input: true, + }); + expect(rowResults.get('dataGrid[0].nestedTextArea')).to.deep.equal({ + type: 'textarea', + key: 'nestedTextArea', + input: true, + }); + }); + + it('Should iterate over each component with correct pathing given a datagrid\'s child components', () => { + const components = [ + { + type: 'datagrid', + key: 'dataGrid', + input: true, + components: [ + { + type: 'textfield', + key: 'nestedTextField', + input: true, + }, + { + type: 'textarea', + key: 'nestedTextArea', + input: true, + } + ] + } + ]; + const rowResults: Map = new Map(); + eachComponent(components[0].components, (component: Component, path: string) => { + rowResults.set(path, component); + }, true, 'dataGrid[0]'); + expect(rowResults.size).to.equal(2); + expect(rowResults.get('dataGrid[0].nestedTextField')).to.deep.equal({ + type: 'textfield', + key: 'nestedTextField', + input: true, + }); + expect(rowResults.get('dataGrid[0].nestedTextArea')).to.deep.equal({ + type: 'textarea', + key: 'nestedTextArea', + input: true, + }); + }); +}); + +describe('eachComponentData', () => { + it('Should iterate over each component and data given a flat components array', () => { + const components = [ + { + type: 'textfield', + key: 'textField', + label: 'Text Field', + input: true, + }, + { + type: 'textarea', + key: 'textArea', + label: 'Text Area', + input: true, + } + ]; + const data = { + textField: 'hello', + textArea: 'world', + }; + const rowResults: Map = new Map(); + eachComponentData(components, data, (component, data, row, path) => { + const value = get(data, path); + rowResults.set(path, [component, value]); + }); + expect(rowResults.size).to.equal(2); + expect(rowResults.get('textField')).to.deep.equal([ + { + type: 'textfield', + key: 'textField', + label: 'Text Field', + input: true, + }, + 'hello' + ]); + expect(rowResults.get('textArea')).to.deep.equal([ + { + type: 'textarea', + key: 'textArea', + label: 'Text Area', + input: true, + }, + 'world' + ]); + }); + + it('Should iterate over each component and data given a container component and a nested components array', () => { + const components = [ + { + type: 'textfield', + key: 'textField', + label: 'Text Field', + input: true, + }, + { + type: 'container', + key: 'container', + label: 'Container', + input: true, + components: [ + { + type: 'textfield', + key: 'nestedTextField', + label: 'Nested Text Field', + input: true, + }, + { + type: 'textarea', + key: 'nestedTextArea', + label: 'Nested Text Area', + input: true, + } + ] + } + ]; + const data = { + textField: 'hello', + container: { + nestedTextField: 'world', + nestedTextArea: 'foo', + }, + }; + const rowResults: Map = new Map(); + eachComponentData(components, data, (component, data, row, path) => { + const value = get(data, path); + rowResults.set(path, [component, value]); + }); + expect(rowResults.size).to.equal(4); + expect(rowResults.get('textField')).to.deep.equal([ + { + type: 'textfield', + key: 'textField', + label: 'Text Field', + input: true, + }, + 'hello' + ]); + expect(rowResults.get('container')).to.deep.equal([ + { + type: 'container', + key: 'container', + label: 'Container', + input: true, + components: [ + { + type: 'textfield', + key: 'nestedTextField', + label: 'Nested Text Field', + input: true, + }, + { + type: 'textarea', + key: 'nestedTextArea', + label: 'Nested Text Area', + input: true, + } + ] + }, + { + nestedTextField: 'world', + nestedTextArea: 'foo', + } + ]); + expect(rowResults.get('container.nestedTextField')).to.deep.equal([ + { + type: 'textfield', + key: 'nestedTextField', + label: 'Nested Text Field', + input: true, + }, + 'world' + ]); + expect(rowResults.get('container.nestedTextArea')).to.deep.equal([ + { + type: 'textarea', + key: 'nestedTextArea', + label: 'Nested Text Area', + input: true, + }, + 'foo' + ]); + }); + + it('Should iterate over each component and data given a datagrid component and a nested components array', () => { + const components = [ + { + type: 'textfield', + key: 'textField', + label: 'Text Field', + input: true, + }, + { + type: 'datagrid', + key: 'dataGrid', + label: 'Data Grid', + input: true, + components: [ + { + type: 'textfield', + key: 'nestedTextField', + label: 'Nested Text Field', + input: true, + }, + { + type: 'textarea', + key: 'nestedTextArea', + label: 'Nested Text Area', + input: true, + } + ] + } + ]; + const data = { + textField: 'hello', + dataGrid: [ + { + nestedTextField: 'world', + nestedTextArea: 'foo', + }, + { + nestedTextField: 'bar', + nestedTextArea: 'baz', + } + ], + }; + const rowResults: Map = new Map(); + eachComponentData(components, data, (component, data, row, path) => { + const value = get(data, path); + rowResults.set(path, [component, value]); + }); + expect(rowResults.size).to.equal(6); + expect(rowResults.get('textField')).to.deep.equal([ + { + type: 'textfield', + key: 'textField', + label: 'Text Field', + input: true, + }, + 'hello' + ]); + expect(rowResults.get('dataGrid')).to.deep.equal([ + { + type: 'datagrid', + key: 'dataGrid', + label: 'Data Grid', + input: true, + components: [ + { + type: 'textfield', + key: 'nestedTextField', + label: 'Nested Text Field', + input: true, + }, + { + type: 'textarea', + key: 'nestedTextArea', + label: 'Nested Text Area', + input: true, + } + ] + }, + [ + { + nestedTextField: 'world', + nestedTextArea: 'foo', + }, + { + nestedTextField: 'bar', + nestedTextArea: 'baz', + } + ] + ]); + expect(rowResults.get('dataGrid[0].nestedTextField')).to.deep.equal([ + { + type: 'textfield', + key: 'nestedTextField', + label: 'Nested Text Field', + input: true, + }, + 'world' + ]); + }); +}); diff --git a/src/utils/formUtil.ts b/src/utils/formUtil.ts index e73b655c..8f5f565d 100644 --- a/src/utils/formUtil.ts +++ b/src/utils/formUtil.ts @@ -201,6 +201,9 @@ export const componentChildPath = (component: any, parentPath?: string, path?: s if (isComponentModelType(component, 'dataObject')) { return `${path}.data`; } + if (isComponentModelType(component, 'array')) { + return `${path}[0]`; + } if (isComponentNestedDataType(component)) { return path; } @@ -1112,8 +1115,8 @@ export function isComponentDataEmpty(component: Component, data: any, path: stri } else if (isDataGridComponent(component) || isEditGridComponent(component) || isDataTableComponent(component) || hasChildComponents(component)) { if (component.components?.length) { let childrenEmpty = true; - // TODO: eachComponentData currently can't handle passing child components directly because it won't get the path right; - // wrapping component in an array and skipping it's callback is a workaround to start with the correct path, but it is not ideal + // wrap component in an array to let eachComponentData handle introspection to child components (e.g. this will be different + // for data grids versus nested forms, etc.) eachComponentData([component], data, (thisComponent, data, row, path, components, index) => { if (component.key === thisComponent.key) return; if (!isComponentDataEmpty(thisComponent, data, path)) { diff --git a/src/utils/unwind.ts b/src/utils/unwind.ts index e50e1a5d..1ca2a4ac 100644 --- a/src/utils/unwind.ts +++ b/src/utils/unwind.ts @@ -141,7 +141,7 @@ export function unwind(form: any, submission: any) { var paths = filter(path.replace(new RegExp(".?" + component.key + "$"), '').split('.')); /* eslint-enable no-useless-escape */ if (!hasDataPath && paths.length && !component.isInsideNestedForm) { - key = paths.map(function (subpath: any) { return subpath + "[0]"; }).join('.') + "." + component.key; + key = paths.join('.') + "." + component.key; } if (component.multiple) { paths.push(component.key);