diff --git a/javascripts/src/components/common/fields.tsx b/javascripts/src/components/common/fields.tsx index d5cf690b..998fe103 100644 --- a/javascripts/src/components/common/fields.tsx +++ b/javascripts/src/components/common/fields.tsx @@ -5,15 +5,16 @@ import {Field} from "../../../../types/mobx-react-form"; import LabeledFieldComponent from "./LabeledFieldComponent"; import {injectIntl, InjectedIntl} from 'react-intl'; import {HoudiniField} from "../../lib/houdini_form"; +import ReactInput from "./form/ReactInput"; -export const BasicField = injectIntl(observer((props:{field:Field, intl?:InjectedIntl, wrapperClassName?:string}) =>{ +export const BasicField = observer((props:{field:Field, placeholder?:string, label?:string, wrapperClassName?:string}) =>{ let field = props.field as HoudiniField return - + -})) \ No newline at end of file +}) \ No newline at end of file diff --git a/javascripts/src/components/common/form/ReactForm.tsx b/javascripts/src/components/common/form/ReactForm.tsx new file mode 100644 index 00000000..c61a828b --- /dev/null +++ b/javascripts/src/components/common/form/ReactForm.tsx @@ -0,0 +1,49 @@ +// License: LGPL-3.0-or-later +import * as React from 'react'; +import { observer, Provider } from 'mobx-react'; +import {Field, Form} from "mobx-react-form"; +import {observable, action, toJS} from 'mobx'; + +export interface ReactFormProps +{ + form:Form +} + +///Mostly useless class but, at some point, will replace all our form elements +@observer +export class ReactForm extends React.Component { + + + + @observable + form:Form + + @action.bound + componentWillMount() + { + this.form = this.props.form + } + + + + + componentDidUpdate(){ + + + } + + componentWillUnmount(){ + } + + + render() { + + return + {this.props.children} + + } +} + + + + diff --git a/javascripts/src/components/common/form/ReactInput.spec.tsx b/javascripts/src/components/common/form/ReactInput.spec.tsx new file mode 100644 index 00000000..b2a030fe --- /dev/null +++ b/javascripts/src/components/common/form/ReactInput.spec.tsx @@ -0,0 +1,164 @@ +// License: LGPL-3.0-or-later +import * as React from 'react'; +import 'jest'; +import ReactInput from './ReactInput' +import {Form} from "mobx-react-form"; +import {mount} from 'enzyme'; +import {toJS, observable, action, runInAction} from 'mobx'; +import {observer} from 'mobx-react'; +import {InputHTMLAttributes} from 'react'; +import {ReactForm} from "./ReactForm"; + + + +@observer +class TestChange extends React.Component{ + @observable + remove:boolean + @observable + form: Form + + @action.bound + componentWillMount(){ + this.form = new Form({fields:[{ + name: 'name', + extra: null} + ]}) + } + + + + @action.bound + onClick(){ + this.remove = true + } + render() { + let reactInput = !this.remove ? + {this.props.children} + : undefined + + return + + {reactInput} + this.onClick()}/> + + } +} + + +class WrappedInput extends React.Component>{ + + render(){ + let notChildren = {...this.props} + delete notChildren.children + return + + + } +} + +describe('ReactInput', () => { + + let form: Form + beforeEach(() => { + form = new Form({ + fields: [ + { + name: 'name', + extra: null + } + ] + }) + }) + + describe('no children passed in', () => { + test('gets added properly', () => { + let res = mount( + + + ) + + + //Did the attributes settings work as expected back to the objects + expect(form.$('name').label).toEqual('label') + expect(form.$('name').placeholder).toEqual('holder') + expect(form.$('name').value).toEqual('') + + //is the aria attribute passted through to the input + let input = res.find('input') + expect(input.prop('aria-required')).toEqual(true) + + + // is the input properly bound? + input.simulate('change', {target: { value: 'something' } }) + expect(form.$('name').value).toEqual('something') + }) + + test('gets removed properly', () => { + + let res = mount() + + // The two casts are needed because Typescript was going blowing up without the 'any' first. + // Why was it? *shrugs* + let f = res.find('ReactForm').instance() as any as ReactForm + expect(f.form.size).toEqual(1) + + res.find('input').simulate('change', {target: { value: 'something' } }) + + expect(f.form.$('name').value).toEqual('something') + + res.find('button').simulate('click') + expect(f.form.size).toEqual(1) + + expect(toJS(res.find('form'))).toMatchSnapshot() + + expect(f.form.$('name').label).toEqual('label1') + expect(f.form.$('name').placeholder).toEqual('holder') + }) + }) + + describe('children passed in', () => { + test('gets added properly', () => { + let res = mount( + + + + + ) + + //Did the attributes settings work as expected back to the objects + expect(form.$('name').label).toEqual('label') + expect(form.$('name').placeholder).toEqual('holder') + expect(form.$('name').value).toEqual('') + + //is the aria attribute passted through to the input + let input = res.find('input') + expect(input.prop('aria-required')).toEqual(true) + + + // is the input properly bound? + input.simulate('change', {target: { value: 'something' } }) + expect(form.$('name').value).toEqual('something') + }) + + test('gets removed properly', () => { + + let res = mount( + + ) + let f = res.find('ReactForm').instance() as any as ReactForm + res.find('input').simulate('change', {target: { value: 'something' } }) + + expect(f.form.$('name').value).toEqual('something') + expect(f.form.size).toEqual(1) + res.find('button').simulate('click') + expect(f.form.size).toEqual(1) + + expect(f.form.$('name').label).toEqual('label1') + expect(f.form.$('name').placeholder).toEqual('holder') + }) + + }) +}) \ No newline at end of file diff --git a/javascripts/src/components/common/form/ReactInput.tsx b/javascripts/src/components/common/form/ReactInput.tsx new file mode 100644 index 00000000..d63d756c --- /dev/null +++ b/javascripts/src/components/common/form/ReactInput.tsx @@ -0,0 +1,90 @@ +// License: LGPL-3.0-or-later +import * as React from 'react'; +import { observer, inject, Provider } from 'mobx-react'; +import {InjectedIntlProps, injectIntl} from 'react-intl'; +import {Field} from "mobx-react-form"; +import {observable, action, toJS, runInAction} from 'mobx'; +import {InputHTMLAttributes} from 'react'; + + + +export interface ReactInputProps +{ + field:Field + label?:string + placeholder?:string +} + +function castToNullIfUndef(i:any){ + return i === undefined ? null : i +} + + +class ReactInput extends React.Component, {}> { + + constructor(props:ReactInputProps){ + super(props) + } + + @observable + field:Field + + + @action.bound + componentWillMount(){ + + this.field = this.props.field + + + this.updateProps() + } + + componentWillUnmount(){ + } + + + componentDidUpdate(prevProps: Readonly, prevState: Readonly<{}>): void { + this.updateProps() + } + + @action.bound + updateProps() { + this.field.set('label', castToNullIfUndef(this.props.label)) + this.field.set('placeholder', castToNullIfUndef(this.props.placeholder)) + } + + @action.bound + renderChildren(){ + let ourProps = this.winnowProps() + let elem = React.cloneElement(this.props.children as React.ReactElement, + {...ourProps, ...this.field.bind() }) + return elem + + } + + ///Removes the properties we don't want to put into the input element + @action.bound + winnowProps(): ReactInputProps & InputHTMLAttributes { + let ourProps = {...this.props} + delete ourProps.field + delete ourProps.value + return ourProps + + } + + render() { + + if (this.props.children) + { + return this.renderChildren() + } + else { + return + } + } +} + +export default observer(ReactInput) + + + diff --git a/javascripts/src/components/common/form/__snapshots__/ReactInput.spec.tsx.snap b/javascripts/src/components/common/form/__snapshots__/ReactInput.spec.tsx.snap new file mode 100644 index 00000000..19600342 --- /dev/null +++ b/javascripts/src/components/common/form/__snapshots__/ReactInput.spec.tsx.snap @@ -0,0 +1,9 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`ReactInput no children passed in gets removed properly 1`] = ` + + + +`; diff --git a/javascripts/src/components/registration_page/NonprofitInfoForm.tsx b/javascripts/src/components/registration_page/NonprofitInfoForm.tsx index 03cb577c..1299920c 100644 --- a/javascripts/src/components/registration_page/NonprofitInfoForm.tsx +++ b/javascripts/src/components/registration_page/NonprofitInfoForm.tsx @@ -17,47 +17,31 @@ export interface NonprofitInfoFormProps export const FieldDefinitions : Array = [ { name: 'organization_name', - label: 'registration.wizard.nonprofit.name.label', - placeholder: 'registration.wizard.nonprofit.name.placeholder', - type: 'text', validators: [Validations.isFilled] }, { name: 'website', - label: 'registration.wizard.nonprofit.website.label', - placeholder: 'registration.wizard.nonprofit.website.placeholder', validators: [Validations.optional(Validations.isUrl)] }, { name: 'org_email', - label: 'registration.wizard.nonprofit.email.label', - placeholder: 'registration.wizard.nonprofit.email.placeholder', validators: [Validations.optional(Validations.isEmail)] }, { name: 'org_phone', - label: 'registration.wizard.nonprofit.phone.label', - placeholder: 'registration.wizard.nonprofit.phone.placeholder', type: "tel" }, { name: 'city', - label: 'registration.wizard.nonprofit.city.label', - placeholder: 'registration.wizard.nonprofit.city.placeholder', validators: [Validations.isFilled] }, { name: 'state', - label: 'registration.wizard.nonprofit.state.label', - placeholder: 'registration.wizard.nonprofit.state.placeholder', - type: 'text', validators: [Validations.isFilled] }, { name: 'zip', - label: 'registration.wizard.nonprofit.zip.label', - placeholder: 'registration.wizard.nonprofit.zip.placeholder', validators: [Validations.isFilled] } ] @@ -67,17 +51,33 @@ class NonprofitInfoForm extends React.Component - - + + + - - + + - - - + + + diff --git a/javascripts/src/components/registration_page/RegistrationWizard.tsx b/javascripts/src/components/registration_page/RegistrationWizard.tsx index e7a7b8c6..d3d58258 100644 --- a/javascripts/src/components/registration_page/RegistrationWizard.tsx +++ b/javascripts/src/components/registration_page/RegistrationWizard.tsx @@ -175,24 +175,6 @@ export class InnerRegistrationWizard extends React.Component diff --git a/javascripts/src/components/registration_page/UserInfoForm.tsx b/javascripts/src/components/registration_page/UserInfoForm.tsx index 89140939..553a1b23 100644 --- a/javascripts/src/components/registration_page/UserInfoForm.tsx +++ b/javascripts/src/components/registration_page/UserInfoForm.tsx @@ -12,8 +12,6 @@ import {areWeOrAnyParentSubmitting} from "../../lib/houdini_form"; export const FieldDefinitions : Array = [ { name: 'name', - label: 'registration.wizard.contact.name.label', - placeholder: 'registration.wizard.contact.name.placeholder', validators: [Validations.isFilled] }, { @@ -24,14 +22,12 @@ export const FieldDefinitions : Array = [ }, { name: 'password', - label: 'registration.wizard.contact.password.label', type: 'password', validators: [Validations.isFilled], related: ['userTab.password_confirmation'] }, { name: 'password_confirmation', - label: 'registration.wizard.contact.password_confirmation.label', type: 'password', validators: [Validations.shouldBeEqualTo("userTab.password")] } @@ -50,12 +46,22 @@ class UserInfoForm extends React.Component - - + + - - + + = [ { name: 'email', - label: 'email', type: 'text', validators: [Validations.isFilled] }, { name: 'password', - label: 'password', type: 'password', validators: [Validations.isFilled] } @@ -100,20 +98,14 @@ class InnerSessionLoginForm extends React.Component{(this.form as any).error} : '' return - - + + {errorDiv} void); @@ -173,6 +173,7 @@ interface FieldHandlers { interface FieldDefinition { name: string + key?: string label?: string value?: any default?: any @@ -253,12 +254,13 @@ interface FormInitializer{ interface initializationDefinition { fields?:FieldDefinitions[] + hooks?: FormHooks } export class Form implements Base { - constructor(definition:initializationDefinition, options?:any) + constructor(definition?:initializationDefinition, options?:any) plugins(): void setup(): any onInit(): void @@ -269,7 +271,7 @@ export class Form implements Base { $(fieldName: string): Field; - add(obj: any): any; + add(obj:FieldDefinition): any; check(computed: string, deep?: boolean): boolean; @@ -292,7 +294,7 @@ export class Form implements Base { map(callback: (i: Field) => void); - observe(obj: any); + observe(...obj: any) select(path: string): Field; @@ -304,10 +306,12 @@ export class Form implements Base { update(obj: any): void; readonly submitting: boolean; + readonly isValid:boolean; protected validator :any readonly isValid :boolean; + readonly size:number } @@ -327,7 +331,7 @@ interface SharedFieldFormMethods { has(key:string):boolean map(callback:(i:Field) => void) each(callback:(i:Field) => void) - add(obj:any):any + add(obj:FieldDefinition): any; del(key:any) observe(obj:any) intercept(obj:any)