From 501eebdb36a393804f3114ed684f9653bd11b32e Mon Sep 17 00:00:00 2001 From: Juan Polanco Date: Wed, 9 Mar 2022 11:17:10 -0500 Subject: [PATCH 01/50] create the createFormPropertySource and add it to the global property sources --- src/lib/props/propDefinitions.types.ts | 9 ++- .../createFormPropertySource.ts | 64 +++++++++++++++++++ src/lib/utils/global.ts | 2 + 3 files changed, 74 insertions(+), 1 deletion(-) create mode 100644 src/lib/props/property-sources/createFormPropertySource.ts diff --git a/src/lib/props/propDefinitions.types.ts b/src/lib/props/propDefinitions.types.ts index 839279fe..3e9afdf3 100644 --- a/src/lib/props/propDefinitions.types.ts +++ b/src/lib/props/propDefinitions.types.ts @@ -6,6 +6,7 @@ import type { RefElementType } from '../refs/refDefinitions.types'; export type SourceOption = | SourceOptionCss | SourceOptionHtmlText + | SourceOptionForm | { type?: 'data' | 'json' | 'attr'; target?: string; @@ -19,6 +20,12 @@ export type SourceOptionHtmlText = { target?: string; }; +export type SourceOptionForm = { + type: 'form'; + target?: string; + name?: string; +}; + export type SourceOptionCss = { type: 'css'; target?: string; @@ -52,7 +59,7 @@ export type PropTypeInfo = Pick< > & { name: string; source: { - name: string; + name?: string; target: RefElementType | undefined; } & Pick & Pick; diff --git a/src/lib/props/property-sources/createFormPropertySource.ts b/src/lib/props/property-sources/createFormPropertySource.ts new file mode 100644 index 00000000..17e03a61 --- /dev/null +++ b/src/lib/props/property-sources/createFormPropertySource.ts @@ -0,0 +1,64 @@ +import dedent from 'ts-dedent'; +import type { PropertySource } from '../getComponentProps'; +import { convertSourceValue } from './convertSourceValue'; + +export function createFormPropertySource(): PropertySource { + return () => { + return { + sourceName: 'form', + hasProp: (propInfo) => Boolean(propInfo.source.target && propInfo.type !== Function), + getProp: (propInfo) => { + // let value; + const element = propInfo.source.target; + + const isForm = element && element.nodeName === 'FORM'; + const isInput = element && element?.nodeName === 'INPUT'; + + if (!isForm && !isInput) { + console.warn( + dedent`The property "${propInfo.name}" of type "${propInfo.type.name}" requires a valid 'form' or 'input' element + Returning "undefined".`, + ); + return undefined; + } + + if (isForm) { + const formData = new FormData(element as HTMLFormElement); + const childInputValue = formData.get(propInfo.source.name || ''); + + if (propInfo.type !== Object && !propInfo.source.name) { + console.warn( + dedent`The property "${propInfo.name}" is trying to get a FormData object but is type "${propInfo.type.name}", set it as type "Object" + Returning "undefined".`, + ); + return undefined; + } + return childInputValue + ? convertSourceValue(propInfo, childInputValue as string) + : formData; + } + + if (isInput) { + return (element as HTMLInputElement).value; + } + + return undefined; + + /* if(isValidInput) { + const formData = new FormData(element as HTMLFormElement); + return formData.get(propInfo.source.name) || formData; + } + + if(value != undefined) { + return convertSourceValue(propInfo, value); + } */ + + // const getDirectValue = propInfo.source.target?.nodeType + /* propInfo.name + propInfo.source + propInfo.type + propInfo.isOptional */ + }, + }; + }; +} diff --git a/src/lib/utils/global.ts b/src/lib/utils/global.ts index e493bf6e..815e8bc5 100644 --- a/src/lib/utils/global.ts +++ b/src/lib/utils/global.ts @@ -14,6 +14,7 @@ import { createClassListPropertySource } from '../props/property-sources/createC import { createDataAttributePropertySource } from '../props/property-sources/createDataAttributePropertySource'; import { createJsonScriptPropertySource } from '../props/property-sources/createJsonScriptPropertySource'; import { createReactivePropertySource } from '../props/property-sources/createReactivePropertySource'; +import { createFormPropertySource } from '../props/property-sources/createFormPropertySource'; // TODO: Move to "App"? class MubanGlobal { @@ -28,6 +29,7 @@ class MubanGlobal { createAttributePropertySource(), createTextPropertySource(), createHtmlPropertySource(), + createFormPropertySource(), ]; } From edd65fb2046ad6d4ace8541db7a551f7e9279231 Mon Sep 17 00:00:00 2001 From: Juan Polanco Date: Wed, 9 Mar 2022 11:38:50 -0500 Subject: [PATCH 02/50] Create test cases for the createFormPropertySource function --- .../createFormPropertySource.test.ts | 116 ++++++++++++++++++ 1 file changed, 116 insertions(+) create mode 100644 src/lib/props/property-sources/createFormPropertySource.test.ts diff --git a/src/lib/props/property-sources/createFormPropertySource.test.ts b/src/lib/props/property-sources/createFormPropertySource.test.ts new file mode 100644 index 00000000..958274f8 --- /dev/null +++ b/src/lib/props/property-sources/createFormPropertySource.test.ts @@ -0,0 +1,116 @@ +import type { PropTypeInfo } from '../propDefinitions.types'; +import { createFormPropertySource } from './createFormPropertySource'; + +const form = document.createElement('form'); +form.innerHTML = ` + + + + + + + + + + +`; + +describe('createFormPropertySource', () => { + describe('function itself', () => { + it('should create without errors', () => { + expect(createFormPropertySource).not.toThrow(); + }); + it('should allow calling the created source without errors', () => { + expect(createFormPropertySource()).not.toThrow(); + }); + }); + + describe('hasProp', () => { + it('should return false if the type is "Function"', () => { + const functionPropInfo: PropTypeInfo = { + name: 'email', + type: Function, + source: { + name: 'email', + target: form, + type: 'form', + }, + }; + expect(createFormPropertySource()(form).hasProp(functionPropInfo)).toBe(false); + }); + it('should return true if the type is different than "Function"', () => { + const validPropInfoTypes = [Number, String, Boolean, Date, Array, Object]; + const propInfos: Array = validPropInfoTypes.map((type) => ({ + name: 'foo', + type, + source: { + name: 'foo', + target: form, + type: 'form', + }, + })); + propInfos.forEach((propInfo) => { + expect(createFormPropertySource()(form).hasProp(propInfo)).toBe(true); + }); + }); + }); + + describe('getProp', () => { + it('Should return the input value if the target is an input and the type is not "checkbox"', () => {}); + it('Should return the checked value if the target is a checkbox and the propType is boolean', () => {}); + it('Should return the input value if the target is a checked checkbox and the propType is not boolean', () => {}); + it('Should return the input value if the target is a checked checkbox and the propType is not boolean', () => {}); + it('Should return the select value if the target is a select that is not multiple', () => {}); + it('Should return an array of strings if the target is a multiselect', () => {}); + it('Should return an array of strings if the target is a multiselect', () => {}); + it('Should return an array of strings if propType is array and the target is a checkbox', () => {}); + it('Should return a File if the target is an input with type "file"', () => {}); + it('Should return a FileList if the target is an input with type "file" and is multiple', () => {}); + it('Should return a FileList if the target is an input with type "file" and is multiple', () => {}); + it('Should return undefined when trying to get FormData not using type "Object"', () => { + const wrongTypePropInfo: PropTypeInfo = { + name: 'myForm', + type: String, // This should be Object + source: { + target: form, + type: 'form', + }, + }; + expect(createFormPropertySource()(form).getProp(wrongTypePropInfo)).toBe(undefined); + }); + it('Should return FormData when using type "Object" and an unnamed source', () => { + const wrongTypePropInfo: PropTypeInfo = { + name: 'myForm', + type: Object, + source: { + target: form, + type: 'form', + }, + }; + expect(createFormPropertySource()(form).getProp(wrongTypePropInfo)).toBeInstanceOf(FormData); + expect( + (createFormPropertySource()(form).getProp(wrongTypePropInfo) as FormData).get('email'), + ).toBe('juan.polanco@mediamonks.com'); + }); + it('Should return the input value when passing a form element and a named source', () => { + const formDataValuePropInfo: PropTypeInfo = { + name: 'myForm', + type: String, + source: { + target: form, + type: 'form', + name: 'email', + }, + }; + expect(createFormPropertySource()(form).getProp(formDataValuePropInfo)).toBe( + 'juan.polanco@mediamonks.com', + ); + }); + }); +}); From c0c9adc33b7eb5d4fe63b750f8c267cb36ce4e3a Mon Sep 17 00:00:00 2001 From: Juan Polanco Date: Wed, 9 Mar 2022 16:42:38 -0500 Subject: [PATCH 03/50] Update createFormPropertySource() adding 'checkbox' and 'multiple' cases --- .../createFormPropertySource.ts | 104 +++++++++++------- 1 file changed, 67 insertions(+), 37 deletions(-) diff --git a/src/lib/props/property-sources/createFormPropertySource.ts b/src/lib/props/property-sources/createFormPropertySource.ts index 17e03a61..65518497 100644 --- a/src/lib/props/property-sources/createFormPropertySource.ts +++ b/src/lib/props/property-sources/createFormPropertySource.ts @@ -1,6 +1,7 @@ import dedent from 'ts-dedent'; import type { PropertySource } from '../getComponentProps'; import { convertSourceValue } from './convertSourceValue'; +import flow from 'lodash/flow'; export function createFormPropertySource(): PropertySource { return () => { @@ -8,56 +9,85 @@ export function createFormPropertySource(): PropertySource { sourceName: 'form', hasProp: (propInfo) => Boolean(propInfo.source.target && propInfo.type !== Function), getProp: (propInfo) => { - // let value; - const element = propInfo.source.target; - - const isForm = element && element.nodeName === 'FORM'; - const isInput = element && element?.nodeName === 'INPUT'; - - if (!isForm && !isInput) { + const element = propInfo.source.target!; + const isCheckbox = + element.nodeName === 'INPUT' && (element as HTMLInputElement).type === 'checkbox'; + const isForm = element.nodeName === 'FORM'; + const isInput = + ['INPUT', 'TEXTAREA', 'SELECT'].includes(element.nodeName) && + (element as HTMLInputElement).type !== 'checkbox'; + const isMultiSelect = + element.nodeName === 'SELECT' && (element as HTMLSelectElement).multiple; + const isFile = + element.nodeName === 'INPUT' && (element as HTMLInputElement).type === 'file'; + const isValidtag = ['INPUT', 'FORM', 'TEXTAREA', 'SELECT'].includes(element.nodeName); + if (!isValidtag) { console.warn( - dedent`The property "${propInfo.name}" of type "${propInfo.type.name}" requires a valid 'form' or 'input' element + dedent`The property "${propInfo.name}" of type "${propInfo.type.name}" requires an element of type input, form, textarea, or select. ${element.nodeName} was given Returning "undefined".`, ); return undefined; } - if (isForm) { - const formData = new FormData(element as HTMLFormElement); - const childInputValue = formData.get(propInfo.source.name || ''); + const formDataValue = (prevValue: unknown) => { + if (isForm) { + if (propInfo.type !== Object && !propInfo.source.name) { + console.warn( + dedent`The property "${propInfo.name}" is trying to get a FormData object but is type "${propInfo.type.name}", set it as type "Object" + Returning "undefined".`, + ); + return undefined; + } + const formData = new FormData(element as HTMLFormElement); + const childInputValue = formData.getAll(propInfo.source.name || ''); + const value = + propInfo.type === Array + ? childInputValue + : convertSourceValue(propInfo, (childInputValue[0] as string) || ''); - if (propInfo.type !== Object && !propInfo.source.name) { - console.warn( - dedent`The property "${propInfo.name}" is trying to get a FormData object but is type "${propInfo.type.name}", set it as type "Object" - Returning "undefined".`, - ); - return undefined; + return childInputValue.length ? value : formData; } - return childInputValue - ? convertSourceValue(propInfo, childInputValue as string) - : formData; - } + return prevValue; + }; - if (isInput) { - return (element as HTMLInputElement).value; - } + const textInput = (prevValue: unknown) => { + const input = element as HTMLInputElement; + if (isInput && !input.multiple) return convertSourceValue(propInfo, input.value); + return prevValue; + }; - return undefined; + const checkbox = (prevValue: unknown) => { + const input = element as HTMLInputElement; + if (isCheckbox) return input.checked; + return prevValue; + }; - /* if(isValidInput) { - const formData = new FormData(element as HTMLFormElement); - return formData.get(propInfo.source.name) || formData; - } + const nonBooleanCheckbox = (prevValue: unknown) => { + const input = element as HTMLInputElement; + if (isCheckbox && propInfo.type !== Boolean) + return convertSourceValue(propInfo, input.value); + return prevValue; + }; - if(value != undefined) { - return convertSourceValue(propInfo, value); - } */ + const multiSelect = (prevValue: unknown) => { + if (isMultiSelect) { + return Array.from((element as HTMLSelectElement).selectedOptions).map( + (option) => option.value, + ); + } + return prevValue; + }; + + const file = (prevValue: unknown) => { + const input = element as HTMLInputElement; + if (isFile) { + if (propInfo.type === Object) input.files?.length ? input.files[0] : undefined; + if (propInfo.type === Array) input.files; + } + return prevValue; + }; - // const getDirectValue = propInfo.source.target?.nodeType - /* propInfo.name - propInfo.source - propInfo.type - propInfo.isOptional */ + return flow([formDataValue, textInput, checkbox, nonBooleanCheckbox, multiSelect, file])(); }, }; }; From 70ef62f95bf7ce56d08ad0c0123aec1605fc2f0b Mon Sep 17 00:00:00 2001 From: Juan Polanco Date: Wed, 9 Mar 2022 16:43:12 -0500 Subject: [PATCH 04/50] Update createFormPropertySource() tests cases --- .../createFormPropertySource.test.ts | 205 +++++++++++++++--- 1 file changed, 176 insertions(+), 29 deletions(-) diff --git a/src/lib/props/property-sources/createFormPropertySource.test.ts b/src/lib/props/property-sources/createFormPropertySource.test.ts index 958274f8..23a3f7f2 100644 --- a/src/lib/props/property-sources/createFormPropertySource.test.ts +++ b/src/lib/props/property-sources/createFormPropertySource.test.ts @@ -3,22 +3,26 @@ import { createFormPropertySource } from './createFormPropertySource'; const form = document.createElement('form'); form.innerHTML = ` - - - - + + + - - - - - + + + + + + + + - + `; describe('createFormPropertySource', () => { @@ -62,17 +66,160 @@ describe('createFormPropertySource', () => { }); describe('getProp', () => { - it('Should return the input value if the target is an input and the type is not "checkbox"', () => {}); - it('Should return the checked value if the target is a checkbox and the propType is boolean', () => {}); - it('Should return the input value if the target is a checked checkbox and the propType is not boolean', () => {}); - it('Should return the input value if the target is a checked checkbox and the propType is not boolean', () => {}); - it('Should return the select value if the target is a select that is not multiple', () => {}); - it('Should return an array of strings if the target is a multiselect', () => {}); - it('Should return an array of strings if the target is a multiselect', () => {}); - it('Should return an array of strings if propType is array and the target is a checkbox', () => {}); - it('Should return a File if the target is an input with type "file"', () => {}); - it('Should return a FileList if the target is an input with type "file" and is multiple', () => {}); - it('Should return a FileList if the target is an input with type "file" and is multiple', () => {}); + it('Should return the input value if the target is an input and the type is not "checkbox"', () => { + const emailInput: PropTypeInfo = { + name: 'email', + type: String, + source: { + target: form.querySelector('#email')! as HTMLElement, + type: 'form', + }, + }; + const passwordInput: PropTypeInfo = { + name: 'password', + type: String, + source: { + target: form.querySelector('#password')! as HTMLElement, + type: 'form', + }, + }; + const descriptionInput: PropTypeInfo = { + name: 'description', + type: String, + source: { + target: form.querySelector('#description')! as HTMLElement, + type: 'form', + }, + }; + expect(createFormPropertySource()(form).getProp(emailInput)).toBe( + 'juan.polanco@mediamonks.com', + ); + expect(createFormPropertySource()(form).getProp(passwordInput)).toBe('123456'); + expect(createFormPropertySource()(form).getProp(descriptionInput)).toBe('lorem ipsum'); + }); + it('Should return the checked value if the target is a checkbox and the propType is boolean', () => { + const optionA: PropTypeInfo = { + name: 'checkbox', + type: Boolean, + source: { + target: form.querySelector('#optionA')! as HTMLElement, + type: 'form', + }, + }; + const optionB: PropTypeInfo = { + name: 'checkbox', + type: Boolean, + source: { + target: form.querySelector('#optionB')! as HTMLElement, + type: 'form', + }, + }; + expect(createFormPropertySource()(form).getProp(optionA)).toBe(false); + expect(createFormPropertySource()(form).getProp(optionB)).toBe(true); + }); + it('Should return the input value if the target is a checked checkbox and the propType is not boolean', () => { + const optionB: PropTypeInfo = { + name: 'checkbox', + type: String, + source: { + target: form.querySelector('#optionB')! as HTMLElement, + type: 'form', + }, + }; + expect(createFormPropertySource()(form).getProp(optionB)).toBe('foo'); + }); + it('Should return an array of strings if the target is checkbox and the propType is Array', () => { + const choices: PropTypeInfo = { + name: 'checkbox', + type: Array, + source: { + target: form, + type: 'form', + name: 'choices', + }, + }; + expect(JSON.stringify(createFormPropertySource()(form).getProp(choices))).toStrictEqual( + JSON.stringify(['apple', 'banana']), + ); + }); + it('Should return the select value if the target is a select that is not multiple', () => { + const directSelect: PropTypeInfo = { + name: 'select', + type: String, + source: { + target: form.querySelector('#preference')! as HTMLElement, + type: 'form', + }, + }; + const formDataSelect: PropTypeInfo = { + name: 'select', + type: String, + source: { + target: form, + type: 'form', + name: 'preference', + }, + }; + const selectBoolean: PropTypeInfo = { + name: 'select', + type: Boolean, + source: { + target: form, + type: 'form', + name: 'preferenceBoolean', + }, + }; + expect(createFormPropertySource()(form).getProp(selectBoolean)).toBe(true); + expect(createFormPropertySource()(form).getProp(directSelect)).toBe('foo'); + expect(createFormPropertySource()(form).getProp(formDataSelect)).toBe('foo'); + }); + it('Should return an array of strings if the target is a multiselect', () => { + const candidates: PropTypeInfo = { + name: 'multiselect', + type: Array, + source: { + target: form.querySelector('#candidates')! as HTMLElement, + type: 'form', + }, + }; + const candidatesFromFormData: PropTypeInfo = { + name: 'multiselect', + type: Array, + source: { + target: form, + type: 'form', + name: 'candidates', + }, + }; + expect(JSON.stringify(createFormPropertySource()(form).getProp(candidates))).toStrictEqual( + JSON.stringify(['foo', 'bar']), + ); + expect( + JSON.stringify(createFormPropertySource()(form).getProp(candidatesFromFormData)), + ).toStrictEqual(JSON.stringify(['foo', 'bar'])); + }); + it('Should return a File if the target is an input with type "file"', () => { + const photo: PropTypeInfo = { + name: 'file', + type: Object, + source: { + target: form.querySelector('#photo')! as HTMLElement, + type: 'form', + }, + }; + expect(createFormPropertySource()(form).getProp(photo)).toBe(undefined); + }); + it('Should return a FileList if the target is an input with type "file" and is type Array', () => { + const photoArray: PropTypeInfo = { + name: 'file', + type: Array, + source: { + target: form.querySelector('#photo')! as HTMLElement, + type: 'form', + }, + }; + expect(createFormPropertySource()(form).getProp(photoArray)).toBe(undefined); + }); it('Should return undefined when trying to get FormData not using type "Object"', () => { const wrongTypePropInfo: PropTypeInfo = { name: 'myForm', @@ -85,7 +232,7 @@ describe('createFormPropertySource', () => { expect(createFormPropertySource()(form).getProp(wrongTypePropInfo)).toBe(undefined); }); it('Should return FormData when using type "Object" and an unnamed source', () => { - const wrongTypePropInfo: PropTypeInfo = { + const validForm: PropTypeInfo = { name: 'myForm', type: Object, source: { @@ -93,13 +240,13 @@ describe('createFormPropertySource', () => { type: 'form', }, }; - expect(createFormPropertySource()(form).getProp(wrongTypePropInfo)).toBeInstanceOf(FormData); - expect( - (createFormPropertySource()(form).getProp(wrongTypePropInfo) as FormData).get('email'), - ).toBe('juan.polanco@mediamonks.com'); + expect(createFormPropertySource()(form).getProp(validForm)).toBeInstanceOf(FormData); + expect((createFormPropertySource()(form).getProp(validForm) as FormData).get('email')).toBe( + 'juan.polanco@mediamonks.com', + ); }); it('Should return the input value when passing a form element and a named source', () => { - const formDataValuePropInfo: PropTypeInfo = { + const validForm: PropTypeInfo = { name: 'myForm', type: String, source: { @@ -108,7 +255,7 @@ describe('createFormPropertySource', () => { name: 'email', }, }; - expect(createFormPropertySource()(form).getProp(formDataValuePropInfo)).toBe( + expect(createFormPropertySource()(form).getProp(validForm)).toBe( 'juan.polanco@mediamonks.com', ); }); From ca697776fa89f16aff967b60934b53ddf8fe1df0 Mon Sep 17 00:00:00 2001 From: Juan Polanco Date: Thu, 10 Mar 2022 11:48:17 -0500 Subject: [PATCH 05/50] Updates usage of the propInfo sourceName in the property sources --- .../props/property-sources/createAttributePropertySource.ts | 4 ++-- .../props/property-sources/createClassListPropertySource.ts | 6 +++--- .../property-sources/createDataAttributePropertySource.ts | 4 ++-- .../property-sources/createJsonScriptPropertySource.ts | 4 ++-- .../props/property-sources/createReactivePropertySource.ts | 4 ++-- 5 files changed, 11 insertions(+), 11 deletions(-) diff --git a/src/lib/props/property-sources/createAttributePropertySource.ts b/src/lib/props/property-sources/createAttributePropertySource.ts index e8de7564..9af9dda9 100644 --- a/src/lib/props/property-sources/createAttributePropertySource.ts +++ b/src/lib/props/property-sources/createAttributePropertySource.ts @@ -8,13 +8,13 @@ export function createAttributePropertySource(): PropertySource { Boolean( propInfo.source.target && propInfo.type !== Function && - propInfo.source.target.hasAttribute(propInfo.source.name), + propInfo.source.target.hasAttribute(propInfo.source.name!), ), getProp: (propInfo) => { let value; const rawValue = propInfo.type !== Function - ? propInfo.source.target!.getAttribute(propInfo.source.name) ?? undefined + ? propInfo.source.target!.getAttribute(propInfo.source.name!) ?? undefined : undefined; if (rawValue !== undefined) { diff --git a/src/lib/props/property-sources/createClassListPropertySource.ts b/src/lib/props/property-sources/createClassListPropertySource.ts index e0e3cf9a..080de206 100644 --- a/src/lib/props/property-sources/createClassListPropertySource.ts +++ b/src/lib/props/property-sources/createClassListPropertySource.ts @@ -17,9 +17,9 @@ export function createClassListPropertySource(): PropertySource { // in case of boolean, check for existence if (propInfo.type === Boolean) { const hasValue = Boolean( - target.classList.contains(propInfo.source.name) || - target.classList.contains(camelCase(propInfo.source.name)) || - target.classList.contains(paramCase(propInfo.source.name)), + target.classList.contains(propInfo.source.name!) || + target.classList.contains(camelCase(propInfo.source.name!)) || + target.classList.contains(paramCase(propInfo.source.name!)), ); // only return false from missing value if this source is used explicitly // or if value is found diff --git a/src/lib/props/property-sources/createDataAttributePropertySource.ts b/src/lib/props/property-sources/createDataAttributePropertySource.ts index 8ddb5950..392547a3 100644 --- a/src/lib/props/property-sources/createDataAttributePropertySource.ts +++ b/src/lib/props/property-sources/createDataAttributePropertySource.ts @@ -8,13 +8,13 @@ export function createDataAttributePropertySource(): PropertySource { Boolean( propInfo.source.target && propInfo.type !== Function && - propInfo.source.name in propInfo.source.target.dataset, + propInfo.source.name! in propInfo.source.target.dataset, ), getProp: (propInfo) => { let value; const rawValue = propInfo.type !== Function - ? propInfo.source.target!.dataset[propInfo.source.name] ?? undefined + ? propInfo.source.target!.dataset[propInfo.source.name!] ?? undefined : undefined; if (rawValue !== undefined) { diff --git a/src/lib/props/property-sources/createJsonScriptPropertySource.ts b/src/lib/props/property-sources/createJsonScriptPropertySource.ts index 328c68e6..0ca7c982 100644 --- a/src/lib/props/property-sources/createJsonScriptPropertySource.ts +++ b/src/lib/props/property-sources/createJsonScriptPropertySource.ts @@ -32,11 +32,11 @@ export function createJsonScriptPropertySource(): PropertySource { hasProp: (propInfo) => Boolean( propInfo.source.target && - propInfo.source.name in getJsonContent(propInfo.source.target as HTMLElement), + propInfo.source.name! in getJsonContent(propInfo.source.target as HTMLElement), ), getProp: (propInfo) => { // TODO: convert to Date - all other data types should be fine in JSON already - return getJsonContent(propInfo.source.target! as HTMLElement)[propInfo.source.name]; + return getJsonContent(propInfo.source.target! as HTMLElement)[propInfo.source.name!]; }, }; }; diff --git a/src/lib/props/property-sources/createReactivePropertySource.ts b/src/lib/props/property-sources/createReactivePropertySource.ts index 61405c75..cb761d4b 100644 --- a/src/lib/props/property-sources/createReactivePropertySource.ts +++ b/src/lib/props/property-sources/createReactivePropertySource.ts @@ -6,8 +6,8 @@ export function createReactivePropertySource(): PropertySource { const props = reactive>({}); return { sourceName: 'reactive', - hasProp: (propInfo) => propInfo.source.name in props, - getProp: (propInfo) => props[propInfo.source.name], + hasProp: (propInfo) => propInfo.source.name! in props, + getProp: (propInfo) => props[propInfo.source.name!], }; }; } From 39d16e30670c1124402c3b2f73eb1f6af7b90249 Mon Sep 17 00:00:00 2001 From: Juan Polanco Date: Thu, 10 Mar 2022 12:16:59 -0500 Subject: [PATCH 06/50] Add stories for the 'form' propType.source --- .../core/props/FormProps.stories.ts | 72 +++++++++++++++++++ 1 file changed, 72 insertions(+) create mode 100644 test-storybook/src/components/core/props/FormProps.stories.ts diff --git a/test-storybook/src/components/core/props/FormProps.stories.ts b/test-storybook/src/components/core/props/FormProps.stories.ts new file mode 100644 index 00000000..2acfa5ae --- /dev/null +++ b/test-storybook/src/components/core/props/FormProps.stories.ts @@ -0,0 +1,72 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +import type { Story } from '@muban/storybook/types-6-0'; +import { html } from '@muban/template'; +import { either, test } from 'isntnt'; +import { bind, defineComponent, propType, computed, ref } from '@muban/muban'; +import type { PropTypeDefinition, ComponentRefItem } from '@muban/muban'; + +export default { + title: 'core/props/form', +}; + +const getInfoBinding = (refs: any, props: any) => { + console.log(props); + return bind(refs.info, { + text: computed(() => JSON.stringify(props, null, 2)), + }); +}; + +const createPropsComponent = ( + props: Record, + refs: Record = {}, +) => { + return defineComponent({ + name: 'props', + refs: { + info: 'info', + ...refs, + }, + props, + setup({ props, refs }) { + return [getInfoBinding(refs, props)]; + }, + }); +}; + +/* string +number +boolean +date +object +array */ + +export const Form: Story = () => ({ + component: createPropsComponent( + { + inputText: propType.string.source({ type: 'form', target: 'inputTextRef' }), + inputNumber: propType.number.source({ type: 'form', target: 'inputNumberRef' }), + inputBoolean: propType.boolean.source({ type: 'form', target: 'inputBooleanRef' }), + inputDate: propType.date.source({ type: 'form', target: 'inputDateRef' }), + inputObject: propType.object.source({ type: 'form', target: 'inputObjectRef' }), + inputArray: propType.array.source({ type: 'form', target: 'inputArrayRef' }), + }, + { + inputTextRef: 'inputTextRef', + inputNumberRef: 'inputNumberRef', + inputBooleanRef: 'inputBooleanRef', + inputDateRef: 'inputDateRef', + inputObjectRef: 'inputObjectRef', + inputArrayRef: 'inputArrayRef', + }, + ), + template: () => html`
+

Input

+
+
+
+
+
+
+

+  
`, +}); From acf4d8e77daa3e0fd34781b9574c7abc8a8ef809 Mon Sep 17 00:00:00 2001 From: Juan Polanco Date: Thu, 10 Mar 2022 12:36:59 -0500 Subject: [PATCH 07/50] Update checkbox behavior in createFormPropertySource() --- src/lib/props/property-sources/createFormPropertySource.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/lib/props/property-sources/createFormPropertySource.ts b/src/lib/props/property-sources/createFormPropertySource.ts index 65518497..c6fa8e63 100644 --- a/src/lib/props/property-sources/createFormPropertySource.ts +++ b/src/lib/props/property-sources/createFormPropertySource.ts @@ -58,13 +58,13 @@ export function createFormPropertySource(): PropertySource { const checkbox = (prevValue: unknown) => { const input = element as HTMLInputElement; - if (isCheckbox) return input.checked; + if (isCheckbox && propInfo.type === Boolean) return input.checked; return prevValue; }; const nonBooleanCheckbox = (prevValue: unknown) => { const input = element as HTMLInputElement; - if (isCheckbox && propInfo.type !== Boolean) + if (isCheckbox && propInfo.type !== Boolean && input.checked) return convertSourceValue(propInfo, input.value); return prevValue; }; From 76c976c352b73a43ba79d80b848cf44cf893dc94 Mon Sep 17 00:00:00 2001 From: Juan Polanco Date: Thu, 10 Mar 2022 13:22:57 -0500 Subject: [PATCH 08/50] Update stories for createFormPropertySource() --- .../core/props/FormProps.stories.ts | 80 ++++++++++++++----- 1 file changed, 59 insertions(+), 21 deletions(-) diff --git a/test-storybook/src/components/core/props/FormProps.stories.ts b/test-storybook/src/components/core/props/FormProps.stories.ts index 2acfa5ae..b6c30572 100644 --- a/test-storybook/src/components/core/props/FormProps.stories.ts +++ b/test-storybook/src/components/core/props/FormProps.stories.ts @@ -9,12 +9,10 @@ export default { title: 'core/props/form', }; -const getInfoBinding = (refs: any, props: any) => { - console.log(props); - return bind(refs.info, { +const getInfoBinding = (refs: any, props: any) => + bind(refs.info, { text: computed(() => JSON.stringify(props, null, 2)), }); -}; const createPropsComponent = ( props: Record, @@ -33,13 +31,6 @@ const createPropsComponent = ( }); }; -/* string -number -boolean -date -object -array */ - export const Form: Story = () => ({ component: createPropsComponent( { @@ -49,6 +40,24 @@ export const Form: Story = () => ({ inputDate: propType.date.source({ type: 'form', target: 'inputDateRef' }), inputObject: propType.object.source({ type: 'form', target: 'inputObjectRef' }), inputArray: propType.array.source({ type: 'form', target: 'inputArrayRef' }), + checkboxOnBoolean: propType.boolean.source({ type: 'form', target: 'checkboxOnBooleanRef' }), + checkboxOnString: propType.string.source({ type: 'form', target: 'checkboxOnStringRef' }), + checkboxOnValueString: propType.string.source({ + type: 'form', + target: 'checkboxOnValueStringRef', + }), + checkboxOffBoolean: propType.boolean.source({ + type: 'form', + target: 'checkboxOffBooleanRef', + }), + checkboxOffString: propType.string.source({ type: 'form', target: 'checkboxOffStringRef' }), + checkboxOffValueString: propType.string.source({ + type: 'form', + target: 'checkboxOffValueStringRef', + }), + selectText: propType.string.source({ type: 'form', target: 'selectRef' }), + multiSelectText: propType.number.source({ type: 'form', target: 'multiSelectRef' }), + formData: propType.object.source({ type: 'form', target: 'formRef' }), }, { inputTextRef: 'inputTextRef', @@ -57,16 +66,45 @@ export const Form: Story = () => ({ inputDateRef: 'inputDateRef', inputObjectRef: 'inputObjectRef', inputArrayRef: 'inputArrayRef', + checkboxOnBooleanRef: 'checkboxOnBooleanRef', + checkboxOnStringRef: 'checkboxOnStringRef', + checkboxOnValueStringRef: 'checkboxOnValueStringRef', + checkboxOffBooleanRef: 'checkboxOffBooleanRef', + checkboxOffStringRef: 'checkboxOffStringRef', // This wont show up in the info as it's undefined + checkboxOffValueStringRef: 'checkboxOffValueStringRef', // This wont show up in the info as it's undefined + selectRef: 'selectRef', + multiSelectRef: 'multiSelectRef', + formRef: 'formRef', }, ), - template: () => html`
-

Input

-
-
-
-
-
-
-

-  
`, + template: () => html`
+
+
+
+
+
+
+
+
+
+
+
+
+
+ + +

+    
+
`, }); From 63f797e5694f881f8c0f647b179a6a0bb8d13a40 Mon Sep 17 00:00:00 2001 From: Juan Polanco Date: Wed, 23 Mar 2022 08:55:38 -0500 Subject: [PATCH 09/50] Remove the file function from createFormPropertySource --- .../property-sources/createFormPropertySource.ts | 11 +---------- 1 file changed, 1 insertion(+), 10 deletions(-) diff --git a/src/lib/props/property-sources/createFormPropertySource.ts b/src/lib/props/property-sources/createFormPropertySource.ts index c6fa8e63..b6d9a8f2 100644 --- a/src/lib/props/property-sources/createFormPropertySource.ts +++ b/src/lib/props/property-sources/createFormPropertySource.ts @@ -78,16 +78,7 @@ export function createFormPropertySource(): PropertySource { return prevValue; }; - const file = (prevValue: unknown) => { - const input = element as HTMLInputElement; - if (isFile) { - if (propInfo.type === Object) input.files?.length ? input.files[0] : undefined; - if (propInfo.type === Array) input.files; - } - return prevValue; - }; - - return flow([formDataValue, textInput, checkbox, nonBooleanCheckbox, multiSelect, file])(); + return flow([formDataValue, textInput, checkbox, nonBooleanCheckbox, multiSelect])(); }, }; }; From 1f8d41941d51c3a7feb38cba233312cdf9eb7520 Mon Sep 17 00:00:00 2001 From: Juan Polanco Date: Wed, 23 Mar 2022 09:00:04 -0500 Subject: [PATCH 10/50] Remove lodash dependency from createFormPropertySource --- .../props/property-sources/createFormPropertySource.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/lib/props/property-sources/createFormPropertySource.ts b/src/lib/props/property-sources/createFormPropertySource.ts index b6d9a8f2..e3f22df0 100644 --- a/src/lib/props/property-sources/createFormPropertySource.ts +++ b/src/lib/props/property-sources/createFormPropertySource.ts @@ -1,7 +1,6 @@ import dedent from 'ts-dedent'; import type { PropertySource } from '../getComponentProps'; import { convertSourceValue } from './convertSourceValue'; -import flow from 'lodash/flow'; export function createFormPropertySource(): PropertySource { return () => { @@ -18,8 +17,6 @@ export function createFormPropertySource(): PropertySource { (element as HTMLInputElement).type !== 'checkbox'; const isMultiSelect = element.nodeName === 'SELECT' && (element as HTMLSelectElement).multiple; - const isFile = - element.nodeName === 'INPUT' && (element as HTMLInputElement).type === 'file'; const isValidtag = ['INPUT', 'FORM', 'TEXTAREA', 'SELECT'].includes(element.nodeName); if (!isValidtag) { console.warn( @@ -78,7 +75,10 @@ export function createFormPropertySource(): PropertySource { return prevValue; }; - return flow([formDataValue, textInput, checkbox, nonBooleanCheckbox, multiSelect])(); + return [formDataValue, textInput, checkbox, nonBooleanCheckbox, multiSelect].reduce( + (prev, current) => current(prev), + undefined, + ); }, }; }; From b8e1c5064c23ed3c0293fe5da54a99e6fe074f0d Mon Sep 17 00:00:00 2001 From: Juan Polanco Date: Wed, 23 Mar 2022 09:02:34 -0500 Subject: [PATCH 11/50] Fix HtmlElement casting in the tests for createFormPropertySource --- .../createFormPropertySource.test.ts | 20 +++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/src/lib/props/property-sources/createFormPropertySource.test.ts b/src/lib/props/property-sources/createFormPropertySource.test.ts index 23a3f7f2..43377ef5 100644 --- a/src/lib/props/property-sources/createFormPropertySource.test.ts +++ b/src/lib/props/property-sources/createFormPropertySource.test.ts @@ -71,7 +71,7 @@ describe('createFormPropertySource', () => { name: 'email', type: String, source: { - target: form.querySelector('#email')! as HTMLElement, + target: form.querySelector('#email')!, type: 'form', }, }; @@ -79,7 +79,7 @@ describe('createFormPropertySource', () => { name: 'password', type: String, source: { - target: form.querySelector('#password')! as HTMLElement, + target: form.querySelector('#password')!, type: 'form', }, }; @@ -87,7 +87,7 @@ describe('createFormPropertySource', () => { name: 'description', type: String, source: { - target: form.querySelector('#description')! as HTMLElement, + target: form.querySelector('#description')!, type: 'form', }, }; @@ -102,7 +102,7 @@ describe('createFormPropertySource', () => { name: 'checkbox', type: Boolean, source: { - target: form.querySelector('#optionA')! as HTMLElement, + target: form.querySelector('#optionA')!, type: 'form', }, }; @@ -110,7 +110,7 @@ describe('createFormPropertySource', () => { name: 'checkbox', type: Boolean, source: { - target: form.querySelector('#optionB')! as HTMLElement, + target: form.querySelector('#optionB')!, type: 'form', }, }; @@ -122,7 +122,7 @@ describe('createFormPropertySource', () => { name: 'checkbox', type: String, source: { - target: form.querySelector('#optionB')! as HTMLElement, + target: form.querySelector('#optionB')!, type: 'form', }, }; @@ -147,7 +147,7 @@ describe('createFormPropertySource', () => { name: 'select', type: String, source: { - target: form.querySelector('#preference')! as HTMLElement, + target: form.querySelector('#preference')!, type: 'form', }, }; @@ -178,7 +178,7 @@ describe('createFormPropertySource', () => { name: 'multiselect', type: Array, source: { - target: form.querySelector('#candidates')! as HTMLElement, + target: form.querySelector('#candidates')!, type: 'form', }, }; @@ -203,7 +203,7 @@ describe('createFormPropertySource', () => { name: 'file', type: Object, source: { - target: form.querySelector('#photo')! as HTMLElement, + target: form.querySelector('#photo')!, type: 'form', }, }; @@ -214,7 +214,7 @@ describe('createFormPropertySource', () => { name: 'file', type: Array, source: { - target: form.querySelector('#photo')! as HTMLElement, + target: form.querySelector('#photo')!, type: 'form', }, }; From 1d2179f13c7ed2f48c15cfad2fce77ef3d4e47d0 Mon Sep 17 00:00:00 2001 From: Juan Polanco Date: Wed, 23 Mar 2022 09:20:02 -0500 Subject: [PATCH 12/50] Update createFormPropertySource tests to have an isolated testing form per unit test --- .../createFormPropertySource.test.ts | 105 ++++++++++-------- 1 file changed, 58 insertions(+), 47 deletions(-) diff --git a/src/lib/props/property-sources/createFormPropertySource.test.ts b/src/lib/props/property-sources/createFormPropertySource.test.ts index 43377ef5..a73285c1 100644 --- a/src/lib/props/property-sources/createFormPropertySource.test.ts +++ b/src/lib/props/property-sources/createFormPropertySource.test.ts @@ -1,30 +1,6 @@ import type { PropTypeInfo } from '../propDefinitions.types'; import { createFormPropertySource } from './createFormPropertySource'; -const form = document.createElement('form'); -form.innerHTML = ` - - - - - - - - - - - -`; - describe('createFormPropertySource', () => { describe('function itself', () => { it('should create without errors', () => { @@ -36,6 +12,7 @@ describe('createFormPropertySource', () => { }); describe('hasProp', () => { + const form = document.createElement('form'); it('should return false if the type is "Function"', () => { const functionPropInfo: PropTypeInfo = { name: 'email', @@ -49,6 +26,7 @@ describe('createFormPropertySource', () => { expect(createFormPropertySource()(form).hasProp(functionPropInfo)).toBe(false); }); it('should return true if the type is different than "Function"', () => { + const form = document.createElement('form'); const validPropInfoTypes = [Number, String, Boolean, Date, Array, Object]; const propInfos: Array = validPropInfoTypes.map((type) => ({ name: 'foo', @@ -66,7 +44,13 @@ describe('createFormPropertySource', () => { }); describe('getProp', () => { - it('Should return the input value if the target is an input and the type is not "checkbox"', () => { + it('Should return the input value if the target is a text input', () => { + const form = document.createElement('form'); + form.innerHTML = ` + + + + `; const emailInput: PropTypeInfo = { name: 'email', type: String, @@ -97,7 +81,13 @@ describe('createFormPropertySource', () => { expect(createFormPropertySource()(form).getProp(passwordInput)).toBe('123456'); expect(createFormPropertySource()(form).getProp(descriptionInput)).toBe('lorem ipsum'); }); + it('Should return the checked value if the target is a checkbox and the propType is boolean', () => { + const form = document.createElement('form'); + form.innerHTML = ` + + + `; const optionA: PropTypeInfo = { name: 'checkbox', type: Boolean, @@ -117,7 +107,12 @@ describe('createFormPropertySource', () => { expect(createFormPropertySource()(form).getProp(optionA)).toBe(false); expect(createFormPropertySource()(form).getProp(optionB)).toBe(true); }); + it('Should return the input value if the target is a checked checkbox and the propType is not boolean', () => { + const form = document.createElement('form'); + form.innerHTML = ` + + `; const optionB: PropTypeInfo = { name: 'checkbox', type: String, @@ -128,7 +123,13 @@ describe('createFormPropertySource', () => { }; expect(createFormPropertySource()(form).getProp(optionB)).toBe('foo'); }); + it('Should return an array of strings if the target is checkbox and the propType is Array', () => { + const form = document.createElement('form'); + form.innerHTML = ` + + + `; const choices: PropTypeInfo = { name: 'checkbox', type: Array, @@ -142,7 +143,19 @@ describe('createFormPropertySource', () => { JSON.stringify(['apple', 'banana']), ); }); + it('Should return the select value if the target is a select that is not multiple', () => { + const form = document.createElement('form'); + form.innerHTML = ` + + + `; const directSelect: PropTypeInfo = { name: 'select', type: String, @@ -173,7 +186,15 @@ describe('createFormPropertySource', () => { expect(createFormPropertySource()(form).getProp(directSelect)).toBe('foo'); expect(createFormPropertySource()(form).getProp(formDataSelect)).toBe('foo'); }); + it('Should return an array of strings if the target is a multiselect', () => { + const form = document.createElement('form'); + form.innerHTML = ` + + `; const candidates: PropTypeInfo = { name: 'multiselect', type: Array, @@ -198,29 +219,9 @@ describe('createFormPropertySource', () => { JSON.stringify(createFormPropertySource()(form).getProp(candidatesFromFormData)), ).toStrictEqual(JSON.stringify(['foo', 'bar'])); }); - it('Should return a File if the target is an input with type "file"', () => { - const photo: PropTypeInfo = { - name: 'file', - type: Object, - source: { - target: form.querySelector('#photo')!, - type: 'form', - }, - }; - expect(createFormPropertySource()(form).getProp(photo)).toBe(undefined); - }); - it('Should return a FileList if the target is an input with type "file" and is type Array', () => { - const photoArray: PropTypeInfo = { - name: 'file', - type: Array, - source: { - target: form.querySelector('#photo')!, - type: 'form', - }, - }; - expect(createFormPropertySource()(form).getProp(photoArray)).toBe(undefined); - }); + it('Should return undefined when trying to get FormData not using type "Object"', () => { + const form = document.createElement('form'); const wrongTypePropInfo: PropTypeInfo = { name: 'myForm', type: String, // This should be Object @@ -231,7 +232,12 @@ describe('createFormPropertySource', () => { }; expect(createFormPropertySource()(form).getProp(wrongTypePropInfo)).toBe(undefined); }); + it('Should return FormData when using type "Object" and an unnamed source', () => { + const form = document.createElement('form'); + form.innerHTML = ` + + `; const validForm: PropTypeInfo = { name: 'myForm', type: Object, @@ -245,7 +251,12 @@ describe('createFormPropertySource', () => { 'juan.polanco@mediamonks.com', ); }); + it('Should return the input value when passing a form element and a named source', () => { + const form = document.createElement('form'); + form.innerHTML = ` + + `; const validForm: PropTypeInfo = { name: 'myForm', type: String, From cca2c88c4b68d9ab33ff2a15a3b9356573cc35ba Mon Sep 17 00:00:00 2001 From: Juan Polanco Date: Wed, 23 Mar 2022 09:55:54 -0500 Subject: [PATCH 13/50] Add docs for the 'form' propType source --- docs/api/props.md | 44 +++++++++++++++++++++++++++++++++++++++++--- docs/guide/props.md | 4 +++- 2 files changed, 44 insertions(+), 4 deletions(-) diff --git a/docs/api/props.md b/docs/api/props.md index 830eb1fd..6e963f67 100644 --- a/docs/api/props.md +++ b/docs/api/props.md @@ -33,7 +33,7 @@ export type PropTypeDefinition = { shapeType?: Function; sourceOptions?: { target?: string; - type?: 'data' | 'json' | 'attr' | 'css' | 'text' | 'html'; + type?: 'data' | 'json' | 'attr' | 'css' | 'text' | 'html' | 'form'; name?: string; options?: { cssPredicate?: Array; @@ -265,7 +265,7 @@ HTML besides the default behaviour. ```ts declare function source(options: { target?: string; - type?: 'data' | 'json' | 'attr' | 'css' | 'text' | 'html'; + type?: 'data' | 'json' | 'attr' | 'css' | 'text' | 'html' | 'form'; name?: string; options?: { cssPredicate?: Array; @@ -276,7 +276,7 @@ declare function source(options: { * `target?: string` – The refName (those you configure as part of the component options) from which you want to extract this property. Defaults to the data-component element. -* `type?: 'data' | 'json' | 'attr' | 'css' | 'text' | 'html'` - The type source you want to extract. +* `type?: 'data' | 'json' | 'attr' | 'css' | 'text' | 'html' | 'form'` - The type source you want to extract. Defaults to the `data + json + css` source (`css` only for boolean props). * `data` – Reads the `data-attribute` from your target element. * `json` – Reads the object key from a `