From 286c00c0d874eac11f69492dcd7e43a97cef851e Mon Sep 17 00:00:00 2001 From: Thomas Pucci Date: Thu, 5 Apr 2018 17:57:37 +0200 Subject: [PATCH 1/2] feat(package): Add withPickerValues hoc --- index.js | 33 ++++--- jest.config.js | 9 +- package.json | 2 + src/withPickerValues/DisableKeyboard.js | 26 ++++++ src/withPickerValues/KeyboardModal.js | 111 +++++++++++++++++++++++ src/withPickerValues/PickerModal.js | 54 +++++++++++ src/withPickerValues/index.js | 1 + src/withPickerValues/withPickerValues.js | 17 ++++ yarn.lock | 30 +++++- 9 files changed, 266 insertions(+), 17 deletions(-) create mode 100644 src/withPickerValues/DisableKeyboard.js create mode 100644 src/withPickerValues/KeyboardModal.js create mode 100644 src/withPickerValues/PickerModal.js create mode 100644 src/withPickerValues/index.js create mode 100644 src/withPickerValues/withPickerValues.js diff --git a/index.js b/index.js index 346cebf..0e73233 100644 --- a/index.js +++ b/index.js @@ -1,14 +1,24 @@ -import { compose } from "recompose"; -import setFormikInitialValue from "./src/setFormikInitialValue"; -import withError from "./src/withError"; -import withFocus from "./src/withFocus"; -import withFormik from "./src/withFormik"; -import withInputTypeProps from "./src/withInputTypeProps"; -import withTouched from "./src/withTouched"; -import makeReactNativeField from "./src/makeReactNativeField"; -import { withNextInputAutoFocusForm, withNextInputAutoFocusInput } from "./src/withNextInputAutoFocus"; +import { compose } from 'recompose'; +import setFormikInitialValue from './src/setFormikInitialValue'; +import withError from './src/withError'; +import withFocus from './src/withFocus'; +import withFormik from './src/withFormik'; +import withInputTypeProps from './src/withInputTypeProps'; +import withTouched from './src/withTouched'; +import withPickerValues from './src/withPickerValues'; +import makeReactNativeField from './src/makeReactNativeField'; +import { + withNextInputAutoFocusForm, + withNextInputAutoFocusInput, +} from './src/withNextInputAutoFocus'; -const makeInputsGreatAgain = compose(withInputTypeProps, setFormikInitialValue, withError, withTouched, makeReactNativeField); +const makeInputsGreatAgain = compose( + withInputTypeProps, + setFormikInitialValue, + withError, + withTouched, + makeReactNativeField +); export default makeInputsGreatAgain; @@ -22,5 +32,6 @@ export { withInputTypeProps, withTouched, withNextInputAutoFocusForm, - withNextInputAutoFocusInput + withNextInputAutoFocusInput, + withPickerValues, }; diff --git a/jest.config.js b/jest.config.js index 2e6709f..cf80afd 100644 --- a/jest.config.js +++ b/jest.config.js @@ -1,6 +1,9 @@ -const path = require("path"); +const path = require('path'); module.exports = { - preset: "react-native", - setupFiles: [path.join(__dirname, "./jest/preamble.js")] + preset: 'react-native', + transformIgnorePatterns: [ + 'node_modules/(?!(react-native|react-native-button|react-native-core-library|@bam.tech/[w-]*|static-container|react-native-tab-view)|react-native-iphone-x-helper/)', + ], + setupFiles: [path.join(__dirname, './jest/preamble.js')], }; diff --git a/package.json b/package.json index 6b6ab74..0a06d75 100644 --- a/package.json +++ b/package.json @@ -21,6 +21,8 @@ "forms" ], "dependencies": { + "@bam.tech/react-native-modalbox": "^1.4.2", + "@bam.tech/react-native-root-siblings": "^2.0.3", "lodash": "4.17.5", "recompose": "^0.26.0" }, diff --git a/src/withPickerValues/DisableKeyboard.js b/src/withPickerValues/DisableKeyboard.js new file mode 100644 index 0000000..28a39a6 --- /dev/null +++ b/src/withPickerValues/DisableKeyboard.js @@ -0,0 +1,26 @@ +// @flow + +import React, { PureComponent } from 'react'; +import { TouchableOpacity, View } from 'react-native'; + +type _Props = { + children: ?*, + onPress: () => any, + children: any, +}; + +export default class DisableInputKeyboard extends PureComponent { + static defaultProps = { + children: null, + }; + + props: _Props; + + render() { + return ( + + {this.props.children} + + ); + } +} diff --git a/src/withPickerValues/KeyboardModal.js b/src/withPickerValues/KeyboardModal.js new file mode 100644 index 0000000..332c38d --- /dev/null +++ b/src/withPickerValues/KeyboardModal.js @@ -0,0 +1,111 @@ +// @flow + +import React, { PureComponent } from 'react'; +import { Easing, Keyboard } from 'react-native'; +import Modal from '@bam.tech/react-native-modalbox'; +import RootSiblings from '@bam.tech/react-native-root-siblings'; + +type _Props = { + style: Object, + easingAnimation: () => void, + children: any, +}; + +const renderModal = (props: _Props, open: ?boolean) => ( + + {props.children} + +); + +let keyboardModalInstance = null; +let displayedKeyboardComponent = null; +let keyboardModalCount = 0; +let keyboardDidShowListener = null; +let currentProps; + +const updateKeyboardModalComponent = (props: _Props, open: ?boolean) => { + if (open) currentProps = props; + if (keyboardModalInstance) keyboardModalInstance.update(renderModal(props, open)); +}; + +const open = (keyboardComponent: any) => { + if (displayedKeyboardComponent) displayedKeyboardComponent.displayed = false; + + displayedKeyboardComponent = keyboardComponent; + displayedKeyboardComponent.displayed = true; + + updateKeyboardModalComponent(keyboardComponent.props, true); +}; + +const keyboardDidShow = () => updateKeyboardModalComponent(currentProps, false); + +const createKeyboardModalComponent = (props: _Props) => { + keyboardModalCount += 1; + + if (keyboardModalCount > 1) return; + + currentProps = props; + + keyboardModalInstance = new RootSiblings(renderModal(props)); + debugger; + keyboardDidShowListener = Keyboard.addListener('keyboardWillShow', keyboardDidShow); +}; + +const removeKeyboardModalComponent = () => { + keyboardModalCount -= 1; + + if (keyboardModalCount === 0) { + if (keyboardDidShowListener) keyboardDidShowListener.remove(); + if (keyboardModalInstance) keyboardModalInstance.remove(); + } +}; + +export default class KeyboardModal extends PureComponent { + static dismiss() { + if (keyboardModalCount > 0) { + updateKeyboardModalComponent(currentProps, false); + } + } + + static defaultProps = { + easingAnimation: Easing.ease, + }; + + componentWillMount() { + createKeyboardModalComponent(this.props); + } + + componentWillReceiveProps(nextProps: _Props) { + if (this.displayed) { + updateKeyboardModalComponent(nextProps); + } + } + + componentWillUnmount() { + removeKeyboardModalComponent(); + } + + displayed: boolean = false; + + open() { + this.displayed = true; + Keyboard.dismiss(); + open(this); + } + + close() { + this.displayed = false; + updateKeyboardModalComponent(this.props, false); + } + + render() { + return null; + } +} diff --git a/src/withPickerValues/PickerModal.js b/src/withPickerValues/PickerModal.js new file mode 100644 index 0000000..9449e0d --- /dev/null +++ b/src/withPickerValues/PickerModal.js @@ -0,0 +1,54 @@ +// @flow +import React, { PureComponent } from 'react'; +import { View, Platform, Picker } from 'react-native'; + +import KeyboardModal from './KeyboardModal'; +import DisableKeyboard from './DisableKeyboard'; + +type PropsType = { + values: Array<{ label: string, value: string }>, +}; + +class PickerModal extends PureComponent { + pickerModal: ?KeyboardModal; + + openPicker = () => { + if (this.pickerModal) this.pickerModal.open(); + }; + + onValueChange = (value: any) => { + if (this.props.onChangeText) this.props.onChangeText(value); + }; + + renderPicker = () => { + const picker = ( + + + {this.props.values.map(item => )} + + ); + + return Platform.OS === 'ios' ? ( + { + this.pickerModal = ref; + }} + > + {picker} + + ) : ( + picker + ); + }; + + render() { + return ( + + {this.props.children} + {this.renderPicker()} + + ); + } +} + +export default PickerModal; diff --git a/src/withPickerValues/index.js b/src/withPickerValues/index.js new file mode 100644 index 0000000..39e6c7d --- /dev/null +++ b/src/withPickerValues/index.js @@ -0,0 +1 @@ +export { default } from './withPickerValues'; diff --git a/src/withPickerValues/withPickerValues.js b/src/withPickerValues/withPickerValues.js new file mode 100644 index 0000000..ab1a52b --- /dev/null +++ b/src/withPickerValues/withPickerValues.js @@ -0,0 +1,17 @@ +// @flow + +import React from 'react'; +import { compose } from 'recompose'; + +import PickerModal from './PickerModal'; + +const withPickerModal = Component => props => { + const selectedItem = props.values.find(item => item.value === props.value); + return ( + + + + ); +}; + +export default compose(withPickerModal); diff --git a/yarn.lock b/yarn.lock index 72071af..f98de8b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -16,6 +16,20 @@ esutils "^2.0.2" js-tokens "^3.0.0" +"@bam.tech/react-native-modalbox@^1.4.2": + version "1.4.2" + resolved "https://registry.yarnpkg.com/@bam.tech/react-native-modalbox/-/react-native-modalbox-1.4.2.tgz#85d130358354834bccb74f5a8d0384e8b5491e29" + dependencies: + create-react-class "^15.6.0" + prop-types "^15.5.10" + +"@bam.tech/react-native-root-siblings@^2.0.3": + version "2.0.3" + resolved "https://registry.yarnpkg.com/@bam.tech/react-native-root-siblings/-/react-native-root-siblings-2.0.3.tgz#ac1dee579874998b2796ad20f6648d7033320c5c" + dependencies: + fbemitter "^2.1.1" + static-container "^1.0.0" + "@mrmlnc/readdir-enhanced@^2.2.1": version "2.2.1" resolved "https://registry.yarnpkg.com/@mrmlnc/readdir-enhanced/-/readdir-enhanced-2.2.1.tgz#524af240d1a360527b730475ecfa1344aa540dde" @@ -1811,7 +1825,7 @@ create-error-class@^3.0.0: dependencies: capture-stack-trace "^1.0.0" -create-react-class@^15.5.2: +create-react-class@^15.5.2, create-react-class@^15.6.0: version "15.6.3" resolved "https://registry.yarnpkg.com/create-react-class/-/create-react-class-15.6.3.tgz#2d73237fb3f970ae6ebe011a9e66f46dbca80036" dependencies: @@ -2522,6 +2536,12 @@ fb-watchman@^2.0.0: dependencies: bser "^2.0.0" +fbemitter@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/fbemitter/-/fbemitter-2.1.1.tgz#523e14fdaf5248805bb02f62efc33be703f51865" + dependencies: + fbjs "^0.8.4" + fbjs-scripts@^0.8.1: version "0.8.1" resolved "https://registry.yarnpkg.com/fbjs-scripts/-/fbjs-scripts-0.8.1.tgz#c1c6efbecb7f008478468976b783880c2f669765" @@ -2535,7 +2555,7 @@ fbjs-scripts@^0.8.1: semver "^5.1.0" through2 "^2.0.0" -fbjs@^0.8.1, fbjs@^0.8.14, fbjs@^0.8.16, fbjs@^0.8.9: +fbjs@^0.8.1, fbjs@^0.8.14, fbjs@^0.8.16, fbjs@^0.8.4, fbjs@^0.8.9: version "0.8.16" resolved "https://registry.yarnpkg.com/fbjs/-/fbjs-0.8.16.tgz#5e67432f550dc41b572bf55847b8aca64e5337db" dependencies: @@ -5348,7 +5368,7 @@ promise@^7.1.1: dependencies: asap "~2.0.3" -prop-types@^15.5.8, prop-types@^15.6.0: +prop-types@^15.5.10, prop-types@^15.5.8, prop-types@^15.6.0: version "15.6.1" resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.6.1.tgz#36644453564255ddda391191fb3a125cbdf654ca" dependencies: @@ -6342,6 +6362,10 @@ stacktrace-parser@^0.1.3: version "0.1.4" resolved "https://registry.yarnpkg.com/stacktrace-parser/-/stacktrace-parser-0.1.4.tgz#01397922e5f62ecf30845522c95c4fe1d25e7d4e" +static-container@^1.0.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/static-container/-/static-container-1.2.0.tgz#1c6e92b869da47d32abe47f45f50bf39661d9c2f" + static-extend@^0.1.1: version "0.1.2" resolved "https://registry.yarnpkg.com/static-extend/-/static-extend-0.1.2.tgz#60809c39cbff55337226fd5e0b520f341f1fb5c6" From 652949da7ebb9dbf151ac7ed9d8cc5c47d4517df Mon Sep 17 00:00:00 2001 From: Thomas Pucci Date: Thu, 5 Apr 2018 18:02:33 +0200 Subject: [PATCH 2/2] docs(package): Add withPickerValues paragraph --- README.md | 178 +++++++++++++++++++++++++++++++++--------------------- 1 file changed, 110 insertions(+), 68 deletions(-) diff --git a/README.md b/README.md index de90f29..e5b2791 100644 --- a/README.md +++ b/README.md @@ -6,26 +6,27 @@ This repository is a set of high order components designed to help you take cont **Features** -- Easily composable set of helpers -- Connects your React Native input to Formik with no boilerplate (See `makeReactNativeField`) -- Add a `type` prop on your TextInput to take care of the input options based on the type (See `withInputTypeProps`) -- Automatically focus the next input (See `withNextInputAutoFocus`) -- Awesome Test coverage [![Coverage Status](https://coveralls.io/repos/github/bamlab/react-native-formik/badge.svg?branch=master)](https://coveralls.io/github/bamlab/react-native-formik?branch=master) +* Easily composable set of helpers +* Connects your React Native input to Formik with no boilerplate (See `makeReactNativeField`) +* Add a `type` prop on your TextInput to take care of the input options based on the type (See `withInputTypeProps`) +* Automatically focus the next input (See `withNextInputAutoFocus`) +* Awesome Test coverage [![Coverage Status](https://coveralls.io/repos/github/bamlab/react-native-formik/badge.svg?branch=master)](https://coveralls.io/github/bamlab/react-native-formik?branch=master) **Table of contents** -- [Installation](#installation) -- [Advanced Example](#advanced-example) -- [Formatting inputs](#formatting-inputs) -- [API](#api) - - [makeReactNativeField](#makereactnativefield) - - [setFormikInitialValue](#setFormikInitialValue) - - [withError](#witherror) - - [withFocus](#withfocus) - - [withFormik](#withformik) - - [withInputTypeProps](#withinputtypeprops) - - [withNextInputAutoFocus](#withnextinputautofocus) - - [withTouched](#withtouched) +* [Installation](#installation) +* [Advanced Example](#advanced-example) +* [Formatting inputs](#formatting-inputs) +* [API](#api) + * [makeReactNativeField](#makereactnativefield) + * [setFormikInitialValue](#setFormikInitialValue) + * [withError](#witherror) + * [withFocus](#withfocus) + * [withFormik](#withformik) + * [withInputTypeProps](#withinputtypeprops) + * [withNextInputAutoFocus](#withnextinputautofocus) + * [withTouched](#withtouched) + * [withPickerValues](#withpickervalues) ## Installation @@ -38,6 +39,7 @@ yarn add react-native-formik Say we want to create a form with Material design inputs. ### Create a custom input + Let's create our custom text input design, called `MaterialTextInput`: We can use [react-native-material-textfield](https://github.com/n4kz/react-native-material-textfield) for the material design. @@ -47,9 +49,9 @@ Notice our component also implement a `focus` function, for `withNextInputAutoFo ```javascript // MaterialTextInput.js -import React from "react"; -import { Text, View } from "react-native"; -import { TextField } from "react-native-material-textfield"; +import React from 'react'; +import { Text, View } from 'react-native'; +import { TextField } from 'react-native-material-textfield'; export default class MaterialTextInput extends React.PureComponent { // Your custom input needs a focus function for `withNextInputAutoFocus` to work @@ -61,19 +63,25 @@ export default class MaterialTextInput extends React.PureComponent { const { error, touched, ...props } = this.props; const displayError = !!error && touched; - const errorColor = "rgb(239, 51, 64)"; + const errorColor = 'rgb(239, 51, 64)'; return ( this.input = input} + ref={input => (this.input = input)} labelHeight={12} - baseColor={displayError ? errorColor : "#1976D2"} + baseColor={displayError ? errorColor : '#1976D2'} tintColor="#2196F3" textColor="#212121" {...props} /> - + {error} @@ -90,9 +98,9 @@ Compose our input with high order components to make it awesome. Let's add in `withNextInputAutoFocusInput`: ```javascript -import { compose } from "recompose"; -import makeInputGreatAgain, { withNextInputAutoFocusInput } from "react-native-formik"; -import MaterialTextInput from "./MaterialTextInput"; +import { compose } from 'recompose'; +import makeInputGreatAgain, { withNextInputAutoFocusInput } from 'react-native-formik'; +import MaterialTextInput from './MaterialTextInput'; const MyInput = compose(makeInputGreatAgain, withNextInputAutoFocusInput)(MaterialTextInput); ``` @@ -100,8 +108,8 @@ const MyInput = compose(makeInputGreatAgain, withNextInputAutoFocusInput)(Materi To complement `withNextInputAutoFocusInput`, we need to create a `Form` component, for instance: ```javascript -import { View } from "react-native"; -import { withNextInputAutoFocusForm } from "react-native-formik"; +import { View } from 'react-native'; +import { withNextInputAutoFocusForm } from 'react-native-formik'; const Form = withNextInputAutoFocusForm(View); ``` @@ -109,7 +117,7 @@ const Form = withNextInputAutoFocusForm(View); We can also create a validation schema, with `yup`. It's of course possible to use other validation possibilities provided by Formik, but `yup` makes validation and error messaging painless. ```javascript -import Yup from "yup"; +import Yup from 'yup'; const validationSchema = Yup.object().shape({ email: Yup.string() @@ -117,7 +125,7 @@ const validationSchema = Yup.object().shape({ .email("well that's not an email"), password: Yup.string() .required() - .min(2, "pretty sure this will be hacked") + .min(2, 'pretty sure this will be hacked'), }); ``` @@ -126,7 +134,7 @@ Then the form in itself becomes simple: ```javascript export default props => ( console.log(values)} + onSubmit={values => console.log(values)} validationSchema={validationSchema} render={props => { return ( @@ -146,30 +154,32 @@ export default props => ( Full code: ```javascript -import React from "react"; -import { Button, TextInput, View } from "react-native"; -import { compose } from "recompose" -import { Formik } from "formik"; -import Yup from "yup"; -import makeInputGreatAgain, { withNextInputAutoFocusForm, withNextInputAutoFocusInput } from "react-native-formik"; -import MaterialTextInput from "./MaterialTextInput"; +import React from 'react'; +import { Button, TextInput, View } from 'react-native'; +import { compose } from 'recompose'; +import { Formik } from 'formik'; +import Yup from 'yup'; +import makeInputGreatAgain, { + withNextInputAutoFocusForm, + withNextInputAutoFocusInput, +} from 'react-native-formik'; +import MaterialTextInput from './MaterialTextInput'; const MyInput = compose(makeInputGreatAgain, withNextInputAutoFocusInput)(MaterialTextInput); const Form = withNextInputAutoFocusForm(View); const validationSchema = Yup.object().shape({ email: Yup.string() - .required("please! email?") + .required('please! email?') .email("well that's not an email"), password: Yup.string() .required() - .min(2, "pretty sure this will be hacked") + .min(2, 'pretty sure this will be hacked'), }); - export default props => ( console.log(values)} + onSubmit={values => console.log(values)} validationSchema={validationSchema} render={props => { return ( @@ -212,22 +222,24 @@ const formatPhoneNumber: string => string = (unformattedPhoneNumber) => ...; ### makeReactNativeField Connects your React Native component to the Formik context: -- its value will be set -- it will send `onChangeText` and `onBlur` events to Formik + +* its value will be set +* it will send `onChangeText` and `onBlur` events to Formik Now you only need this code: + ```javascript -import React from "react"; -import { TextInput, View } from "react-native"; -import { Formik } from "formik"; -import { makeReactNativeField } from "react-native-formik"; +import React from 'react'; +import { TextInput, View } from 'react-native'; +import { Formik } from 'formik'; +import { makeReactNativeField } from 'react-native-formik'; const MyInput = makeReactNativeField(TextInput); export default props => { return ( console.log(values)} + onSubmit={values => console.log(values)} render={props => { return ( @@ -242,32 +254,33 @@ export default props => { ``` instead of: + ```javascript -import React from "react"; -import { TextInput, View } from "react-native"; -import { Formik } from "formik"; -import { makeReactNativeField } from "react-native-formik"; +import React from 'react'; +import { TextInput, View } from 'react-native'; +import { Formik } from 'formik'; +import { makeReactNativeField } from 'react-native-formik'; const MyInput = makeReactNativeField(TextInput); export default props => { return ( console.log(values)} + onSubmit={values => console.log(values)} render={props => { return ( props.setFieldValue("email", text)} - onBlur={() => setFieldTouched("email")} + onChangeText={text => props.setFieldValue('email', text)} + onBlur={() => setFieldTouched('email')} /> props.setFieldValue("password", text)} - onBlur={() => setFieldTouched("password")} + onChangeText={text => props.setFieldValue('password', text)} + onBlur={() => setFieldTouched('password')} /> ); @@ -302,8 +315,8 @@ Let's face it, you'll always want to remove auto-capitalization for email inputs Using `withInputTypeProps` and passing a `type`, you'll always get the correct props for you input. ```javascript -import { TextInput } from "react-native"; -import { withInputTypeProps } from "react-native-formik"; +import { TextInput } from 'react-native'; +import { withInputTypeProps } from 'react-native-formik'; const MyInput = withInputTypeProps(TextInput); @@ -316,21 +329,21 @@ Check [the props set by the type](./src/withInputTypeProps) in the source! ### withNextInputAutoFocus -- when an input is submitted, it will automatically focuses on the next or submit the form if it's the last one -- sets return key to "next" or "done" if input is the last one or not -- :warning: your input component needs to be a class and needs to implement a `focus` function -- :warning: Inputs need to be wrapped by `withNextInputAutoFocusInput` and the container of the inputs need to be wrapped in `withNextInputAutoFocusForm`. +* when an input is submitted, it will automatically focuses on the next or submit the form if it's the last one +* sets return key to "next" or "done" if input is the last one or not +* :warning: your input component needs to be a class and needs to implement a `focus` function +* :warning: Inputs need to be wrapped by `withNextInputAutoFocusInput` and the container of the inputs need to be wrapped in `withNextInputAutoFocusForm`. ```javascript -import { TextInput, View } from "react-native"; -import { withNextInputAutoFocusForm, withNextInputAutoFocusInput } from "react-native-formik"; +import { TextInput, View } from 'react-native'; +import { withNextInputAutoFocusForm, withNextInputAutoFocusInput } from 'react-native-formik'; const MyInput = withNextInputAutoFocusInput(TextInput); const Form = withNextInputAutoFocusForm(View); export default props => ( console.log(values)} + onSubmit={values => console.log(values)} validationSchema={validationSchema} render={props => { return ( @@ -348,3 +361,32 @@ export default props => ( ### withTouched Pass in the Formik touched value for the input as a prop. + +### withPickerValues + +Wraps your component into a `TouchableOpacity` which, when pressed, opens a dialog to pick a value. +You need to provide a `values` props with the pickable items. + +```javascript +import { TextInput, View } from 'react-native'; +import { withPickerValues } from 'react-native-formik'; + +const MyPicker = withPickerValues(TextInput); + +export default props => ( + console.log(values)} + validationSchema={validationSchema} + render={props => { + return ( + + + + ); + }} + /> +); +```