npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2024 – Pkg Stats / Ryan Hefner

react-native-merlin

v1.3.0

Published

Web like forms in React Native

Downloads

13

Readme

About

Merlin is still in early beta, it's in a few apps currently in development and the API is mostly stable.

Features

  • 🔎  Auto focus on the next input
  • 📚  Built-in validation with support for custom validators.
  • ❌  Built-in error handling.
  • 💾  Support for external form state.
  • 📦  Dependency free.

Roadmap

  • Async form validators.

...and more in the future.


Installation

#yarn
yarn add react-native-merlin

# npm
npm install --save react-native-merlin

Usage

Merlin takes care of state management for you. At it's most basic it looks like this:

<Form onSubmit={values => console.log(values)}>
  <Form.Input name="username" required />
  <Form.Submit title="Submit" />
</Form>

This will render out a FormProvider to hold your form state, an input which defaults to a TextInput and a submit button which defaults to a Button. It will also run validation on the username to check it has input, and if all the validation passes then your onSubmit is called with the form values.

{
  username: 'Lancelot'
}

Handling Form Submissions

It's not much use to have a form that just console logs out some values if it's valid and does nothing if there is an error, so we need to add some logic around that. First up is showing any relevant errors to the user using <Form.Error>.

<Form onSubmit={values => console.log(values)} onError={errors => console.log(errors)}>
  <Form.Input name="username" required />
  <Form.Error name="username" />
  <Form.Submit title="Submit" />
</Form>

This will render out a Text element if there are any errors that match the input name, these can be placed anywhere inside the <Form> parent.

The other change we made was adding the onError prop so we know when the form submission failed due to a validation error. We can then report this back to other parts of the app or to an external service.

Currently, the validation will only run on an input when the form is submitted or when the input changes and it already has an error, this is the default behaviour so we don't start trying to validate as the user starts typing which is bad UX. If you need this on certain fields that need feedback as you type(such as a password strength indicator). Then you can use the instantValidation prop which will run the validator on every change.

Async Form Submissions

Your form submission is most likely going to post off to a backend and take a second or two to finish submitting. In this time you might want to disable the form from being submitted again or display something to the user so they know it's working away in the background. Merlin makes this simple, provide an async function as the onSubmit handler and Merlin will wait until it resolves.

To get access to the current form state you can use <Form.State> and a render function.

const wait = duration => new Promise(success => setTimeout(success, duration))
const handleSubmit = async values => {
  await wait(2000) // this is where you would do your post instead.
  console.log('Submitted! ', values)
}

;<Form onSubmit={handleSubmit}>
  <Form.Input name="username" required />
  <Form.Error name="username" />
  <Form.State>
    {({ submitting }) => <Form.Submit disabled={submitting} title={submitting ? 'Submitting...' : 'Submit'} />}
  </Form.State>
</Form>

Integrating with external validation errors

If you are submitting to an external service, you will probably get some validation errors back if your local validation doesn't quite match the servers. Not to worry, you can handle this with a bit of extra work in Merlin. First we need to add a ref to the form so we can get access to a few helper methods.

Then we can use the addErrors helper to add our external errors.

const formRef = useRef(null)
const wait = duration => new Promise((success, fail) => setTimeout(fail, duration))
const handleSubmit = async values => {
  try {
    await wait(2000) // this is where you would do your post instead.
  } catch (e) {
    formRef.current.addErrors(error => ({
      username: error('externalError', 'Error from the api!'),
    }))
  }
}

;<Form ref={formRef} onSubmit={handleSubmit}>
  <Form.Input name="username" required />
  <Form.Submit title="Submit" />
</Form>

Using Custom Validators

Merlin ships with some basic validators loosely based on the built-in HTML5 form validation. Currently just required, minLength and maxLength but these will be expanded on in the future if needed to cover more common use cases. If you need to expand beyond this and want to integrate your own validators, it's very simple. Just add a validator prop to the <Form.Input>.

const isNotFoo = (value, error) => value !== 'Foo' && error('notFoo', `Value should not be Foo.`)

;<Form onError={errors => console.log(errors)}>
  <Form.Input name="username" required validator={isNotFoo} />
  <Form.Submit title="Submit" />
</Form>

A validator is provided (value, error, values).

value is the current value of the field you are running the validator on. error is a helper to return an error in the format that Merlin expects. The first argument is the type of error, and the second is the message and values gives you access to other values in the form in case you need them to do a comparision. A great example of this is password matching for a confirmation field.

const confirmPassword = (value, error, values) => value !== values.password && error('passwordMismatch', "Passwords don't match");

<Form onError={errors => console.log(errors)}>
    <Form.Input name="password" required />
    <Form.Input
      name="password_confirmation"
      required
      validator={confirmPassword}
    />
    <Form.Error name="password_confirmation">
</Form>

Using Custom Error Messages

Merlin gives you some default error messages out of the box, but sometimes these aren't the nicest looking if your using certain input names. Take the password_confirmation from above for example. By default that will return The password_confirmation field is required. which isn't great. You can work around this by supplying custom messages to the <Form.Input> to overwrite the existing ones or add support for custom error types.

<Form>
  <Form.Input
    name="password_confirmation"
    required
    messages={{
      required: 'Password Confirmation is a required field.',
      notFoo: ({ name, value }) => `This can also be a function that returns a string with the ${name}`,
    }}
  />
</Form>

Using Custom Inputs

Just using the built-in inputs provided by react-native won't get you very far when it comes to styling up you form, adding custom functionality or integrating 3rd party inputs.

You can specify what the <Form.Input> should render as by providing the as prop. Merlin will then render the input using that component instead and pass along all the props you defined as well as any props managed by Merlin (such as the value or error for the field).

<Form onSubmit={values => console.log(values)}>
  <Form.Input as={StyledTextInput} name="username" label="User" required />
  <Form.Submit as={Button} title="Submit" />
</Form>

And this is what the StyledTextInput would look like. You need to make sure to use forwardRef to pass along the ref handled by Merlin onto the actual input you want to use.

const StyledTextInput = React.forwardRef(({ error, label, ...props }, ref) => (
  <View>
    {label && <Text style={styles.label}>{label}</Text>}
    <TextInput {...props} ref={ref} />
    {error && <Text>{error.message}</Text>}
  </View>
))

If you're using a third-party component or integrating an existing component, you may need to tell Merlin how to integrate with your input properly. There are a few ways that you can do this. This example uses a Switch component from react-native.

<Form.Input
  as={Switch}
  name="example"
  required
  valueKey="value"
  eventKey="onValueChange"
  parseValue={value => (value ? 1 : 0)}
/>

Here we are making use of valueKey to tell Merlin what prop the component expects the form value as, eventKey to know what to listen to so we can update the form state and parseValue to transform the value before we put it in the form state. In this instance, we are converting the Boolean to a Number, but it could be anything. Using these three props makes it possible to integrate almost any component directly into Merlin.

Using External State

If you don't want to manage your state locally using Merlin but still want all the other benefits then you can pass in values and errors to the <Form> component.


const initialValues = {
  username: 'Arthur',
};

<Form
    values={initialValues}
>
    <Form.Input name="username" />
    <Form.Submit title="Submit">
</Form>

By default, this will just set the initial state of the form on the first render to match those values but you can ensure that the form stays up to date with any changes by passing the watch prop. You can also watch values and errors separately with watchValues and watchErrors respectively.


const [values, setValues] = useState({
    username: 'Percival'
})

<Form
    values={values}
    watchValues={true}
>
    <View>
        <Button onPress={() => setValues({username: 'Uther'})} title="Change Name">
    </View>
    <View>
        <Form.Input name="username" />
        <Form.Submit title="Submit">
    </View>
</Form>

Components

Form

<Form onSubmit={} onError={} values={{}} errors={{}} watch={false} watchValues={false} watchErrors={false}>
  {/* Your content goes here*/}
</Form>
Props

| Prop | Type | Description | | ------------- | ---------------------------- | ------------------------------------------------------------------------------ | | onSubmit | Function | Function to call when the form passes validation on submission. | | onError | Function | Function to call when the form fails validation on submission. | | values | Object (Default: {}) | External values to pass to the form for the inputs to use as an initial value. | | errors | Array (Default: {}) | External errors to pass to the form for the inputs to use as an initial error. | | watch | Boolean (Default: false) | Should the form update internal state if values or errors changes. | | watchValues | Boolean (Default: false) | Should the form update internal state if values changes. | | watchErrors | Boolean (Default: false) | Should the form update internal state if errors changes. |

Ref props

| Prop | Type | Description | | ------------- | -------- | ------------------------------------------------------------------------------------------------------------------ | | submit | Function | Submit the form from outside of the form context. | | addErrors | Function | Add additional errors to the internal form errors, for instance from an external API. | | clearErrors | Function | Clear errors from the internal form errors, pass an array of names to specify what to remove or remove everything. |

const formRef = useRef()
const form = formRef.current

// Submit the form from outside the form context
const submit = () => form.submit()

// Add external errors.
const externalErrors = () => form.addErrors(error => {
  return {
    username: error('externalUsername', 'Username is not foo!')
  }
}

// Clear specific errors
const clear = () => form.clearErrors(['username'])
// Clear all errors
const clearAll = () => form.clearErrors()

<View>
  <Form ref={formRef}>
    <Form.Input name="username" required />
    <Form.Error name="username />
    <Form.Input name="password" secureTextInput />
    <Form.Error name="password />
    <Form.Submit title="Submit" />
  </Form>

  <Button title="Submit" onPress={submit} />
  <Button title="Add Errors" onPress={externalErrors} />
  <Button title="Clear Errors" onPress={clear} />
</View>

Form.Input

<Form.Input
  name=""
  as={TextInput}
  eventKey="onChangeText"
  valueKey="value"
  parseValue={}
  required
  maxLength={}
  minLength={}
  validator={}
/>
Props

| Prop | Type | Description | | ------------------- | ---------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------- | | name | String | Name for the input when mapped in to the form values. | | as | Component (Default: TextInput) | Component to render the input as. | | eventKey | String (Default: onChangeText) | Event from the input to listen to for value updates. | | valueKey | String (Default: value) | Prop from the input that expects the form value | | parseValue | Function | Function to handle input values before updating them in the form. | | instantValidation | Boolean (Default: false) | Should we start validating as soon as the user starts changing the input or only re-validate if we currently have an error for the field. | | required | Boolean (Default: false) | Field is required to not be falsey to submit the form. | | maxLength | Number | Field is required to be under the maxLength to submit the form. | | minLength | Number | Field is required to be over the minLength to submit the form. | | validator | Function | Custom validation function, return true to pass or return a custom error. | | messages | Object | An object of custom error messages where key is equal to the error type and value is the message |

Custom validator
const validator = (v, error, values) => {
  // Either return an error to fail or nothing/true to pass.
  // You can get access to other form values from values

  if (v !== 'Foo') {
    return error('notFoo', 'Thats not foo!')
  }

  return true
}

Form.Submit

<Form.Submit id="" as={Button} eventKey="" />
Props

| Prop | Type | Description | | ---------- | ---------------------------------- | -------------------------------------------------------------------------------- | | id | String | Can be consumed in the form onSubmit method to see where the submit came from. | | as | Component (Default: Button) | Component to render the input as. | | eventKey | String (Default: onChangeText) | Event from the input to listen to for value updates. |

Form.Error

<Form.Error name="" as={Text} />
Props

| Prop | Type | Description | | ------ | ----------------------------- | ------------------------------------------ | | name | String | Name of the field to render the error for. | | as | Component (Default: Text) | Component to render the input as. |

Render prop arguments

| Prop | Type | Description | | ------- | ------------------------------------ | ------------------------------------------ | | error | Object ({type: '', message: ''}) | Error passed down from the form validation |

Form.State

<Form.State as={React.Fragment} />
Props

| Prop | Type | Description | | ---- | --------------------------------------- | --------------------------------- | | as | Component (Default: React.Fragment) | Component to render the input as. |

Render prop arguments

| Prop | Type | Description | | ------------ | ------- | ----------------------------------------------------- | | values | Object | Access to the internal values state from the form. | | errors | Array | Access to the internal errors state from the form. | | inputs | Array | Access to the internal inputs tracking from the form. | | submitting | Boolean | Is the form currently submitting. |

Contribute

First off, thanks for taking the time to contribute! Now, take a moment to be sure your contributions make sense to everyone else.

Reporting Issues

Found a problem? Want a new feature? First of all see if your issue or idea has already been reported. If not, just open a new clear and descriptive issue.

Submitting pull requests

Pull requests are the greatest contributions, so be sure they are focused in scope, and do avoid unrelated commits.

  • Fork it!
  • Clone your fork: git clone https://github.com/<your-username>/react-native-merlin
  • Navigate to the newly cloned directory: cd react-native-merlin
  • Create a new branch for the new feature: git checkout -b my-new-feature
  • Install the tools necessary for development: yarn
  • Make your changes.
  • Commit your changes: git commit -am 'Add some feature'
  • Push to the branch: git push origin my-new-feature
  • Submit a pull request with full remarks documenting your changes.

License

MIT License © Harry Parton