diff --git a/src/components/radio/Radio.js b/src/components/radio/Radio.js index 1299021087..35195262bc 100644 --- a/src/components/radio/Radio.js +++ b/src/components/radio/Radio.js @@ -2,6 +2,7 @@ import _ from 'lodash'; import ListComponent from '../_classes/list/ListComponent'; import { Formio } from '../../Formio'; import { boolValue, componentValueTypes, getComponentSavedTypes } from '../../utils/utils'; +import { v4 as uuidv4 } from 'uuid'; export default class RadioComponent extends ListComponent { static schema(...extend) { @@ -164,6 +165,7 @@ export default class RadioComponent extends ListComponent { }); this.optionsLoaded = !this.component.dataSrc || this.component.dataSrc === 'values'; this.loadedOptions = []; + this.valuesMap = new Map(); if (!this.visible) { this.itemsLoadedResolve(); @@ -211,9 +213,12 @@ export default class RadioComponent extends ListComponent { dataValue = _.toString(this.dataValue); } - if (this.isSelectURL && _.isObject(this.loadedOptions[index].value)) { - const optionValue = this.component.dataType === 'string' ? JSON.stringify(this.loadedOptions[index].value) : this.loadedOptions[index].value; - input.checked = _.isEqual(optionValue, this.dataValue); + if (this.isSelectURL) { + const valueKey = this.loadedOptions[index].value; + const optionValue = this.valuesMap.has(valueKey) + ? this.valuesMap.get(valueKey) + : valueKey; + input.checked = _.isEqual(this.normalizeValue(optionValue), this.dataValue); } else { input.checked = (dataValue === input.value && (input.value || this.component.dataSrc !== 'url')); @@ -253,9 +258,15 @@ export default class RadioComponent extends ListComponent { let value = this.component.inputType === 'checkbox' ? '' : this.dataValue; this.refs.input.forEach((input, index) => { if (input.checked) { - value = (this.isSelectURL && _.isObject(this.loadedOptions[index].value)) ? - this.loadedOptions[index].value : - input.value; + if (!this.isSelectURL) { + value = input.value; + return; + } + + const optionValue = this.loadedOptions[index].value; + value = this.valuesMap.has(optionValue) + ? this.valuesMap.get(optionValue) + : optionValue; } }); return value; @@ -310,8 +321,8 @@ export default class RadioComponent extends ListComponent { setValueAt(index, value) { if (this.refs.input && this.refs.input[index] && value !== null && value !== undefined) { - const inputValue = this.refs.input[index].value; - this.refs.input[index].checked = (inputValue === value.toString()); + const inputValue = this.getValueByInput(this.refs.input[index]); + this.refs.input[index].checked = _.isEqual(inputValue, value); } } @@ -324,6 +335,27 @@ export default class RadioComponent extends ListComponent { return super.shouldLoad; } + prepareValue(item, options = {}) { + const value = this.component.valueProperty && !options.skipValueProperty + ? _.get(item, this.component.valueProperty) + : item; + + if (this.component.type === 'radio' && typeof value !== 'string') { + const uuid = uuidv4(); + this.valuesMap.set(uuid, value); + return uuid; + } + + return value; + } + + getValueByInput(input) { + const inputValue = input.value; + return this.valuesMap.has(inputValue) + ? this.valuesMap.get(inputValue) + : inputValue; + } + loadItems(url, search, headers, options, method, body) { if (this.optionsLoaded) { this.itemsLoadedResolve(); @@ -381,7 +413,7 @@ export default class RadioComponent extends ListComponent { label: this.itemTemplate(item) }; if (_.isEqual(item, this.selectData || _.pick(this.dataValue, _.keys(item)))) { - this.loadedOptions[i].value = this.dataValue; + this.loadedOptions[i].value = this.prepareValue(this.dataValue, { skipValueProperty: true }); } }); this.optionsLoaded = true; @@ -392,13 +424,16 @@ export default class RadioComponent extends ListComponent { const listData = []; items?.forEach((item, i) => { const valueAtProperty = _.get(item, this.component.valueProperty); - this.loadedOptions[i] = { - value: this.component.valueProperty ? valueAtProperty : item, - label: this.component.valueProperty ? this.itemTemplate(item, valueAtProperty) : this.itemTemplate(item, item, i) - }; - listData.push(this.templateData[this.component.valueProperty ? valueAtProperty : i]); + const value = this.prepareValue(item); + const label = this.component.valueProperty + ? this.itemTemplate(item, valueAtProperty, i) + : this.itemTemplate(item, item, i); + this.loadedOptions[i] = { label, value }; + listData.push(this.templateData[i]); + if (this.valuesMap.has(value)) { + this.templateData[value] = this.templateData[i]; + } - const value = this.loadedOptions[i].value; if (!this.isRadio && ( _.isObject(value) || _.isBoolean(value) || _.isUndefined(value) )) { @@ -426,7 +461,9 @@ export default class RadioComponent extends ListComponent { const value = this.dataValue; this.refs.wrapper.forEach((wrapper, index) => { const input = this.refs.input[index]; - const checked = (input.type === 'checkbox') ? value[input.value] || input.checked : (input.value.toString() === value.toString()); + const checked = (input.type === 'checkbox') + ? value[input.value] || input.checked + : _.isEqual(this.normalizeValue(this.getValueByInput(input)), value); if (checked) { //add class to container when selected this.addClass(wrapper, this.optionSelectedClass); @@ -441,10 +478,30 @@ export default class RadioComponent extends ListComponent { } } + setMetadata(value) { + let key = value; + if (typeof value !== 'string') { + const checkedInput = Array.prototype.find.call( + this.refs.input, + (input => input.type === 'radio' && input.getAttribute('checked')) + ); + key = checkedInput?.value || key; + } + if (this.isSelectURL && this.templateData && this.templateData[key]) { + const submission = this.root.submission; + if (!submission.metadata.selectData) { + submission.metadata.selectData = {}; + } + + _.set(submission.metadata.selectData, this.path, this.templateData[key]); + } + } + updateValue(value, flags) { const changed = super.updateValue(value, flags); if (changed) { this.setSelectedClasses(); + this.setMetadata(this.dataValue); } if (!flags || !flags.modified || !this.isRadio) { @@ -507,15 +564,10 @@ export default class RadioComponent extends ListComponent { break; } - if (this.isSelectURL && this.templateData && this.templateData[value]) { - const submission = this.root.submission; - if (!submission.metadata.selectData) { - submission.metadata.selectData = {}; - } - - _.set(submission.metadata.selectData, this.path, this.templateData[value]); - } - return super.normalizeValue(value); } + + isSingleInputValue() { + return true; + } } diff --git a/test/unit/Radio.unit.js b/test/unit/Radio.unit.js index a4fd032280..66578c0d98 100644 --- a/test/unit/Radio.unit.js +++ b/test/unit/Radio.unit.js @@ -16,7 +16,8 @@ import { comp9, comp10, comp11, - comp13 + comp13, + comp14, } from './fixtures/radio'; import { fastCloneDeep } from '@formio/core'; @@ -278,7 +279,8 @@ describe('Radio Component', () => { setTimeout(()=>{ values.forEach((value, i) => { - assert.equal(_.isEqual(value, radio.loadedOptions[i].value), true); + assert.equal(radio.loadedOptions[i].label, `${value.name}`); + assert.equal(typeof radio.loadedOptions[i].value, 'string'); }); radio.setValue(values[1]); @@ -343,9 +345,7 @@ describe('Radio Component', () => { }, 350); }).catch(done); }); -}); -describe('Radio Component', () => { it('should have red asterisk left hand side to the options labels if component is required and label is hidden', () => { return Harness.testCreate(RadioComponent, comp7).then(component => { const options = component.element.querySelectorAll('.form-check-label'); @@ -609,4 +609,269 @@ describe('Radio Component', () => { }) .catch(done); }); + + describe('Value property refers to different type of values', () => { + const originalMakeRequest = Formio.makeRequest; + const values = [ + { + label: 'String', + value: 'str', + }, + { + label: 'Object', + value: { + a: 2, + b: 'c', + } + }, + { + label: 'Boolean', + value: true, + }, + { + label: 'Array', + value: [ + 10, + 1330, + '132410', + ] + }, + { + label: 'Number', + value: 5, + } + ]; + + const listDataMetadata = { + radio: [ + { + label: 'String' + }, + { + label: 'Object' + }, + { + label: 'Boolean' + }, + { + label: 'Array' + }, + { + label: 'Number' + } + ] + }; + + before(() => { + Formio.makeRequest = function() { + return new Promise(resolve => { + resolve(values); + }); + }; + }); + + after (() => { + Formio.makeRequest = originalMakeRequest; + }); + + it('Should create correct options', (done) => { + const form = _.cloneDeep(comp14); + const element = document.createElement('div'); + Formio.createForm(element, form).then(form => { + const radio = form.getComponent('radio'); + + setTimeout(() => { + assert.equal(radio.loadedOptions.length, values.length); + values.forEach((value, i) => { + assert.equal(radio.loadedOptions[i].label, `${value.label}`); + assert.equal(typeof radio.loadedOptions[i].value, 'string'); + assert.deepEqual(form.submission.metadata.listData, listDataMetadata); + }); + done(); + }, 200); + }).catch(done); + }); + + it('Should set value by setValue', (done) => { + const form = _.cloneDeep(comp14); + const element = document.createElement('div'); + Formio.createForm(element, form).then(form => { + const radio = form.getComponent('radio'); + setTimeout(() => { + values.forEach(({label, value}, i) => { + radio.setValue(value); + assert.equal(radio.refs.input[i].checked, true); + assert.deepEqual(radio.dataValue, value); + assert.deepEqual(form.submission.metadata.selectData.radio, { + label, + }); + }); + + done(); + }, 200); + }).catch(done); + }); + + it('Should work with String value type', (done) => { + const form = _.cloneDeep(comp14); + const element = document.createElement('div'); + Formio.createForm(element, form).then(form => { + const radio = form.getComponent('radio'); + + setTimeout(() => { + assert.equal(radio.refs.input.length, values.length); + const input = radio.refs.input[0]; + input.click(); + + setTimeout(() => { + assert.equal(radio.dataValue, values[0].value); + assert.deepEqual(form.submission.metadata.selectData.radio, { + label: values[0].label, + }); + done(); + }, 200); + }, 200); + }).catch(done); + }); + + it('Should work with Object value type', (done) => { + const form = _.cloneDeep(comp14); + const element = document.createElement('div'); + Formio.createForm(element, form).then(form => { + const radio = form.getComponent('radio'); + + setTimeout(() => { + assert.equal(radio.refs.input.length, values.length); + const input = radio.refs.input[1]; + input.click(); + + setTimeout(() => { + assert.deepEqual(radio.dataValue, values[1].value); + assert.deepEqual(form.submission.metadata.selectData.radio, { + label: values[1].label, + }); + done(); + }, 200); + }, 200); + }).catch(done); + }); + + it('Should work with Boolean value type', (done) => { + const form = _.cloneDeep(comp14); + const element = document.createElement('div'); + Formio.createForm(element, form).then(form => { + const radio = form.getComponent('radio'); + + setTimeout(() => { + assert.equal(radio.refs.input.length, values.length); + const input = radio.refs.input[2]; + input.click(); + + setTimeout(() => { + assert.equal(radio.dataValue, values[2].value); + assert.deepEqual(form.submission.metadata.selectData.radio, { + label: values[2].label, + }); + done(); + }, 200); + }, 200); + }).catch(done); + }); + + it('Should work with Array value type', (done) => { + const form = _.cloneDeep(comp14); + const element = document.createElement('div'); + Formio.createForm(element, form).then(form => { + const radio = form.getComponent('radio'); + + setTimeout(() => { + assert.equal(radio.refs.input.length, values.length); + const input = radio.refs.input[3]; + input.click(); + + setTimeout(() => { + assert.deepEqual(radio.dataValue, values[3].value); + assert.deepEqual(form.submission.metadata.selectData.radio, { + label: values[3].label, + }); + done(); + }, 200); + }, 200); + }).catch(done); + }); + + it('Should work with Number value type', (done) => { + const form = _.cloneDeep(comp14); + const element = document.createElement('div'); + Formio.createForm(element, form).then(form => { + const radio = form.getComponent('radio'); + + setTimeout(() => { + assert.equal(radio.refs.input.length, values.length); + const input = radio.refs.input[4]; + input.click(); + + setTimeout(() => { + assert.equal(radio.dataValue, values[4].value); + assert.deepEqual(form.submission.metadata.selectData.radio, { + label: values[4].label, + }); + done(); + }, 200); + }, 200); + }).catch(done); + }); + + it('Should check input with Object value type using submission data', (done) => { + const form = _.cloneDeep(comp14); + const element = document.createElement('div'); + Formio.createForm(element, form).then(form => { + form.setSubmission({ + data: { + radio: values[1].value, + }, + metadata: { + listData: listDataMetadata, + selectData: { + radio: { + label: values[1].label, + } + } + } + }).then(() => { + const radio = form.getComponent('radio'); + setTimeout(() => { + assert.equal(radio.refs.input[1].checked, true); + done(); + }, 200); + }) + }).catch(done); + }); + + it('Should check input with Array value type using submission data', (done) => { + const form = _.cloneDeep(comp14); + const element = document.createElement('div'); + Formio.createForm(element, form).then(form => { + form.setSubmission({ + data: { + radio: values[3].value, + }, + metadata: { + listData: listDataMetadata, + selectData: { + radio: { + label: values[3].label, + } + } + } + }).then(() => { + const radio = form.getComponent('radio'); + setTimeout(() => { + assert.equal(radio.refs.input[3].checked, true); + done(); + }, 200); + }) + }).catch(done); + }); + }); }); diff --git a/test/unit/fixtures/radio/comp14.js b/test/unit/fixtures/radio/comp14.js new file mode 100644 index 0000000000..27996817b4 --- /dev/null +++ b/test/unit/fixtures/radio/comp14.js @@ -0,0 +1,30 @@ +export default { + type: 'form', + components: [ + { + type: 'radio', + label: 'Radio', + key: 'radio', + dataSrc: 'url', + data: { + url: 'https://cdn.rawgit.com/mshafrir/2646763/raw/states_titlecase.json' + }, + valueProperty: 'value', + template: '{{ item.label }}', + input: true + }, + { + label: 'Submit', + showValidations: false, + alwaysEnabled: false, + tableView: false, + key: 'submit', + type: 'button', + input: true + } + ], + title: 'FIO-7225', + display: 'form', + name: 'fio7225', + path: 'fio7225', +}; diff --git a/test/unit/fixtures/radio/index.js b/test/unit/fixtures/radio/index.js index fcbdfcbbda..78b8f8a76e 100644 --- a/test/unit/fixtures/radio/index.js +++ b/test/unit/fixtures/radio/index.js @@ -11,4 +11,5 @@ import comp10 from './comp10'; import comp11 from './comp11'; import comp12 from './comp12'; import comp13 from './comp13'; -export { comp1, comp2, comp3, comp4, comp5, comp6, comp7, comp8, comp9, comp10, comp11, comp12, comp13 }; +import comp14 from './comp14'; +export { comp1, comp2, comp3, comp4, comp5, comp6, comp7, comp8, comp9, comp10, comp11, comp12, comp13, comp14 };