From e012b8b105e843429c1eb63139c732333c0a95aa Mon Sep 17 00:00:00 2001 From: James Lucas Date: Thu, 19 Oct 2023 12:06:11 +1100 Subject: [PATCH 1/4] fix: don't bother lifting col/row classes if we don't have any to lift --- src/js/layout.js | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/src/js/layout.js b/src/js/layout.js index bc37077aa..97400078b 100644 --- a/src/js/layout.js +++ b/src/js/layout.js @@ -12,22 +12,22 @@ const processClassName = (data, field) => { if (classes && classes.length > 0) { className += ` ${classes.join(' ')}` - } - // Now that the row- & col- types were lifted, remove from the actual input field and any child elements - if (field.classList) { - field.classList.remove(...classes) - } + // Now that the row- & col- types were lifted, remove from the actual input field and any child elements + if (field.classList) { + field.classList.remove(...classes) + } - //field may be a single element, dom fragment, or an array of elements - if (!Array.isArray(field)) { - field = [field] - } - field.forEach(item => item.querySelectorAll('[class*=row-],[class*=col-]').forEach(element => { - if (element.classList) { - element.classList.remove(...classes) + //field may be a single element, dom fragment, or an array of elements + if (!Array.isArray(field)) { + field = [field] } - })) + field.forEach(item => item.querySelectorAll('[class*=row-],[class*=col-]').forEach(element => { + if (element.classList) { + element.classList.remove(...classes) + } + })) + } } return className From 62b8e3bf03b618103c871a1c3975553bc5d877c6 Mon Sep 17 00:00:00 2001 From: James Lucas Date: Thu, 19 Oct 2023 12:30:54 +1100 Subject: [PATCH 2/4] fix: fix lifting bootstrap col/row fields for autocomplete --- src/js/layout.js | 17 +++++++++-------- tests/form-builder.test.js | 17 +++++++++++++++++ 2 files changed, 26 insertions(+), 8 deletions(-) diff --git a/src/js/layout.js b/src/js/layout.js index 97400078b..a1d72eae2 100644 --- a/src/js/layout.js +++ b/src/js/layout.js @@ -14,19 +14,20 @@ const processClassName = (data, field) => { className += ` ${classes.join(' ')}` // Now that the row- & col- types were lifted, remove from the actual input field and any child elements - if (field.classList) { - field.classList.remove(...classes) - } - //field may be a single element, dom fragment, or an array of elements if (!Array.isArray(field)) { field = [field] } - field.forEach(item => item.querySelectorAll('[class*=row-],[class*=col-]').forEach(element => { - if (element.classList) { - element.classList.remove(...classes) + field.forEach(item => { + if (item.classList) { + item.classList.remove(...classes) } - })) + item.querySelectorAll('[class*=row-],[class*=col-]').forEach(element => { + if (element.classList) { + element.classList.remove(...classes) + } + }) + }) } } diff --git a/tests/form-builder.test.js b/tests/form-builder.test.js index b50ab5a71..e37612909 100644 --- a/tests/form-builder.test.js +++ b/tests/form-builder.test.js @@ -333,6 +333,23 @@ describe('FormBuilder typeUserAttrs detection', () => { input = fbWrap.find('.button-field .testAttribute-wrap label') expect(input.text()).toBe('override') }) + test('lifts bootstrap classes from field', async () => { + const fbWrap = $('
') + const fb = await fbWrap.formBuilder({}).promise + fb.actions.addField({ type: 'text', className: 'form-control row-1 col-md-4'}) + const input = fbWrap.find('.form-field[type="text"] .prev-holder input') + expect(input.attr('class')).toContain('form-control') + expect(input.attr('class')).not.toContain('row-1') + expect(input.attr('class')).not.toContain('col-md-4') + fb.actions.addField({ type: 'select', className: 'form-control row-1 col-md-4', values: {'yes':'yes','no':'no'}}) + const select = fbWrap.find('.form-field[type="select"] .prev-holder select') + expect(select.attr('class')).toContain('form-control') + expect(select.attr('class')).not.toContain('row-1') + expect(select.attr('class')).not.toContain('col-md-4') + fb.actions.addField({ type: 'autocomplete', className: 'form-control row-1 col-md-4'}) + const auto = fbWrap.find('.form-field[type="autocomplete"] .prev-holder .form-group') + expect(auto.find('[class*=row-],[class*=col-]')).toHaveLength(0) + }) }) describe('FormBuilder can return formData', () => { From 2dde66a17b498c267d1cc63238abbaaac76b6968 Mon Sep 17 00:00:00 2001 From: James Lucas Date: Thu, 19 Oct 2023 12:31:47 +1100 Subject: [PATCH 3/4] update documentation to remove invalid calls on formBuilder instance --- docs/formRender/actions/clear.md | 17 ++++++++------ docs/formRender/actions/html.md | 4 ++-- docs/formRender/actions/render.md | 8 +++---- docs/formRender/actions/setData.md | 5 ++-- docs/formRender/actions/userData.md | 36 ++++++++++++++--------------- 5 files changed, 37 insertions(+), 33 deletions(-) diff --git a/docs/formRender/actions/clear.md b/docs/formRender/actions/clear.md index c3d4dca4f..70e072cc3 100644 --- a/docs/formRender/actions/clear.md +++ b/docs/formRender/actions/clear.md @@ -3,15 +3,18 @@ Clears all user data from the form, even tinyMCE. ## Usage -
const formRenderOptions = {
+```javascript
+const formRenderOptions = {
   formData: '[{"type":"text","label":"Text Field","name":"text-1526099104236","subtype":"text"}]';
 }
-const formRenderInstance = $('#render-container').formRender(formRenderOptions);
-const html = formRenderInstance('html'); // HTML DOM nodes
+const renderContainer = $('#render-container')
+const formRenderInstance = renderContainer.formRender(formRenderOptions);
+const html = renderContainer.formRender('html'); // HTML DOM nodes
 const htmlString = html.outerHTML;
-
+``` // User enters data -
formRenderInstance.clear();
-// or
-$('#render-container').formRender('clear')
+ +```javascript +$('#render-container').formRender('clear') +``` // User entered data is cleared diff --git a/docs/formRender/actions/html.md b/docs/formRender/actions/html.md index 74c0d52eb..e646d54d6 100644 --- a/docs/formRender/actions/html.md +++ b/docs/formRender/actions/html.md @@ -3,9 +3,9 @@ To grab the rendered form's HTML complete with user entered data ## Usage -

+```javascript
 const html = $('#render-container').formRender('html'); // HTML string
-
+``` ### In Action

diff --git a/docs/formRender/actions/render.md b/docs/formRender/actions/render.md index 5acc27637..2c334cc33 100644 --- a/docs/formRender/actions/render.md +++ b/docs/formRender/actions/render.md @@ -1,6 +1,6 @@ # `render` action -Programmtically render or re-render a formRender instance with new data +Programmatically render or re-render a formRender instance with new data Arguments | Arg | Type | Value(s) | Default | @@ -9,9 +9,9 @@ Arguments | options | {Object} | override options for new render | {} | ## Usage -
const wrap = $('.render-wrap');
+```javascript
+const wrap = $('.render-wrap');
 const formRender = wrap.formRender();
 // then
 wrap.formRender('render', formData);
-// or
-formRender.actions.render(formData)
+``` \ No newline at end of file diff --git a/docs/formRender/actions/setData.md b/docs/formRender/actions/setData.md index ae3d0b5a9..de2a587b8 100644 --- a/docs/formRender/actions/setData.md +++ b/docs/formRender/actions/setData.md @@ -3,11 +3,12 @@ Set formData *without* rendering or re-initializing formRender. ## Usage -
const wrap = $('.render-wrap');
+```javascript
+const wrap = $('.render-wrap');
 const formRender = wrap.formRender();
 // then
 wrap.formRender('setData', formData);
-
+``` ## Demo

See the Pen

diff --git a/docs/formRender/actions/userData.md b/docs/formRender/actions/userData.md index dd57335a7..7dd0182c9 100644 --- a/docs/formRender/actions/userData.md +++ b/docs/formRender/actions/userData.md @@ -4,46 +4,46 @@ The UserData option aims to enable formRender to both capture the form data befo ## Usage 1. Setup formRender with base definition as normal -
var formRenderOptions = {
+```javascript
+var formRenderOptions = {
   formData: '[{"type":"text","label":"Text Field","name":"text-1526099104236","subtype":"text"}]';
 }
 var formRenderInstance = $('#render-container').formRender(formRenderOptions);
-
+``` 1. After the user has entered data into the rendered form, their data is accessible from `formRenderInstance.userData` or `$('#render-container').formRender('userData')` -
console.log(formRenderInstance.userData);
+```javascript
+console.log(formRenderInstance.userData);
 // "[{"type":"text","label":"Full Name","name":"text-1526099104236","subtype":"text","userData":["John Smith"]}]"
-
+``` UserData works for autocomplete, select, checkbox-group, radio-group, text, email, color, tel, number, hidden, date, textarea, textarea-tinymce. For fields that have an "other" option, a value that is not in the pre-defined values is assumed to be the "other" value. -
var formData = '[{"type":"checkbox-group","label":"Checkbox Group","name":"checkbox-group-1526095813035","other":true,"values":[{"label":"Option 1","value":"option-1"},{"label":"2","value":"2"}],"userData":["option-1","Bilbo \\\"baggins\\\""]}]';
+```javascript
+var formData = '[{"type":"checkbox-group","label":"Checkbox Group","name":"checkbox-group-1526095813035","other":true,"values":[{"label":"Option 1","value":"option-1"},{"label":"2","value":"2"}],"userData":["option-1","Bilbo \\\"baggins\\\""]}]';
 
 var formRenderOptions = {formData};
-var frInstance = $('#renderMe').formRender(formRenderOptions);
+var frInstance = $('#renderMe').formRender(formRenderOptions); +``` A common use case for userData would be to create a form, save the input data from the form into a database, and then refresh the page with the saved data. Simply stringify userData when posting. 1. Capture form data -
-
-    $.ajax({
+```javascript
+$.ajax({
     type: "POST",
     url: url,
     data: JSON.stringify(frInstance.userData)
-    });
-
-
+}); +``` 2. Send the saved JSON data back into formRender to display data -
-
-  var fbOptions.formData = {SavedJsonFromDatabase};
-  var frInstance = $('#renderMe').formRender(fbOptions);
-
-
+```javascript +var fbOptions.formData = {SavedJsonFromDatabase}; +var frInstance = $('#renderMe').formRender(fbOptions); +``` ## Demo

See the Pen

From 48700de9984295a77c8e7a7f45350cfbe9f7f928 Mon Sep 17 00:00:00 2001 From: James Lucas Date: Thu, 19 Oct 2023 12:35:12 +1100 Subject: [PATCH 4/4] fix: initialise formRender with an empty formData in cases when no formData is provided, otherwise we are unable to perform setData/render functions on the container --- src/js/form-render.js | 63 ++++++++++++++++++--------------------- tests/form-render.test.js | 52 +++++++++++++++++++++++++------- 2 files changed, 71 insertions(+), 44 deletions(-) diff --git a/src/js/form-render.js b/src/js/form-render.js index 77851ed3c..88b59b6ba 100644 --- a/src/js/form-render.js +++ b/src/js/form-render.js @@ -28,7 +28,7 @@ class FormRender { container: false, // string selector or Node element dataType: 'json', disableHTMLLabels: false, - formData: false, + formData: [], i18n: Object.assign({}, defaultI18n), messages: { formRendered: 'Form Rendered', @@ -72,12 +72,12 @@ class FormRender { } // parse any passed formData - if (!this.options.formData) { - return false + if (this.options.formData) { + this.options.formData = this.parseFormData(this.options.formData) + } else { + this.options.formData = [] } - this.options.formData = this.parseFormData(this.options.formData) - // ability for controls to have their own configuration / options of the format control identifier (type, or type.subtype): {options} control.controlConfig = options.controlConfig || {} @@ -144,7 +144,7 @@ class FormRender { /** * Clean up passed object configuration to prepare for use with the markup function * @param {Object} field - object of field configuration - * @param {number} instanceIndex - instance index + * @param {number} [instanceIndex] - instance index * @return {Object} sanitized field object */ sanitizeField(field, instanceIndex) { @@ -198,10 +198,9 @@ class FormRender { // Begin the core plugin const rendered = [] - // generate field markup if we have fields - if (opts.formData) { - // instantiate the layout class & loop through the field configuration - const engine = new opts.layout(opts.layoutTemplates, false, opts.disableHTMLLabels) + // instantiate the layout class & loop through the field configuration + const engine = new opts.layout(opts.layoutTemplates, false, opts.disableHTMLLabels) + if (opts.formData.length) { for (let i = 0; i < opts.formData.length; i++) { const fieldData = opts.formData[i] const sanitizedField = this.sanitizeField(fieldData, instanceIndex) @@ -212,33 +211,29 @@ class FormRender { rendered.push(field) } + } else { + opts.notify.warning(opts.messages.noFormData) + } - if (element) { - this.instanceContainers[instanceIndex] = element - } + if (element) { + this.instanceContainers[instanceIndex] = element + } - // if rendering, inject the fields into the specified wrapper container/element - if (opts.render && element) { - element.emptyContainer() - element.appendFormFields(rendered) - - runCallbacks() - opts.notify.success(opts.messages.formRendered) - } else { - /** - * Retrieve the html markup for a passed array of DomElements - * @param {Array} fields - array of dom elements - * @return {string} fields html - */ - const exportMarkup = fields => fields.map(elem => elem.innerHTML).join('') - formRender.markup = exportMarkup(rendered) - } + // if rendering, inject the fields into the specified wrapper container/element + if (opts.render && element) { + element.emptyContainer() + element.appendFormFields(rendered) + + runCallbacks() + opts.notify.success(opts.messages.formRendered) } else { - const noData = utils.markup('div', opts.messages.noFormData, { - className: 'no-form-data', - }) - rendered.push(noData) - opts.notify.error(opts.messages.noFormData) + /** + * Retrieve the html markup for a passed array of DomElements + * @param {Array} fields - array of dom elements + * @return {string} fields html + */ + const exportMarkup = fields => fields.map(elem => elem.innerHTML).join('') + formRender.markup = exportMarkup(rendered) } if (opts.disableInjectedStyle === true) { diff --git a/tests/form-render.test.js b/tests/form-render.test.js index 4e32b7c9a..7c8133a85 100644 --- a/tests/form-render.test.js +++ b/tests/form-render.test.js @@ -6,21 +6,13 @@ const basicTextAreaDef = { 'required': false, 'label': 'Text Area', 'className': 'form-control', + 'description': 'A nice help bubble', 'name': 'textarea-elem', 'access': false, 'subtype': 'textarea', 'userData': ['AValue'], } -describe('FormRender invalid setup', () => { - test('will fail on no formData', () => { - const errors = [], warnings = [], success = [] - $('
').formRender({notify: errorHandler(errors,warnings,success)}) - expect(errors[0]).toBe('No form data.') - expect(success).toHaveLength(0) - }) -}) - describe('Form Rendering', () => { test('can render json form document', () => { const container = $('
') @@ -31,7 +23,7 @@ describe('Form Rendering', () => { const html = container.formRender('html') expect(success[0]).toBe('Form Rendered') - expect(html).toContain('') + expect(html).toContain('') expect(container.find('textarea[name=textarea-elem]').val()).toBe('AValue') }) @@ -81,4 +73,44 @@ describe('Form Rendering', () => { fb.clear() expect(fb.userData[0].userData).not.toEqual(['string to find']) }) + + test('will load empty form on no formData', () => { + const errors = [], warnings = [], success = [] + $('
').formRender({notify: errorHandler(errors,warnings,success)}) + expect(warnings[0]).toBe('No form data.') + expect(success).toHaveLength(1) + }) + + test('will load empty form on undefined formData', () => { + const errors = [], warnings = [], success = [] + $('
').formRender({notify: errorHandler(errors,warnings,success), formData: undefined}) + expect(warnings[0]).toBe('No form data.') + expect(success).toHaveLength(1) + }) + + test('Can set data then render after initialisation of empty form', () => { + const errors = [], warnings = [], success = [] + const container = $('
') + container.formRender({notify: errorHandler(errors,warnings,success)}) + container.formRender('setData', [basicTextAreaDef]) + let textarea = container.find('#textarea-elem') + expect(textarea).toHaveLength(0) + + container.formRender('render') + + textarea = container.find('#textarea-elem') + expect(textarea).not.toHaveLength(0) + }) + + test('Can render after initialisation of empty form', () => { + const errors = [], warnings = [], success = [] + const container = $('
') + container.formRender({notify: errorHandler(errors,warnings,success)}) + let textarea = container.find('#textarea-elem') + expect(textarea).toHaveLength(0) + + container.formRender('render', [basicTextAreaDef]) + textarea = container.find('#textarea-elem') + expect(textarea).not.toHaveLength(0) + }) }) \ No newline at end of file