Repository: final-form/react-final-form
Branch: main
Commit: fcea1a1af63a
Files: 293
Total size: 748.1 KB
Directory structure:
gitextract_z4w9ccys/
├── .babelrc.js
├── .eslintrc
├── .flowconfig
├── .github/
│ ├── CODEOWNERS
│ ├── CODE_OF_CONDUCT.md
│ ├── CONTRIBUTING.md
│ ├── FUNDING.yml
│ ├── ISSUE_TEMPLATE.md
│ ├── PULL_REQUEST_TEMPLATE.md
│ └── workflows/
│ ├── ci.yml
│ └── lock.yml
├── .gitignore
├── .prettierignore
├── .prettierrc
├── .travis.yml
├── LICENSE
├── MIGRATION_V7.md
├── README.md
├── docs/
│ ├── api/
│ │ ├── Field.md
│ │ ├── Form.md
│ │ ├── FormSpy.md
│ │ ├── useField.md
│ │ ├── useForm.md
│ │ └── useFormState.md
│ ├── api.md
│ ├── examples/
│ │ ├── chakra.md
│ │ ├── field-level-validation.md
│ │ ├── record-level-validation.md
│ │ ├── simple.md
│ │ ├── submission-errors.md
│ │ ├── subscriptions.md
│ │ └── wizard.md
│ ├── examples.md
│ ├── faq.md
│ ├── getting-started.md
│ ├── migration/
│ │ ├── formik.md
│ │ └── redux-form.md
│ ├── philosophy.md
│ └── types/
│ ├── FieldProps.md
│ ├── FieldRenderProps.md
│ ├── FormProps.md
│ ├── FormRenderProps.md
│ ├── FormSpyProps.md
│ └── FormSpyRenderProps.md
├── eslint.config.mjs
├── examples/
│ ├── async-field-level-validation/
│ │ ├── Spinner.js
│ │ ├── Styles.js
│ │ ├── index.js
│ │ ├── package.json
│ │ └── readme.md
│ ├── async-redux-submission/
│ │ ├── Styles.js
│ │ ├── asyncSubmissionMiddleware.js
│ │ ├── index.js
│ │ ├── package.json
│ │ ├── readme.md
│ │ ├── registrationDuck.js
│ │ └── store.js
│ ├── async-typeahead-redux/
│ │ ├── GithubUserTypeahead.jsx
│ │ ├── Styles.js
│ │ ├── actions.js
│ │ ├── index.js
│ │ ├── package.json
│ │ ├── readme.md
│ │ ├── store.js
│ │ └── useKeyword.js
│ ├── auto-save-field-blur/
│ │ ├── AutoSave.js
│ │ ├── Styles.js
│ │ ├── index.js
│ │ ├── package.json
│ │ └── readme.md
│ ├── auto-save-selective-debounce/
│ │ ├── AutoSave.js
│ │ ├── Styles.js
│ │ ├── index.js
│ │ ├── package.json
│ │ └── readme.md
│ ├── auto-save-with-debounce/
│ │ ├── AutoSave.js
│ │ ├── Styles.js
│ │ ├── index.js
│ │ ├── package.json
│ │ └── readme.md
│ ├── calculated-fields/
│ │ ├── Styles.js
│ │ ├── index.js
│ │ ├── package.json
│ │ └── readme.md
│ ├── chakra/
│ │ ├── index.js
│ │ ├── package.json
│ │ ├── readme.md
│ │ └── validate.js
│ ├── conditional-fields/
│ │ ├── Styles.js
│ │ ├── index.js
│ │ ├── package.json
│ │ ├── pickupTimes.js
│ │ └── readme.md
│ ├── credit-card/
│ │ ├── Card.js
│ │ ├── Styles.js
│ │ ├── cardUtils.js
│ │ ├── index.js
│ │ ├── package.json
│ │ ├── readme.md
│ │ └── sandbox.config.json
│ ├── custom-validation-engine/
│ │ ├── OnBlurValidation.js
│ │ ├── Styles.js
│ │ ├── index.js
│ │ ├── package.json
│ │ └── readme.md
│ ├── debounced-record-level-validation/
│ │ ├── ErrorWithDelay.js
│ │ ├── README.md
│ │ ├── Styles.js
│ │ ├── index.js
│ │ └── package.json
│ ├── declarative-form-rules/
│ │ ├── Styles.js
│ │ ├── index.js
│ │ ├── package.json
│ │ └── readme.md
│ ├── downshift-typeahead/
│ │ ├── DownshiftInput.js
│ │ ├── Styles.js
│ │ ├── fruit.js
│ │ ├── index.js
│ │ ├── package.json
│ │ └── readme.md
│ ├── external-submit/
│ │ ├── Styles.js
│ │ ├── index.js
│ │ ├── package.json
│ │ └── readme.md
│ ├── field-arrays/
│ │ ├── Styles.js
│ │ ├── index.js
│ │ ├── package.json
│ │ └── readme.md
│ ├── field-level-validation/
│ │ ├── Styles.js
│ │ ├── index.js
│ │ ├── package.json
│ │ └── readme.md
│ ├── field-warnings/
│ │ ├── Styles.js
│ │ ├── index.js
│ │ ├── package.json
│ │ ├── readme.md
│ │ └── warning-engine.js
│ ├── fields-component/
│ │ ├── Styles.js
│ │ ├── index.js
│ │ ├── package.json
│ │ └── readme.md
│ ├── focus-first-error/
│ │ ├── Styles.js
│ │ ├── index.js
│ │ ├── package.json
│ │ ├── readme.md
│ │ └── validate.js
│ ├── format-on-blur/
│ │ ├── Styles.js
│ │ ├── index.js
│ │ ├── package.json
│ │ └── readme.md
│ ├── format-string-by-pattern/
│ │ ├── Styles.js
│ │ ├── index.js
│ │ ├── package.json
│ │ └── readme.md
│ ├── hybrid-sync-async-record-level-validation/
│ │ ├── Spinner.js
│ │ ├── Styles.js
│ │ ├── index.js
│ │ ├── package.json
│ │ └── readme.md
│ ├── independent-error-component-render-props/
│ │ ├── Styles.js
│ │ ├── index.js
│ │ ├── package.json
│ │ └── readme.md
│ ├── independent-error-component-with-hooks/
│ │ ├── Styles.js
│ │ ├── index.js
│ │ ├── package.json
│ │ └── readme.md
│ ├── listening-for-external-changes/
│ │ ├── BooleanDecay.js
│ │ ├── ExternalModificationDetector.js
│ │ ├── Styles.js
│ │ ├── index.js
│ │ ├── package.json
│ │ └── readme.md
│ ├── loading-initializing-values/
│ │ ├── Styles.js
│ │ ├── index.js
│ │ ├── package.json
│ │ └── readme.md
│ ├── loading-saving-reinitializing/
│ │ ├── LoadSaveReinitializeForm.js
│ │ ├── Styles.js
│ │ ├── index.js
│ │ ├── package.json
│ │ └── readme.md
│ ├── material-ui/
│ │ ├── .prettierrc
│ │ ├── index.js
│ │ ├── package.json
│ │ └── readme.md
│ ├── parse-format/
│ │ ├── Styles.js
│ │ ├── index.js
│ │ ├── package.json
│ │ └── readme.md
│ ├── prefixed-fields/
│ │ ├── Styles.js
│ │ ├── index.js
│ │ ├── package.json
│ │ └── readme.md
│ ├── readme.md
│ ├── record-level-validation/
│ │ ├── Styles.js
│ │ ├── index.js
│ │ ├── package.json
│ │ └── readme.md
│ ├── redux/
│ │ ├── FormStateFromRedux.js
│ │ ├── FormStateToRedux.js
│ │ ├── Styles.js
│ │ ├── finalFormDuck.js
│ │ ├── index.js
│ │ ├── package.json
│ │ ├── readme.md
│ │ └── store.js
│ ├── reusable-field-groups/
│ │ ├── Styles.js
│ │ ├── index.js
│ │ ├── package.json
│ │ └── readme.md
│ ├── simple/
│ │ ├── Styles.js
│ │ ├── index.js
│ │ ├── package.json
│ │ └── readme.md
│ ├── strongly-typed-values-typescript/
│ │ ├── Styles.tsx
│ │ ├── components/
│ │ │ ├── CheckboxInput.tsx
│ │ │ ├── MultiCheckboxInput.tsx
│ │ │ ├── MultiSelectInput.tsx
│ │ │ ├── NumberInput.tsx
│ │ │ ├── RadioInput.tsx
│ │ │ ├── SelectInput.tsx
│ │ │ ├── TextAreaInput.tsx
│ │ │ └── TextInput.tsx
│ │ ├── index.tsx
│ │ └── readme.md
│ ├── styling-with-smooth-ui/
│ │ ├── index.js
│ │ ├── package.json
│ │ └── readme.md
│ ├── submission-errors/
│ │ ├── Styles.js
│ │ ├── index.js
│ │ ├── package.json
│ │ └── readme.md
│ ├── subscriptions/
│ │ ├── RenderCount.js
│ │ ├── Styles.js
│ │ ├── index.js
│ │ ├── package.json
│ │ └── readme.md
│ ├── third-party-components/
│ │ ├── Styles.js
│ │ ├── index.js
│ │ ├── package.json
│ │ ├── readme.md
│ │ └── states.js
│ └── wizard/
│ ├── Styles.js
│ ├── Wizard.js
│ ├── index.js
│ ├── package.json
│ └── readme.md
├── package-scripts.js
├── package.json
├── rollup.config.mjs
├── src/
│ ├── Field.test.js
│ ├── Field.tsx
│ ├── FormSpy.test.js
│ ├── FormSpy.tsx
│ ├── ReactFinalForm.test.js
│ ├── ReactFinalForm.tsx
│ ├── context.test.js
│ ├── context.ts
│ ├── getValue.test.js
│ ├── getValue.ts
│ ├── getters.ts
│ ├── index.ts
│ ├── isReactNative.ts
│ ├── isSyntheticEvent.ts
│ ├── renderComponent.test.js
│ ├── renderComponent.ts
│ ├── shallowEqual.test.js
│ ├── shallowEqual.ts
│ ├── testUtils.ts
│ ├── types.ts
│ ├── useConstant.ts
│ ├── useConstantCallback.test.js
│ ├── useConstantCallback.ts
│ ├── useField.dynamic-name-869.test.js
│ ├── useField.test.js
│ ├── useField.ts
│ ├── useForm.test.js
│ ├── useForm.ts
│ ├── useFormState.test.js
│ ├── useFormState.ts
│ ├── useLatest.ts
│ └── useWhenValueChanges.ts
├── tsconfig.build.json
├── tsconfig.json
├── tslint.json
└── typescript/
├── Field.test.tsx
├── FormSpy.test.tsx
├── ReactFinalForm.test.tsx
├── index.d.ts
├── tsconfig.json
├── useField.test.tsx
└── useFormState.test.tsx
================================================
FILE CONTENTS
================================================
================================================
FILE: .babelrc.js
================================================
const { NODE_ENV } = process.env;
const test = NODE_ENV === "test";
const loose = true;
module.exports = {
presets: [
[
"@babel/preset-env",
{
loose,
...(test ? { targets: { node: "8" } } : {}),
},
],
"@babel/preset-react",
"@babel/preset-typescript",
],
plugins: [
"@babel/plugin-syntax-dynamic-import",
"@babel/plugin-syntax-import-meta",
test && "@babel/plugin-transform-react-jsx-source",
].filter(Boolean),
};
================================================
FILE: .eslintrc
================================================
{
"extends": "react-app",
"plugins": ["react-hooks"],
"rules": {
"jsx-a11y/href-no-hash": 0,
"react-hooks/rules-of-hooks": "error",
"react-hooks/exhaustive-deps": "warn",
"import/no-anonymous-default-export": 0
}
}
================================================
FILE: .flowconfig
================================================
[ignore]
dist
[include]
[libs]
[options]
================================================
FILE: .github/CODEOWNERS
================================================
* @erikras
================================================
FILE: .github/CODE_OF_CONDUCT.md
================================================
# Contributor Covenant Code of Conduct
## Our Pledge
In the interest of fostering an open and welcoming environment, we as
contributors and maintainers pledge to making participation in our project and
our community a harassment-free experience for everyone, regardless of age, body
size, disability, ethnicity, gender identity and expression, level of
experience, nationality, personal appearance, race, religion, or sexual identity
and orientation.
## Our Standards
Examples of behavior that contributes to creating a positive environment
include:
* Using welcoming and inclusive language
* Being respectful of differing viewpoints and experiences
* Gracefully accepting constructive criticism
* Focusing on what is best for the community
* Showing empathy towards other community members
Examples of unacceptable behavior by participants include:
* The use of sexualized language or imagery and unwelcome sexual attention or
advances
* Trolling, insulting/derogatory comments, and personal or political attacks
* Public or private harassment
* Publishing others' private information, such as a physical or electronic
address, without explicit permission
* Other conduct which could reasonably be considered inappropriate in a
professional setting
## Our Responsibilities
Project maintainers are responsible for clarifying the standards of acceptable
behavior and are expected to take appropriate and fair corrective action in
response to any instances of unacceptable behavior.
Project maintainers have the right and responsibility to remove, edit, or reject
comments, commits, code, wiki edits, issues, and other contributions that are
not aligned to this Code of Conduct, or to ban temporarily or permanently any
contributor for other behaviors that they deem inappropriate, threatening,
offensive, or harmful.
## Scope
This Code of Conduct applies both within project spaces and in public spaces
when an individual is representing the project or its community. Examples of
representing a project or community include using an official project e-mail
address, posting via an official social media account, or acting as an appointed
representative at an online or offline event. Representation of a project may be
further defined and clarified by project maintainers.
## Enforcement
Instances of abusive, harassing, or otherwise unacceptable behavior may be
reported by contacting the project team at rasmussenerik@gmail.com. The project
team will review and investigate all complaints, and will respond in a way that
it deems appropriate to the circumstances. The project team is obligated to
maintain confidentiality with regard to the reporter of an incident. Further
details of specific enforcement policies may be posted separately.
Project maintainers who do not follow or enforce the Code of Conduct in good
faith may face temporary or permanent repercussions as determined by other
members of the project's leadership.
## Attribution
This Code of Conduct is adapted from the [Contributor Covenant][homepage],
version 1.4, available at [http://contributor-covenant.org/version/1/4][version]
[homepage]: http://contributor-covenant.org
[version]: http://contributor-covenant.org/version/1/4/
================================================
FILE: .github/CONTRIBUTING.md
================================================
# Contributing
Thanks for your interest in contributing to 🏁 React Final Form! Please take a
moment to review this document **before submitting a pull request**.
We are open to, and grateful for, any contributions made by the community.
## Reporting issues and asking questions
Before opening an issue, please search
the [issue tracker](https://github.com/final-form/react-final-form/issues) to
make sure your issue hasn’t already been reported.
**We use the issue tracker to keep track of bugs and improvements** to 🏁 React
Final Form itself, its examples, and the documentation. We encourage you to open
issues to discuss improvements, architecture, internal implementation, etc. If a
topic has been discussed before, we will ask you to join the previous
discussion.
For support or usage questions, please search and ask on
[StackOverflow with a `react-final-form` tag](https://stackoverflow.com/questions/tagged/react-final-form).
We ask you to do this because StackOverflow has a much better job at keeping
popular questions visible. Unfortunately good answers get lost and outdated on
GitHub.
**If you already asked at StackOverflow and still got no answers, post an issue
with the question link, so we can either answer it or evolve into a bug/feature
request.**
## Sending a pull request
**Please ask first before starting work on any significant new features.**
It's never a fun experience to have your pull request declined after investing a
lot of time and effort into a new feature. To avoid this from happening, we
request that contributors create
[an issue](https://github.com/final-form/react-final-form/issues) to first
discuss any significant new features.
Please try to keep your pull request focused in scope and avoid including
unrelated commits.
After you have submitted your pull request, we’ll try to get back to you as soon
as possible. We may suggest some changes or improvements.
Please format the code before submitting your pull request by running:
```sh
npm run precommit
```
## Coding standards
Our code formatting rules are defined in
[.eslintrc](https://github.com/final-form/react-final-form/blob/master/.eslintrc).
You can check your code against these standards by running:
```sh
npm start lint
```
To automatically fix any style violations in your code, you can run:
```sh
npm run precommit
```
## Running tests
You can run the test suite using the following commands:
```sh
npm test
```
Please ensure that the tests are passing when submitting a pull request. If
you're adding new features to 🏁 React Final Form, please include tests.
================================================
FILE: .github/FUNDING.yml
================================================
# These are supported funding model platforms
github: erikras
patreon: erikras
open_collective: final-form
ko_fi: # Replace with a single Ko-fi username
tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
liberapay: # Replace with a single Liberapay username
issuehunt: # Replace with a single IssueHunt username
otechie: # Replace with a single Otechie username
custom: # Replace with a single custom sponsorship URL
================================================
FILE: .github/ISSUE_TEMPLATE.md
================================================
### Are you submitting a **bug report** or a **feature request**?
### What is the current behavior?
### What is the expected behavior?
### Sandbox Link
### What's your environment?
### Other information
================================================
FILE: .github/PULL_REQUEST_TEMPLATE.md
================================================
================================================
FILE: .github/workflows/ci.yml
================================================
name: CI
on: [push]
jobs:
lint:
name: Lint
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Use Node.js ${{ matrix.node_version }}
uses: actions/setup-node@v2
with:
node-version: "22"
- name: Prepare env
run: yarn install --ignore-scripts --frozen-lockfile
- name: Run linter
run: yarn start lint
prettier:
name: Prettier Check
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Use Node.js ${{ matrix.node_version }}
uses: actions/setup-node@v2
with:
node-version: "22"
- name: Prepare env
run: yarn install --ignore-scripts --frozen-lockfile
- name: Run prettier
run: yarn start prettier
test:
name: Unit Tests
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Use Node.js ${{ matrix.node_version }}
uses: actions/setup-node@v2
with:
node-version: "22"
- name: Prepare env
run: yarn install --ignore-scripts --frozen-lockfile
- name: Run unit tests
run: yarn start test
- name: Run code coverage
uses: codecov/codecov-action@v2.1.0
================================================
FILE: .github/workflows/lock.yml
================================================
name: "Lock Threads"
on:
schedule:
- cron: "0 * * * *"
workflow_dispatch:
permissions:
issues: write
pull-requests: write
concurrency:
group: lock
jobs:
action:
runs-on: ubuntu-latest
steps:
- uses: dessant/lock-threads@v3
with:
issue-inactive-days: "365"
issue-lock-reason: "resolved"
pr-inactive-days: "365"
pr-lock-reason: "resolved"
================================================
FILE: .gitignore
================================================
.vscode
*.iml
.nyc_output
coverage
flow-coverage
node_modules
dist
lib
es
npm-debug.log
.DS_Store
.yalc
yalc.lock
================================================
FILE: .prettierignore
================================================
coverage
dist
node_modules
================================================
FILE: .prettierrc
================================================
{
"trailingComma": "all"
}
================================================
FILE: .travis.yml
================================================
sudo: false
language: node_js
before_install:
- npm install -g npm@6.4.0
cache:
directories:
- node_modules
notifications:
email: false
node_js:
- "10"
- "12"
- "14"
script:
- npm start validate
after_success:
- npx codecov
branches:
only:
- main
================================================
FILE: LICENSE
================================================
MIT License
Copyright (c) 2017 Erik Rasmussen
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
================================================
FILE: MIGRATION_V7.md
================================================
# Migration Guide: react-final-form v6 → v7
## Overview
Version 7.0.0 includes a complete TypeScript rewrite (migrated from Flow). While the runtime behavior remains largely unchanged, there are several TypeScript-specific breaking changes you need to be aware of.
## Breaking Changes
### 1. FormState Properties Now Optional
In v7.0.0, most FormState boolean properties can be `undefined`:
**❌ Before (v6.x):**
```typescript
const { dirty, pristine, valid } = formState;
if (dirty && !pristine) { // Works fine
// ...
}
```
**✅ After (v7.0.0):**
```typescript
const { dirty, pristine, valid } = formState;
if ((dirty ?? false) && !(pristine ?? true)) { // Must handle undefined
// ...
}
```
**Affected properties:**
- `dirty`, `pristine`, `valid`, `invalid`
- `dirtySinceLastSubmit`, `modifiedSinceLastSubmit`
- `submitFailed`, `submitSucceeded`, `submitting`, `validating`
- `hasSubmitErrors`, `hasValidationErrors`
**Note:** `values` is still guaranteed to be defined.
### 2. FieldMetaState Type No Longer Exported
**❌ Before (v6.x):**
```typescript
import { FieldMetaState } from 'react-final-form';
const meta: FieldMetaState = { /* ... */ };
```
**✅ After (v7.0.0):**
```typescript
import { FieldRenderProps } from 'react-final-form';
const meta: FieldRenderProps['meta'] = { /* ... */ };
// Or define it locally:
type FieldMetaState = {
active?: boolean;
data?: Record;
dirty?: boolean;
// ... etc
};
```
### 3. AnyObject Type No Longer Exported
**❌ Before (v6.x):**
```typescript
import { AnyObject } from 'react-final-form';
```
**✅ After (v7.0.0):**
```typescript
// Define locally:
type AnyObject = Record;
```
### 4. UseFieldConfig No Longer Generic
**❌ Before (v6.x):**
```typescript
const config: UseFieldConfig = {
validate: (value) => value ? undefined : 'Required'
};
```
**✅ After (v7.0.0):**
```typescript
const config: UseFieldConfig = {
validate: (value) => value ? undefined : 'Required'
};
```
### 5. FormProps No Longer Accepts Arbitrary Props
In v6.x, you could pass arbitrary props (like `style`, `className`) directly to `
```
**✅ After (v7.0.0):**
```tsx
)}
// Or wrap in a div:
```
## final-form v5.0.0 Changes
If you're also upgrading final-form to v5.0.0, be aware of these changes:
### 1. InternalFormState Requires asyncErrors
**❌ Before (v4.x):**
```typescript
const mockFormState: InternalFormState = {
values: {},
// ...
};
```
**✅ After (v5.0.0):**
```typescript
const mockFormState: InternalFormState = {
values: {},
asyncErrors: {}, // Now required
// ...
};
```
### 2. Mutator Type Signature Changed
**❌ Before (v4.x):**
```typescript
const mutator: Mutator = (args, state, tools) => {
// ...
};
```
**✅ After (v5.0.0):**
```typescript
// If you get type errors with existing mutators:
const mutator = ((args, state, tools) => {
// ...
}) as unknown as Mutator;
```
## Migration Strategy
For a medium to large codebase, expect to modify 100+ files. Here's a recommended approach:
1. **Update dependencies:**
```bash
npm install react-final-form@^7.0.0 final-form@^5.0.0
```
2. **Fix compilation errors in this order:**
- Handle optional boolean properties (use `?? false` or `?? true`)
- Replace `FieldMetaState` imports with `FieldRenderProps['meta']`
- Replace `AnyObject` imports with local type definition
- Remove generic from `UseFieldConfig` → `UseFieldConfig`
- Fix `
```
Now, [let's look at adding some ` `](Field) components!
================================================
FILE: docs/api/FormSpy.md
================================================
# This documentation is meant to be read on [final-form.org](https://final-form.org/docs/react-final-form/api/FormSpy). Links may not work on Github.com.
# ` `
```ts
import { FormSpy } from 'react-final-form'
```
A component that subscribes to form state, and injects both form state and the `form` instance via a render prop.
The ` ` will rerender any time the form state it is subscribed to changes. By default it subscribes to _all_ form state. You can control which form state it subscribes to with the `subscription` prop.
By providing an `onChange` prop, ` ` can also be used to execute code when a particular part of form state changes.
## Props
` ` accepts [`FormSpyProps`](../types/FormSpyProps) and will call the render function with [`FormSpyRenderProps`](../types/FormSpyRenderProps).
The only required prop is one of `onChange`, `component`, `render`, or `children`.
## Basic Usage
It should be noted that ` ` is for very advanced use cases.
**If you are not restricting your form state by providing a `subscription` prop to ` `, you probably do not need ` `!** Just use the form state injected by ` `.
You need to do _one_ of two things when using ` `:
### 1. Provide a way to render the form state
There are three ways to render a ` ` component:
| Prop | Type |
| ---------------------- | --------------------- |
| ` ` | `React.ComponentType` |
| ` ` | `Function` |
| ` ` | `Function` |
The only important distinction is that if you pass a `component` prop, it will be rendered with [`React.createElement()`](https://reactjs.org/docs/react-api.html#createelement), resulting in your component actually being in the React node tree, i.e. inspectable in [DevTools](https://github.com/facebook/react-devtools#react-developer-tools-).
```tsx
// Render a reset button that will be
// disabled when the form is pristine
{props => (
props.form.reset()}
>
Reset
)}
```
### 2. Pass an `onChange` callback
` ` can sometimes be useful to execute code when a particular part of form state changes. This is what the `onChange` callback is for.
**If you pass `onChange`, nothing will be rendered.**
```tsx
{
console.log('Form validity changed to', props.valid)
}}
/>
```
================================================
FILE: docs/api/useField.md
================================================
# This documentation is meant to be read on [final-form.org](https://final-form.org/docs/react-final-form/api/useField). Links may not work on Github.com.
# `useField()`
```ts
import { useField } from 'react-final-form'
```
```ts
(name: string, config: UseFieldConfig) => FieldRenderProps
```
The `useField()` hook takes two parameters:
### `name`
```ts
string
```
**Required**
The name of the field.
### `config`
```ts
UseFieldConfig
```
Optional.
An object that looks just like [`FieldProps`](../types/FieldProps), except without the name.
`useField()` returns [`FieldRenderProps`](../types/FieldRenderProps). It will manage the rerendering of any component you use it in, i.e. the component will only rerender if the field state subscribed to via `useField()` changes.
`useField()` is used internally inside [` `](Field).
================================================
FILE: docs/api/useForm.md
================================================
# This documentation is meant to be read on [final-form.org](https://final-form.org/docs/react-final-form/api/useForm). Links may not work on Github.com.
# `useForm()`
```ts
import { useForm } from 'react-final-form'
```
```ts
() => FormApi
```
The `useForm()` hook plucks the [`FormApi`](/docs/final-form/types/FormApi) out of the React context for you. It will throw an exception if you try to use it outside of a [` `](Form) component.
`useForm()` is used internally inside [`useField()`](useField), [` `](Field), and [` `](FormSpy).
================================================
FILE: docs/api/useFormState.md
================================================
# This documentation is meant to be read on [final-form.org](https://final-form.org/docs/react-final-form/api/useFormState). Links may not work on Github.com.
# `useFormState`
```ts
import { useFormState } from 'react-final-form'
```
The `useFormState()` hook takes one optional parameter, which matches the exact shape of [`FormSpyProps`](../types/FormSpyProps) (except without the render props). It returns a [`FormState`](/docs/final-form/types/FormState).
`useFormState()` is used internally inside [` `](FormSpy).
================================================
FILE: docs/api.md
================================================
# This documentation is meant to be read on [final-form.org](https://final-form.org/docs/react-final-form/api). Links may not work on Github.com.
# API
The API for React Final Form consists of three components and three hooks:
## Components
### [` `](api/Form)
A component that surrounds your entire form and manages the form state. It can inject form state and functionality, e.g. a `handleSubmit` function for you to pass to your `` element, via render props.
### [` `](api/Field)
A component that lives inside your ` ` and creates a "field". It register itself with the surrounding ` ` tag and manages all the state for a particular field, providing input callbacks (e.g. `onBlur`, `onChange`, and `onFocus`) as well as the value of the form and myriad metadata about the state of the field.
### [` `](api/FormSpy)
_[Advanced Usage]_ A component that can tap into form-wide state from inside your ` `. It's primarily only for advanced usage when form renders are being restricted via the `subscription` prop.
## Hooks
### [`useField()`](api/useField)
A hook that will convert any of your components into a ` ` component, registering with the surrounding ` ` and providing field state to your component. `useField()` is used internally by ` `.
### [`useForm()`](api/useForm)
A hook that will pluck the Final Form [`form` instance](/docs/final-form/types/FormApi) out of context.
### [`useFormState()`](api/useFormState)
A hook that will convert any of your components into a ` ` component, allowing fine-grained control over subscribing to parts of the form state.
================================================
FILE: docs/examples/chakra.md
================================================
# This documentation is meant to be read on [final-form.org](https://final-form.org/docs/react-final-form/examples/chakra). Links may not work on Github.com.
# Chakra UI Example
Demonstrates how to use [Chakra UI](https://chakra-ui.com) components with React Final Form.
[](https://codesandbox.io/s/github/final-form/react-final-form/tree/master/examples/chakra)
================================================
FILE: docs/examples/field-level-validation.md
================================================
# This documentation is meant to be read on [final-form.org](https://final-form.org/docs/react-final-form/examples/field-level-validation). Links may not work on Github.com.
# Field Level Validation Example
Introduces field-level validation functions and demonstrates how to display errors next to fields using child render functions.
[](https://codesandbox.io/s/github/final-form/react-final-form/tree/master/examples/field-level-validation)
================================================
FILE: docs/examples/record-level-validation.md
================================================
# This documentation is meant to be read on [final-form.org](https://final-form.org/docs/react-final-form/examples/record-level-validation). Links may not work on Github.com.
# Record-Level Example
Introduces a whole-record validation function and demonstrates how to display errors next to fields using child render functions.
[](https://codesandbox.io/s/github/final-form/react-final-form/tree/master/examples/record-level-validation)
================================================
FILE: docs/examples/simple.md
================================================
# This documentation is meant to be read on [final-form.org](https://final-form.org/docs/react-final-form/examples/simple). Links may not work on Github.com.
# Simple Example
Uses the built-in React inputs: input, select, and textarea to build a form with no validation.
[](https://codesandbox.io/s/github/final-form/react-final-form/tree/master/examples/simple)
================================================
FILE: docs/examples/submission-errors.md
================================================
# This documentation is meant to be read on [final-form.org](https://final-form.org/docs/react-final-form/examples/submission-errors). Links may not work on Github.com.
# Submission Errors
Demonstrates how to return submission errors from failed submits. Notice that the `Promise` should _resolve_ to the submission error (not reject). Rejection is reserved for communications or server exceptions.
[](https://codesandbox.io/s/github/final-form/react-final-form/tree/master/examples/submission-errors)
================================================
FILE: docs/examples/subscriptions.md
================================================
# This documentation is meant to be read on [final-form.org](https://final-form.org/docs/react-final-form/examples/subscriptions). Links may not work on Github.com.
# High Performance Through Subscriptions Example
Demonstrates how, by restricting which parts of form state the form component needs to render, it reduces the number of times the whole form has to rerender. Yet, if some part of form state is needed inside of it, the [` `](../api/FormSpy) component can be used to attain it.
[](https://codesandbox.io/s/github/final-form/react-final-form/tree/master/examples/subscriptions)
================================================
FILE: docs/examples/wizard.md
================================================
# This documentation is meant to be read on [final-form.org](https://final-form.org/docs/react-final-form/examples/wizard). Links may not work on Github.com.
# Wizard Form Example
Demonstrates how to use React Final Form to create a multi-page "wizard" form, with validation on each page.
[](https://codesandbox.io/s/github/final-form/react-final-form/tree/master/examples/wizard)
================================================
FILE: docs/examples.md
================================================
# This documentation is meant to be read on [final-form.org](https://final-form.org/docs/react-final-form/examples). Links may not work on Github.com.
# Examples
---
Wanna help? We need to migrate all of these examples from CodeSandbox to [here](https://github.com/final-form/react-final-form/tree/master/examples). PRs to help with that process would be greatly appreciated. 🙏
---
### [Simple Example](examples/simple)
Uses the built-in React inputs: `input`, `select`, and `textarea` to build a form with no validation.
### [Synchronous Record-Level Validation](examples/record-level-validation)
Introduces a whole-record validation function and demonstrates how to display errors next to fields using child render functions.
### [Synchronous Field-Level Validation](examples/field-level-validation)
Introduces field-level validation functions and demonstrates how to display errors next to fields using child render functions.
### [Synchronous Record-Level Validation (with delayed error render)](https://codesandbox.io/s/z2zqr008pm)
Sometimes you want to give your user a chance to make it through a brief invalid value on their way to a valid one, e.g. a date string that needs two numbers on either side of a slash. With a simple delayed rendering component, this becomes easy. Plus, the error will disappear immediately when the user fixes the problem.
### [Asynchronous Field-Level Validation](https://codesandbox.io/s/wy7z7q5zx5)
Demonstrates how field-level validation rules may be asynchronous (return a
`Promise`), as well as how to show a "validating" spinner during the lifetime of
the `Promise`.
### [Hybrid Synchronous/Asynchronous Record-Level Validation](https://codesandbox.io/s/kl9n295n5)
Demonstrates how you can mix synchronous and asynchronous validation patterns at
the record-level, by returning errors synchronously, and falling back to an
asynchronous call (by returning a `Promise`) if sync validation is passing.
### [Submission Errors](examples/submission-errors)
Demonstrates how to return submission errors from failed submits. Notice that the `Promise` should _resolve_ to the submission error (not reject). Rejection is reserved for communications or server exceptions.
### [Third Party Components](https://codesandbox.io/s/40mr0v2r87)
Demonstrates how easy it is to use third party input components. All the third
party component really needs is `value` and `onChange`, but more complex
components can accept things like errors.
### Material-UI
- [Wrapper components](https://github.com/lookfirst/mui-rff) / [Codesandbox demo](https://codesandbox.io/s/react-final-form-material-ui-example-tqv09)
### 💥 [Performance Optimization Through Subscriptions](examples/subscriptions) 💥
Demonstrates how, by restricting which parts of form state the form component needs to render, it reduces the number of times the whole form has to rerender. Yet, if some part of form state is needed inside of it, the [` `](api/FormSpy) component can be used to attain it.
### [Strongly Typed Form and Field Values with TypeScript](https://codesandbox.io/s/strongly-typed-form-values-with-react-final-form-26jkd)
Demonstrates how to use JSX generics to strongly type fields, forcing only a component that can accept the type for that field.
### [Independent Error Component (with Render Props)](https://codesandbox.io/s/xoo3xq654p)
Demonstrates how to make an independent Error component to subscribe to and
display the error for any form field.
### [Independent Error Component (with Hooks)](https://codesandbox.io/s/react-final-form-independent-error-component-with-hooks-y1grn)
Demonstrates how to make an independent Error component, using Hooks, to subscribe to and
display the error for any form field.
### [Loading and Initializing Values](https://codesandbox.io/s/91w9ro3x9o)
Demonstrates how a form can be initialized, after fetching data, by passing in
`initialValues` as a prop.
### [Field Arrays](https://codesandbox.io/s/kx8qv67nk5)
Demostrates how to use the ` ` component, from
[`react-final-form-arrays`](https://github.com/final-form/react-final-form-arrays),
to render an array of inputs, as well as use `push`, `pop`, and `remove`
mutations.
### [Fields Component](https://codesandbox.io/s/pyrwplknom)
Wondering how to get field state from multiple fields at once?
People coming from Redux-Form might be wondering where the equivalent of Redux Form's `Fields` component is, as a way to get state from several fields at once. The answer is that it's not included in the library because it's so easy to write one recursively composing `Field` components together.
### [Calculated Fields](https://codesandbox.io/s/oq52p6v96y)
Demonstrates how to use the
[`final-form-calculate`](https://github.com/final-form/final-form-calculate)
decorator to achieve realtime field calculations through easily defined rules.
### [Field Warnings](https://codesandbox.io/s/m5qwxpr6o8)
Demonstrates how the power of subscriptions and mutators can be used to build a
warning engine: logic to display a message next to each field that is _not_ an
error (thus it does _not_ prevent form submission).
### [Reusable Field Groups](https://codesandbox.io/s/8z5jm6x80)
Demonstrates how fields can be grouped into reusable components.
### [Prefixed Fields](https://codesandbox.io/s/react-final-form-prefixed-fields-seiy8)
Demonstrates how the React context API can be used to provide a "prefix wrapper"
around fields to add structure to your form date. It's similar to how Redux Form's
[`FormSection`](https://redux-form.com/8.2.2/docs/api/formsection.md/) component works.
Between this and the [Reusable Field Groups](#reusable-field-groups) example, your
use case, if migrating from `FormSection` should be handled.
### [External Submit](https://codesandbox.io/s/1y7noyrlmq)
Demonstrates how you can use `document.getElementById()` or a closure to trigger
a submit from outside of the form. For more information, see
[How can I trigger a submit from outside the form?](https://final-form.org/docs/react-final-form/faq#how-can-i-trigger-a-submit-from-outside-my-form)
### [Wizard Form](examples/wizard)
Demonstrates how to use React Final Form to create a multi-page "wizard" form, with validation on each page.
### [Parse and Format (and Normalize)](https://codesandbox.io/s/10rzowm323)
Demonstrates how to use 🏁 React Final Form's `parse` and `format` props to control exactly how the data flows from the form state through the input and back to the form state. Notice that you can use `parse` to "normalize" your values.
### [Auto-Save with Debounce](https://codesandbox.io/s/5w4yrpyo7k)
Demonstrates how to use a `FormSpy` component to listen for value changes and automatically submit different values after a debounce period.
### [Auto-Save with Selective Debounce](https://codesandbox.io/s/98j0v46zj4)
Demonstrates how to use a `FormSpy` component to listen for value changes and automatically submit different values after a debounce period, but only does the debounce for certain specified fields, in this case, all the text fields.
### [Auto-Save on Field Blur](https://codesandbox.io/s/7k742qpo36)
Demonstrates how to use a `FormSpy` component to listen for values and active field changes to automatically submit values when fields are blurred.
### [Custom Validation Engine](https://codesandbox.io/s/kxxw4l0p9o)
Demonstrates how incredibly extensible `FormSpy`, the [`setFieldData` mutator](https://github.com/final-form/final-form-set-field-data), and render props are by implementing a custom validation engine completely apart from the built-in validation in 🏁 Final Form, thus allowing for special behaviors, like only validating a single field when that field is blurred.
### [Loading, Normalizing, Saving, and Reinitializing](https://codesandbox.io/s/xr0mvl1904)
Demonstrates how to make a wrapper component to handle loading, normalization of data, saving, and reinitializing of the form, to maintain `pristine`/`dirty` state with saved data.
### [🏎️ Downshift Type-Ahead](https://codesandbox.io/s/qzm43nn2mj)
Demonstrates how to use a [🏎️ Downshift](https://github.com/paypal/downshift) type-ahead component as an input.
### [Redux Example](https://codesandbox.io/s/4xq2qpzw79)
The only reason to keep your 🏁 Final Form form data in Redux is if you need to be able to read it from outside your form. This example demonstrates how to use a `FormSpy` to keep a copy of your form data in the Redux store. Note that the canonical authoritative version of the data still lives in 🏁 Final Form. If you need to _mutate_ your data via dispatching Redux actions, you should probably use [Redux Form](https://redux-form.com).
### [Conditional Fields](https://codesandbox.io/s/lm4p3m92q)
Sometimes you might want to conditionally show or hide some parts of your form depending on values the user has already provided for other form inputs. 🏁 React Final Form makes that very easy to do by creating a `Condition` component out of a `Field` component.
### [Listening for External Changes](https://codesandbox.io/s/3x989zl866)
By wrapping a stateful `ExternalModificationDetector` component in a `Field` component, we can listen for changes to a field's value, and by knowing whether or not the field is active, deduce when a field's value changes due to external influences.
### [Focus On First Error](https://codesandbox.io/s/6174kqr403)
Demonstrates how to incorporate the [🏁 Final Form Focus 🧐](https://github.com/final-form/final-form-focus) decorator to provide this functionality out of the box.
### [Credit Card Example](https://codesandbox.io/s/9y8vkrrx9o)
Demonstrates how to make an awesome credit card UX using [React Credit Cards](https://github.com/amarofashion/react-credit-cards).
### [Async Redux Submission](https://codesandbox.io/s/x71mx66z8w)
Want to use `redux-saga` or `redux-observable` to manage your form submissions? Now you can, using [`react-redux-promise-listener`](https://github.com/erikras/react-redux-promise-listener#react-redux-promise-listener) to convert your dispatched Redux actions into the `Promise` that 🏁 React Final Form is expecting for its `onSubmit` function.
### [Declarative Form Rules](https://codesandbox.io/s/52q597j2p)
What if you could define rules to update fields when other fields change _as components_? This example explores such possibilities. There's also [a Medium post](https://medium.com/@erikras/declarative-form-rules-c5949ea97366) about writing it, and creating a companion library, [`react-final-form-listeners`](https://github.com/final-form/react-final-form-listeners#-react-final-form-listeners).
### [Format String By Pattern](https://codesandbox.io/s/no20p7z3l)
Demonstrates how to use the library `format-string-by-pattern` to create input masks for your 🏁 React Final Form fields.
### [AsyncTypeahead and Redux](https://codesandbox.io/s/5m4w2909k)
Demonstrates creating an `AsyncTypeahead` to select github users, while storing the search results in the redux store and the form state (selected github users) via `react-final-form`. Also makes use of the [`setFieldData` mutator](https://github.com/final-form/final-form-set-field-data).
### [Format On Blur](https://codesandbox.io/s/3rp260ly51)
Demonstrates how to use the `formatOnBlur` prop to postpone the formatting of a form field value until the field loses focus. Very useful for formatting numbers, like currencies.
### [Styling with 🍭 Smooth-UI](https://codesandbox.io/s/40o45po3l4)
Demonstrates how to use the Smooth-UI styling library to make your forms look fabulous! All you really need is a higher order component that adapts The 🍭 Smooth-UI form controls to work with 🏁 React Final Form.
### [Styling with Chakra-UI](examples/chakra)
Demonstrates how to use the [Chakra UI](https://chakra-ui.com) styling library to make your forms look fabulous!
### [CLI Example](https://github.com/final-form/rff-cli-example) 🤯
Yes! You can actually use 🏁 React Final Form in a command line interface! Thanks to packages like [Ink](https://github.com/vadimdemedes/ink) and [Pastel](https://github.com/vadimdemedes/pastel), the power of 🏁 Final Form's form state management works just fine on the command line.
================================================
FILE: docs/faq.md
================================================
# This documentation is meant to be read on [final-form.org](https://final-form.org/docs/react-final-form/faq). Links may not work on Github.com.
# FAQ
Below are some frequently asked questions.
## Why not Redux-Form or Formik?
Those are both excellent form libraries. Like all engineering decisions, it depends on your requirements and what trade-offs you wish to make. Both Redux-Form and Formik have considerably larger bundle sizes. Compare [Formik](https://bundlephobia.com/result?p=formik) and [Redux Form](https://bundlephobia.com/result?p=redux-form) to [Final Form](https://bundlephobia.com/result?p=final-form) + [React Final Form](https://bundlephobia.com/result?p=react-final-form).
Redux-Form and React Final Form were both written by [@erikras](https://twitter.com/erikras), who recommends that, unless you _really_ need your form data intimately tied to Redux, you should start any new projects with React Final Form, and try to migrate any older Redux Form projects to it as well.
## Why no HOC?
The only benefit that higher order components provide over render props is access to the injected props from within component lifecycle methods. Plus, it only takes a single line of code to transform a component with a `render` (or `component`) prop into a HOC. If you really want a HOC, you can write your own:
```jsx
import { Form, Field } from 'react-final-form'
class MyForm extends React.Component {
componentDidMount() {
const { initialize } = this.props // access to injected props
ajax.fetch('/myData').then(data => initialize(data))
}
render() {
return ...some fields...
}
}
// 👇 THIS LINE IS THE HOC 👇
export default props =>
```
Doing a HOC
[properly](https://github.com/ReactTraining/react-router/blob/master/packages/react-router/modules/withRouter.js),
as a library should, with hoisted statics and `displayName` and `ref`, etc., is
a hassle and would add unnecessary bulk.
## How can I trigger a submit from outside my form?
This is a common question I see from people migrating from `redux-form`. There
are three possible solutions:
### Via the `form` attribute
You can provide the form id to a submit button.
```jsx
Submit
{/* ^^^^^^^^^^^^^ */}
{/* ^^^^^^^^^^^ */}
...fields go here...
```
### Via `document.getElementById()`
You can use the DOM to get a reference to your `` element and dispatch a
submit event on it. Note that you cannot just call `submit()`, as this will not
trigger React's event handlers.
```jsx
{
document.getElementById('myForm').submit() // ❌
}}>Submit
{
document.getElementById('myForm')
.dispatchEvent(new Event('submit', { cancelable: true, bubbles:true })) // ✅
}}>Submit
...fields go here...
```
See [Sandbox Example](https://codesandbox.io/s/1y7noyrlmq).
### Via Closure
If you define a variable outside of your form, you can then set the value of
that variable to the `handleSubmit` function that 🏁 React Final Form gives you,
and then you can call that function from outside of the form.
```jsx
let submit
return (
Submit // ❌ Not overwritten closure value
submit(event)}>Submit // ✅
{
submit = handleSubmit
return ...fields go here...
}}
/>
)
```
See [Sandbox Example](https://codesandbox.io/s/1y7noyrlmq).
### Via Redux Dead Drop
If you're already using Redux, you could potentially use the same mechanism that
`redux-form` uses:
[Redux Dead Drop](https://medium.com/@erikras/redux-dead-drop-1b9573705bec).
## Why can't I have numeric keys in an object?
So you want to have value structured like `{ 13: 'Bad Luck', 42: 'Meaning of Everything' }`, but you're getting an error. This is because the `setIn` engine in 🏁 Final Form uses the `isNaN`-ness of keys to determine whether or not it should create an object or an array when constructing deep data structures. Adding checks for `bracket[3]` syntax as opposed to `dot.3` syntax adds a _lot_ of complexity, and has consciously been avoided.
You will need to convert all your keys to `NaN` strings before initializing your form values, and then convert them back to numbers on submit. It's not that hard.
```jsx
const stringifyKeys = values =>
Object.keys(values).reduce((result, key) => {
result[`key${key}`] = values[key]
return result
}, {})
const destringifyKeys = values =>
Object.keys(values).reduce((result, key) => {
result[Number(key.substring(3))] = values[key]
return result
}, {})
onSubmit(destringifyKeys(values))}
initialValues={stringifyKeys(initialValues)}>
...
```
## I'm changing form state on first render, e.g. with FormSpy, why are my changes not reflected?
As of `v5`, the only changes that occur during the first render that will cause the form to rerender are if adding field-level validation functions changes the validity of the form. You will need to put your side effect into either a React `useEffect` hook, or just call it in a `setTimeout` to allow the form to finish rendering and setting up its side effects before you go altering the state.
================================================
FILE: docs/getting-started.md
================================================
# This documentation is meant to be read on [final-form.org](https://final-form.org/docs/react-final-form/getting-started). Links may not work on Github.com.
# Getting Started
Before we jump right into code, you might want to learn a little bit about the [philosophy](philosophy) and origin story of React Final Form.
## Installation
```bash
npm install --save final-form react-final-form
```
or
```bash
yarn add final-form react-final-form
```
## Architecture
React Final Form is a thin React wrapper for [Final Form](/), which is a subscriptions-based form state management library that uses the [Observer pattern](https://en.wikipedia.org/wiki/Observer_pattern), so only the components that need updating are re-rendered as the form's state changes.
By default, **React Final Form subscribes to _all_ changes**, but if you want to fine tune your form to optimized blazing-fast perfection, you may specify only the form state that you care about for rendering your gorgeous UI. You can think of it a little like GraphQL's feature of only fetching the data your component needs to render, and nothing else.
## Code
Here's what it looks like in your code:
```jsx
import { Form, Field } from 'react-final-form'
const MyForm = () => (
(
Simple Default Input
First Name
An Arbitrary Reusable Input Component
Interests
Render Function
(
Bio
{meta.touched && meta.error && {meta.error} }
)}
/>
Render Function as Children
{({ input, meta }) => (
Phone
{meta.touched && meta.error && {meta.error} }
)}
Submit
)}
/>
)
```
[Let's explore the API...](api)
================================================
FILE: docs/migration/formik.md
================================================
# This documentation is meant to be read on [final-form.org](https://final-form.org/docs/react-final-form/migration-guide/formik). Links may not work on Github.com.
# Migration from Formik
Good news! Because both React Final Form and Formik are second generation form libraries, they both copied much of their API from [Redux Form](https://redux-form.com), so, despite working very differently under the hood, there is a lot of overlap in their APIs.
## Props passed to ` `
The following props passed to ` ` are more or less identical to those you can pass to [` `](../api/Form):
- [`onSubmit`](../types/FormProps#onsubmit)
- [`initialValues`](../types/FormProps#initialvalues)
- [`validate`](../types/FormProps#validate)
The render props work the same way, except for [what is passed](#props-passed-by-formik).
- [`children`](../types/FormProps#children)
- [`component`](../types/FormProps#component)
- [`render`](../types/FormProps#render)
The only tricky one is:
- [`validateOnBlur`](../types/FormProps#validateonblur)
It's not the same at all. In Formik, `validateOnBlur` defaults to `true` and it allows you to tell Formik _not_ to validate on blur. React Final Form validates on every change by default, and setting `validateOnBlur` to `true` is a way to tell React Final Form to _only_ validate on blur (to _not_ validate on change).
## Props passed by ` `
The following props passed into the render functions by ` ` are identical:
- [`dirty`](/docs/final-form/types/FormState#dirty)
- [`errors`](/docs/final-form/types/FormState#errors)
- [`touched`](/docs/final-form/types/FormState#touched)
- [`handleSubmit`](/docs/final-form/types/FormRenderProps#handlesubmit)
To migrate from Formik, you'd have to search and replace the following:
| Formik | React Final Form |
| ------------------- | ----------------------------------------------------------- |
| `isSubmitting` | [`submitting`](/docs/final-form/types/FormState#submitting) |
| `isValid` | [`valid`](/docs/final-form/types/FormState#valid) |
| `isValidating` | [`validating`](/docs/final-form/types/FormState#validating) |
| `resetForm()` | [`form.reset()`](/docs/final-form/types/FormApi#reset) |
| `submitForm()` | [`form.submit()`](/docs/final-form/types/FormApi#submit) |
| `setFieldTouched()` | [`form.blur()`](/docs/final-form/types/FormApi#blur) |
| `setFieldValue()` | [`form.change()`](/docs/final-form/types/FormApi#change) |
## Props passed to ` `
The APIs of the [` `](../api/Field) components are very similiar. They both have:
- [`name`](../types/FieldProps#name)
- [`children`](../types/FieldProps#children)
- [`component`](../types/FieldProps#component)
- [`render`](../types/FieldProps#render)
- [`validate`](../types/FieldProps#validate)
One difference is that Formik's ` ` will default to `component="input"` if no rendering strategy is provided. React Final Form requires you to specify one of the render strategies.
## Props passed by ` `
The biggest difference is that Formik puts all the `name`, `onChange`, `onBlur`, and `value` into a prop called `field`, React Final Form puts them into a prop called `input`. So:
Formik:
```jsx
}
/>
```
React Final Form:
```jsx
}
// ^^^^^ ^^^^^
/>
```
The other difference is that, while Formik just gives your field component the Formik instance to query for information about field state, React Final Form provides your field state for you in the `meta` prop. Things like [`active`](../types/FieldRenderProps#metaactive) (which Formik doesn't even track), [`dirty`](../types/FieldRenderProps#metadirty), [`pristine`](../types/FieldRenderProps#metapristine), [`valid`](../types/FieldRenderProps#metavalid), [`invalid`](../types/FieldRenderProps#metainvalid), [`touched`](../types/FieldRenderProps#metatouched), [`validating`](../types/FieldRenderProps#metavalidating), and [`visited`](../types/FieldRenderProps#metavisited) (also not in Formik).
## Formik's ` `
React Final Form doesn't provide a similar component, because it just doesn't seem that useful, and it's also trivial to write one yourself.
```jsx
import { useForm } from 'react-final-form'
const FormLikeFormik = props => {
const form = useForm()
return (
{
event.preventDefault()
form.submit()
}}
/>
)
}
```
## Formik's ` `
This component _does_ seem useful, and writing your own is a great introduction to React Final Form's subscription system.
### With ` `
```jsx
import { Field } from 'react-final-form'
const ErrorMessage = ({ name }) => (
{({ meta: { error, touched } }) =>
error && touched ? {error} : null
}
)
```
...or, if you want to look cool...
### With Hooks
```jsx
import { useField } from 'react-final-form'
const ErrorMessage = ({ name }) => {
const {
meta: { error, touched }
} = useField(name, { subscription: { error: true, touched: true } })
return error && touched ? {error} : null
}
```
## Formik's ` `
React Final Form's [` `](../api/Field) has always, from day one, avoided rerenders when parts of the form state change that don't affect the field in question. On top of that, React Final Form's ` ` provides a [`subscription`](../types/FieldProps#subscription) prop that allows you to have even more fine-grain control over _precisely_ which form state will cause your field to rerender.
## Formik's `connect()`
React Final Form has no higher order components, because [it's just not necessary](../faq#why-no-hoc). If you need access to the Final Form instance, you can use the [`useForm()`](../api/useForm) hook.
## A Diff
This is what would need to change to migrate the form on Formik's [Overview](https://jaredpalmer.com/formik/docs/overview#reducing-boilerplate) docs page to React Final Form.
```diff
import React from 'react'
-import { Formik, Form, Field, ErrorMessage } from 'formik'
+import { Form, Field, useField } from 'react-final-form'
+// Obviously this could be reused across your project
+const ErrorMessage = ({ name, component }) => {
+ const {
+ meta: { error, touched }
+ } = useField(name, { subscription: { error: true, touched: true } })
+ return error && touched
+ ? React.createElement(component, null, error)
+ : null
+}
const Basic = () => (
Any place in your app!
- {
let errors = {}
if (!values.email) {
errors.email = 'Required'
} else if (
!/^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}$/i.test(values.email)
) {
errors.email = 'Invalid email address'
}
return errors
}}
- onSubmit={(values, { setSubmitting }) => {
+ onSubmit={(values) => {
+ // final-form manages submitting status
+ // for you *if* you return a promise
+ return new Promise(resolve =>
setTimeout(() => {
alert(JSON.stringify(values, null, 2))
- setSubmitting(false)
+ resolve()
}, 400)
+ )
}}
>
- {({ isSubmitting }) => (
-
+ {({ handleSubmit, submitting }) => (
+
-
+
-
+
-
+
Submit
-
+
)}
-
+
)
export default Basic
```
================================================
FILE: docs/migration/redux-form.md
================================================
# This documentation is meant to be read on [final-form.org](https://final-form.org/docs/react-final-form/migration-guide/redux-form). Links may not work on Github.com.
# Migration from Redux Form
Good news! React Final Form was written by the same guy ([@erikras](https://twitter.com/erikras)) that wrote Redux Form, so much of the API is exactly the same. The primary difference is that, rather than "decorate" your form component with a Higher Order Component, you use React Final Form's [` `](../api/Form) component to give you all your form state via a render prop. Most of the `config` properties from Redux Form maps directly onto the props to [` `](../api/Form), e.g. [`initialValues`](../types/FormProps#initialvalues), [`onSubmit`](../types/FormProps#onsubmit), [`validate`](../types/FormProps#validate), etc.
### Step 1: Change your imports
```diff
import React from 'react'
-import { reduxForm, Field } from 'redux-form'
+import { Form, Field } from 'react-final-form'
```
### Step 2: Surround your ` ` with a ` `
```diff
import React from 'react'
import { Form, Field } from 'react-final-form'
const MyForm = props => {
const { handleSubmit, pristine, reset, submitting } = props
return (
+
+ {() => (
...fields here...
+ )}
+
)
}
```
### Step 3: Move all your `config` values from `reduxForm()` to be props of ` `
```diff
import React from 'react'
import { Form, Field } from 'react-final-form'
const MyForm = props => {
const { handleSubmit, pristine, reset, submitting } = props
return (
{
+ // send values to the cloud
+ }}
+ validate={values => {
+ // do validation here, and return errors object
+ }}
>
{() => (
...fields here...
)}
)
}
export default reduxForm({
form: 'myForm',
- initialValues: {
- firstName: 'Dan'
- },
- onSubmit: values => {
- // send values to the cloud
- },
- validate: values => {
- // do validation here, and return errors object
- }
})(MyForm)
```
### Step 4: Get form state from ` `, not as props
```diff
import React from 'react'
import { Form, Field } from 'react-final-form'
const MyForm = props => {
- const { handleSubmit, pristine, reset, submitting } = props
return (
{
// send values to the cloud
}}
validate={values => {
// do validation here, and return errors object
}}
>
+ {({ handleSubmit, pristine, reset, submitting }) => (
...fields here...
)}
)
}
export default reduxForm({
form: 'myForm'
})(MyForm)
```
### Step 5: Observe API changes
_Some_ of the API is slightly different. For example, rather than providing a `reset` function, the entire [`FormApi`](/docs/final-form/types/FormApi) object is provided, on which `reset()` is a function.
```diff
import React from 'react'
import { Form, Field } from 'react-final-form'
const MyForm = props => {
return (
{
// send values to the cloud
}}
validate={values => {
// do validation here, and return errors object
}}
>
- {({ handleSubmit, pristine, reset, submitting }) => (
+ {({ handleSubmit, pristine, form, submitting }) => (
...fields here...
Submit
Clear Values
)}
)
}
export default reduxForm({
form: 'myForm'
})(MyForm)
```
### Step 6: Remove the HOC
Now you can just export your component. No HOC decorator needed! [Why?](../faq#why-no-hoc)
```diff
-export default reduxForm({
- form: 'myForm'
-})(MyForm)
+export default MyForm
```
### Step 7: Error management
No more `SubmissionError` to throw, error management is made with the return value.
Have a look here to know more: https://final-form.org/docs/react-final-form/types/FormProps#onsubmit
### Step 8: Define your `renderField` functions inline (if you want)
With Redux Form, it was common to have to do something like this:
```jsx
// outside your render() method
const renderField = (field) => (
{field.meta.touched && field.meta.error &&
{field.meta.error} }
)
// inside your render() method
```
With React Final Form, you can define your render logic directly your JSX.
```jsx
// inside your render() method
{field => (
{field.meta.touched && field.meta.error && (
{field.meta.error}
)}
)}
```
It's a little easier to read, but it's less reusable. Large projects are going to create reusable field render functions. If you're migrating a mature project, you can probably continue to use your existing functions/components. [` `](../api/Field) provides the same [`{ input, meta }` shape](../types/FieldRenderProps) that Redux Form does.
### The Full Diff
```diff
import React from 'react'
-import { reduxForm, Field } from 'redux-form',
+import { Form, Field } from 'react-final-form'
const MyForm = props => {
- const { handleSubmit, pristine, reset, submitting } = props
return (
+ {
+ // send values to the cloud
+ }}
+ validate={values => {
+ // do validation here, and return errors object
+ }}
+ >
+ {({ handleSubmit, pristine, form, submitting }) => (
Last Name
Submit
Clear Values
+ )}
+
)
}
-export default reduxForm({
- form: 'myForm',
- initialValues: {
- firstName: 'Dan'
- },
- onSubmit: values => {
- // send values to the cloud
- },
- validate: values => {
- // do validation here, and return errors object
- }
-})(MyForm)
+export default MyForm
```
### ` `
If you're accustomed to being able to get the state of many fields at once using Redux Form's ` `, you may be wondering why this library does not include it. The answer is that _most_ people don't need it, but if you do, you can write it yourself recursively, like this:
```tsx
const Fields = ({
names,
subscription,
fieldsState = {},
children,
originalRender
}) => {
if (!names.length) {
return (originalRender || children)(fieldsState)
}
const [name, ...rest] = names
return (
{fieldState => (
)}
)
}
```
Here's a sandbox demonstrating its usage:
[](https://codesandbox.io/s/pyrwplknom?fontsize=14)
### ` `
React Final Form does not come with field arrays right out of the box. This is because many projects do not need them, and the [philosophy](../philosophy#modularity) of React Final Form is to keep bundle size small, but provide ways to add additional functionality when you need it.
You will need to install two additional packages, `final-form-arrays`, which provides array functionality to the core Final Form instance, and `react-final-form-arrays`, which contains the ` ` component. If you are accustomed to using Redux Form's ` ` component, you already know the API for React Final Form's ` ` component. It injects an "array-like" object called `fields`, which you can `map()` over to get the `string` names for each of the fields in the array. You pass these field names to the ` ` component to render one of the fields in your array. It works just like in Redux Form.
```tsx
import arrayMutators from 'final-form-arrays'
import { FieldArray } from 'react-final-form-arrays'
const MyForm = () => (
{({
handleSubmit,
form: {
mutators: { push, pop } // injected from final-form-arrays above
}
}) => (
... other fields here maybe ...
push('customers', undefined)}>
Add Customer
pop('customers')}>
Remove Customer
{({ fields }) =>
fields.map((name, index) => (
Cust. #{index + 1}
))
}
)}
)
```
Here's a sandbox to demonstrate:
[](https://codesandbox.io/s/distracted-bhaskara-kx8qv67nk5?fontsize=14)
### Cheat Code: The Lazy Way
⚠️ NOT RECOMMENDED ⚠️
There's some chance that you could get away with implementing this function that replicates what the `reduxForm()` decorator does:
```jsx
import { Form } from 'react-final-form'
const reactFinalForm = ({ form, ...config }) => component => props => (
)
```
Then you'd only have to change that one thing.
```diff
-export default reduxForm({
+export default reactFinalForm({
form: 'myForm',
initialValues: {
firstName: 'Dan'
},
onSubmit: values => {
// send values to the cloud
},
validate: values => {
// do validation here, and return errors object
}
})(MyForm)
```
But cheats like this create technical debt, so do the right thing and refactor your forms to use render props. 😄
================================================
FILE: docs/philosophy.md
================================================
# This documentation is meant to be read on [final-form.org](https://final-form.org/docs/react-final-form/philosophy). Links may not work on Github.com.
# Philosophy
For several years, I ([@erikras](https://twitter.com/erikras)) actively maintained the first big form library in the React community, [Redux Form](https://redux-form.com). During those years, I learned many lessons, about open source and React, and saw hundreds of forms use cases from around the world. As Redux Form grew in popularity (and bundle size), I received a lot of feedback from the community. React Final Form is my answer to the concerns of the community.
## Talk
In this talk, I explain the journey through Redux Form to the conception and creation of React Final Form.
[Next Generation Forms with React Final Form – React Alicante 2018, Alicante, Spain](https://youtu.be/WoSzy-4mviQ)
## Goals
React Final Form strives to meet the following goals:
### Strongly Typed
React Final Form provides strong typing via both [Flow](https://flow.org) and [Typescript](https://www.typescriptlang.org) to allow you to catch common bugs _at coding time_.
### Modularity
Just because some forms can be complex doesn't mean that your users should need to download all that code for a simple form! React Final Form and Final Form break out complex functionality into separate packages, so the form state management core doesn't get bloated by complicated use cases. This allows you to _build the form library you need_ for every use case.
Also, this allows for...
### Minimal Bundle Size
React Final Form is a minimal wrapper around the _zero-dependency_ Final Form core. All React Final Form does is know how to get form values out of [`SyntheticEvent`](https://reactjs.org/docs/events.html) and manage field subscriptions to the form.
### High Performance
You probably won't need to fine-tune your form performance, but if your form grows and starts to lag, you'll be glad you've chosen React Final Form. Every bit of form and field state can be chosen _à la carte_ to trigger a rerender in React.
If you're familiar with Redux in React, it's a little bit like how you can use [selectors](https://redux.js.org/recipes/computing-derived-data) to specify exactly which "slice" of state you want your component to be notified about.
The result is that you can streamline your form for maximum performance.
[Ready to get started?](getting-started)
================================================
FILE: docs/types/FieldProps.md
================================================
# This documentation is meant to be read on [final-form.org](https://final-form.org/docs/react-final-form/types/FieldProps). Links may not work on Github.com.
# `FieldProps`
These are props that you pass to [` `](../api/Field). You must provide one of the ways to render: `component`, `render`, or `children`.
## `afterSubmit`
```ts
() => void
```
Optional.
A callback to notify fields after submission has completed successfully.
## `allowNull`
```ts
boolean
```
Optional. Defaults to `false`.
By default, if your value is `null`, [` `](../api/Field) will convert it to `''`, to ensure
[controlled inputs](https://reactjs.org/docs/forms.html#controlled-components).
But if you pass `true` to `allowNull`, [` `](..api/Field) will give you a `null` value.
## `beforeSubmit`
```ts
() => void | false
```
Optional.
A function to call just before calling `onSubmit`. If `beforeSubmit` returns `false`, the submission will be aborted. If one of your fields returns `false` on `beforeSubmit`, other fields may not have their `beforeSubmit` called, as the submission is aborted on the first one that returns `false`.
## `children`
```ts
((props: FieldRenderProps) => React.Node) | React.Node`
```
Optional. (if you specify [`component`](#component) or [`render`](#render))
A render function that is given [`FieldRenderProps`](FieldRenderProps), as well as any non-API props passed into the ` ` component. For example, if you did...
```tsx
{props => {
console.log(props.someArbitraryOtherProp) // would print 42
return
}}
```
Note that if you specify [`render`](#render) or [`component`](#component) _and_ `children`, `render` will be called, with `children` injected as if it were an additional prop. This can be especially useful for doing something like:
```tsx
Red
Green
Blue
```
Related:
- [`FieldRenderProps`](FieldRenderProps)
## `component`
```ts
React.ComponentType | 'input' | 'select' | 'textarea'`
```
Optional. If you are not using `'input'`, `'select`' or `'textarea'`, it is recommended that you use [`children`](#children) or [`render`](#render).
Either the `string` name of one of the default HTML inputs, or a component that is given [`FieldRenderProps`](FieldRenderProps) as props, children and render props, as well as any non-API props passed into the ` ` component. For example, if you did...
```tsx
const MyFieldComp = props => {
console.log(props.someArbitraryOtherProp) // would print 42
return
}
```
Related:
- [`FieldRenderProps`](FieldRenderProps)
## `data`
```ts
Object
```
Optional.
Initial state for arbitrary values to be placed by mutators.
## `defaultValue`
```ts
any
```
Optional.
⚠️ You probably want [`initialValue`](#initialvalue)! ⚠️
The value of the field upon creation. _**This value is only needed if you want your field be `dirty` upon creation (i.e. for its value to be different from its initial value).**_
## `format`
```ts
(value: any, name: string) => any
```
Optional.
A function that takes the value from the form values and the name of the field and formats the value to give to the input. Common use cases include converting javascript `Date` values into a localized date string. Almost always used in conjunction with [`parse`](#parse).
**Note: If you would like to disable the default behavior of converting `undefined` to `''`, you can pass an [identity function](https://en.wikipedia.org/wiki/Identity_function), `v => v`, to `format`. If you do this, making sure your inputs are "controlled" is up to you.**
## `formatOnBlur`
```ts
boolean
```
Optional. Defaults to `false`.
If `true`, the `format` function will only be called when the field is blurred. If `false`, `format` will be called on every render.
## `initialValue`
```ts
any
```
Optional.
The initial value for the field. This value will be used to calculate `dirty` and `pristine` by comparing it to the current value of the field. If you want field to be `dirty` upon creation, you can set one value with `initialValue` and set the value of the field with `defaultValue`.
The value given here will override any `initialValues` given to the entire form.
## `isEqual`
```ts
(a: any, b: any) => boolean
```
Optional. Defaults to `===`.
A function to determine if two values are equal.
## `multiple`
```ts
boolean
```
Optional.
Only of use when using `component="select"` and you want a multiselect.
It will be added on your input component, or you may retrieve its value inside the "input" property of your custom components.
## `name`
```ts
string
```
**Required**
The name of your field. Field values may be deeply nested using dot-and-bracket syntax.
[Learn more about Field Names](/docs/final-form/field-names).
## `parse`
```ts
(value: any, name: string) => any
```
Optional.
A function that takes the value from the input and name of the field and converts the value into the value you want stored as this field's value in the form. Common usecases include converting strings into `Number`s or parsing localized dates into actual javascript `Date` objects. Almost always used in conjuction with [`format`](#format).
**Note: If would like to override the default behavior of converting `''` to `undefined`, you can pass an [identity function](https://en.wikipedia.org/wiki/Identity_function), `v => v`, to `parse`, thus allowing you to have form values of `''`.**
## `ref`
The `ref` is forwarded to the component provided by the `component` prop.
## `render`
```ts
(props: FieldRenderProps) => React.Node
```
Optional. (if you specify [`component`](#component) or [`children`](#children))
A render function that is given [`FieldRenderProps`](FieldRenderProps), as well as any non-API props passed into the ` ` component. For example, if you did...
```tsx
{
console.log(props.someArbitraryOtherProp) // would print 42
return
}}
/>
```
Note that if you specify `render` _and_ [`children`](#children), `render` will be called, with `children` injected as if it were an additional prop.
Related:
- [`FieldRenderProps`](FieldRenderProps)
## `subscription`
```ts
{ [string]: boolean }
```
Optional. _Advanced Usage_
An object of the parts of [`FieldState`](/docs/final-form/types/FieldState) to subscribe to. If a subscription is provided, the [` `](../api/Field) will only rerender when those parts of field state change.
If no `subscription` is provided, it will default to subscribing to _all_ field state changes. i.e. [` `](../api/Field) will rerender whenever any part of the field state changes.
Related:
- [`FieldState`](/docs/final-form/types/FieldState)
## `type`
```ts
string
```
Optional.
If set to `"checkbox"` or `"radio"`, React Final Form will know to manage your values as a checkbox or radio button respectively. Results in a `checked` boolean inside the `input` value given to your render prop.
It will be added on your input component, or you may retrieve its value inside the "input" property of your custom components
## `validate`
```ts
(value: ?any, allValues: Object, meta: ?FieldState) => ?any
```
Optional.
A function that takes the field value, all the values of the form and the `meta` data about the field and returns an error if the value is invalid, or `undefined` if the value is valid.
⚠️ IMPORTANT ⚠️ – By default, in order to allow inline fat-arrow validation functions, the field will not rerender if you change your validation function to an alternate function that has a different behavior. If you need your field to rerender with a new validation function, you will need to update another prop on the `Field`, such as `key`. See the following sandbox for an example:
[](https://codesandbox.io/s/changing-field-level-validators-zc8ei?fontsize=14)
## `validateFields`
```ts
string[]
```
Optional.
An array of field names to validate when this field changes. If `undefined`,
_every_ field will be validated when this one changes; if `[]`, _only this
field_ will have its field-level validation function called when it changes; if
other field names are specified, those fields _and this one_ will be validated
when this field changes.
⚠️ IMPORTANT ⚠️ – By default, in order to allow inline `[]` syntax, the field will not rerender if you change your `validateFields` prop changes. If you need your field to rerender with a new `validateFields` setting, you will need to update another prop on the `Field`, such as `key`.
## `value`
```ts
any
```
Optional.
**This is only used for checkboxes and radio buttons!**
You must also include a `type="radio"` or `type="checkbox"` prop.
### Radio Buttons
The value of the radio button. The radio button will render as `checked` if and only if the value given here `===` the value for the field in the form.
### Checkboxes
#### With `value`
The checkbox will be `checked` if the value given in `value` is contained in the array that is the value for the field for the form. Checking the box will add the value to the array, and unchecking the checkbox will remove the value from the array.
#### Without `value`
The checkbox will be `checked` if the value is truthy. Checking the box will set the value to `true`, and unchecking the checkbox will set the value to `false`.
================================================
FILE: docs/types/FieldRenderProps.md
================================================
# This documentation is meant to be read on [final-form.org](https://final-form.org/docs/react-final-form/types/FieldRenderProps). Links may not work on Github.com.
# `FieldRenderProps`
These are the props that [` `](../api/Field) provides to your render function or component.
This object separates out the values and event handlers intended to be given to the input component from the `meta` data about the field. The `input` can be destructured directly into an ` ` like so: ` `. Keep in mind that **the values in `meta` are dependent on you having subscribed to them** with the [`subscription`](FieldProps#subscription) prop.
## `input.name`
```ts
string
```
**Required**
The name of the field.
## `input.onBlur`
```ts
(?SyntheticFocusEvent<*>) => void`
```
**Required**
The `onBlur` function can take a `SyntheticFocusEvent` like it would if you had given it directly to an ` ` component, but you can also just call it: `props.input.onBlur()` to mark the field as blurred (inactive).
Related:
- [`SyntheticFocusEvent`](https://reactjs.org/docs/events.html#focus-events)
## `input.onChange`
```ts
(SyntheticInputEvent<*> | any) => void
```
**Required**
The `onChange` function can take a `SyntheticInputEvent` like it would if you had given it directly to an ` ` component (in which case it will read the value out of `event.target.value`), but you can also just call it: `props.input.onChange(value)` to update the value of the field.
Related:
- [`SyntheticInputEvent`](https://reactjs.org/docs/events.html#form-events)
## `input.onFocus`
```ts
(?SyntheticFocusEvent<*>) => void
```
**Required**
The `onFocus` function can take a `SyntheticFocusEvent` like it would if you had given it directly to an ` ` component, but you can also just call it: `props.input.onFocus()` to mark the field as focused (active).
Related:
- [`SyntheticFocusEvent`](https://reactjs.org/docs/events.html#focus-events)
## `input.value`
```ts
any
```
Optional. May not be present if you have not [subscribed](FieldProps#subscription) to `value`.
The current value of the field.
## `meta.active`
```ts
boolean
```
Optional: May not be present if you have not [subscribed](FieldProps#subscription) to `active`.
Whether or not the field currently has focus.
## `meta.data`
A place for arbitrary values to be placed by mutators.
## `meta.dirty`
```ts
boolean
```
Optional: May not be present if you have not [subscribed](FieldProps#subscription) to `dirty`.
`true` when the value of the field is not equal to the initial value (using the [`isEqual`](FieldProps#isequal) comparator provided to ` `), `false` if the values are equal.
## `meta.dirtySinceLastSubmit`
```ts
boolean
```
Optional: May not be present if you have not [subscribed](FieldProps#subscription) to `dirtySinceLastSubmit`.
`true` when the value of the field is not equal to the value last submitted (using the [`isEqual`](FieldProps#isequal) comparator provided to ` `), `false` if the values are equal.
## `meta.error`
```ts
any
```
Optional: May not be present if you have not [subscribed](FieldProps#subscription) to `error`.
The current validation error for this field.
## `meta.initial`
```ts
any
```
Optional: May not be present if you have not [subscribed](FieldProps#subscription) to `initial`.
The initial value of the field. `undefined` if it was never initialized.
## `meta.invalid`
```ts
boolean
```
Optional: May not be present if you have not [subscribed](FieldProps#subscription) to `invalid`.
`true` if the field has a validation error or a submission error. `false` otherwise.
## `meta.modified`
```ts
boolean
```
Optional: May not be present if you have not [subscribed](FieldProps#subscription) to `modified`.
`true` if this field's value has ever been changed. `false` otherwise.
Once `true`, it will remain `true` for the lifetime of the field, or until the form is reset.
## `meta.modifiedSinceLastSubmit`
```ts
boolean
```
Optional: May not be present if you have not [subscribed](FieldProps#subscription) to `modifiedSinceLastSubmit`.
`true` if this field's value has ever been changed since last submission. `false` otherwise.
Once `true`, it will remain `true` until the next submit action, or until the form is reset.
## `meta.pristine`
```ts
boolean
```
Optional: May not be present if you have not [subscribed](FieldProps#subscription) to `pristine`.
`true` if the current value is `===` to the initial value, `false` if the values are `!==`.
## `meta.submitError`
```ts
any
```
Optional: May not be present if you have not [subscribed](FieldProps#subscription) to `submitError`.
The submission error for this field.
## `meta.submitFailed`
```ts
boolean
```
Optional: May not be present if you have not [subscribed](FieldProps#subscription) to `submitFailed`.
`true` if a form submission has been tried and failed. `false` otherwise.
## `meta.submitSucceeded`
```ts
boolean
```
Optional: May not be present if you have not [subscribed](FieldProps#subscription) to `submitSucceeded`.
`true` if the form has been successfully submitted. `false` otherwise.
## `meta.submitting`
```ts
boolean
```
Optional: May not be present if you have not [subscribed](FieldProps#subscription) to `submitting`.
`true` if the form is currently being submitted asynchronously. `false` otherwise.
## `meta.touched`
```ts
boolean
```
Optional: May not be present if you have not [subscribed](FieldProps#subscription) to `touched`.
`true` if this field has ever gained and lost focus. `false` otherwise.
Useful for knowing when to display error messages.
## `meta.valid`
```ts
boolean
```
Optional: May not be present if you have not [subscribed](FieldProps#subscription) to `valid`.
`true` if this field has no validation or submission errors. `false` otherwise.
## `meta.validating`
```ts
boolean
```
Optional: May not be present if you have not [subscribed](FieldProps#subscription) to `validating`.
`true` if this field is currently waiting on its asynchronous field-level validation function to resolve. `false` otherwise.
## `meta.visited`
```ts
boolean
```
Optional: May not be present if you have not [subscribed](FieldProps#subscription) to `visited`.
`true` if this field has ever gained focus. `false` otherwise.
================================================
FILE: docs/types/FormProps.md
================================================
# This documentation is meant to be read on [final-form.org](https://final-form.org/docs/react-final-form/types/FormProps). Links may not work on Github.com.
# `FormProps`
These are the props that you pass to [` `](../api/Form). You must provide one of the ways to render: `component`, `render`, or `children`. The rest are mostly just passed along to Final Form's [`Config`](/docs/final-form/types/Config).
## `children`
```ts
((props: FormRenderProps) => React.Node) | React.Node`
```
Optional. (if you specify [`component`](#component) or [`render`](#render))
A render function that is given [`FormRenderProps`](FormRenderProps), as well as any non-API props passed into the ` ` component. For example, if you did...
```tsx
{props => {
console.log(props.someArbitraryOtherProp) // would print 42
return ...
}}
```
Note that if you specify [`render`](#render) _and_ `children`, `render` will be called, with `children` injected as if it were an additional prop.
Related:
- [`FormRenderProps`](FormRenderProps)
## `component`
```ts
React.ComponentType
```
Optional. It is recommended that you use [`children`](#children) or [`render`](#render).
A component that is given [`FormRenderProps`](FormRenderProps) as props, as well as any non-API props passed into the [` `](../api/Form) component. For example, if you did...
```tsx
const MyFormComp = props => {
console.log(props.someArbitraryOtherProp) // would print 42
return ...
}
```
Note that your component will be rendered using [`React.createElement()`](https://reactjs.org/docs/react-api.html#createelement) resulting in your component actually being in the React node tree, i.e. inspectable in [DevTools](https://github.com/facebook/react-devtools#react-developer-tools-).
Related:
- [`FormRenderProps`](FormRenderProps)
## `debug`
```ts
(
state: FormState,
fieldStates: { [string]: FieldState }
) => void
```
Optional.
A callback for debugging that receives the form state and the states of
all the fields. It's called _on every state change_. A typical thing to pass in
might be `console.log`.
Related:
- [`FormState`](/docs/final-form/types/FormState)
- [`FieldState`](/docs/final-form/types/FieldState)
## `decorators`
```ts
Decorator[]
```
Optional.
An array of decorators to apply to the form. [` `](../api/Form) will undecorate the form on unmount.
Related:
- [`Decorator`](/docs/final-form/types/Decorator)
## `form`
```ts
FormApi
```
Optional. _Advanced Usage_
If you'd like to construct your own Final Form `form` instance using [`createForm()`](/docs/final-form/api#createform), you may do so and pass it into [` `](../api/Form) as a prop. Doing so will ignore all the other config props.
Related:
- [`FormApi`](/docs/final-form/types/FormApi)
## `initialValues`
```ts
FormValues | Object
```
Optional.
The initial values of your form. These will also be used to compare against the
current values to calculate `pristine` and `dirty`.
If you are using Typescript, these values must be the same type as the object given to your [`onSubmit`](#onsubmit) function.
## `initialValuesEqual`
```ts
(Object | undefined, Object | undefined) => boolean
```
Optional.
A predicate to determine whether or not the [`initialValues`](#initialvalues) prop has changed, i.e. to know if the form needs to be reinitialized with the new values. Useful for passing in a "deep equals" function if you need to. Defaults to "shallow equals".
## `keepDirtyOnReinitialize`
```ts
boolean
```
Optional.
If `true`, only pristine values will be overwritten when `initialize(newValues)` is called. This can be useful for allowing a user to continue to edit a record while the record is being saved asynchronously, and the form is reinitialized to the saved values when the save is successful. Defaults to `false`.
## `mutators`
```ts
{ [string]: Mutator }
```
Optional.
Named mutator functions.
Related:
- [Mutator](/docs/final-form/types/Mutator)
## `onSubmit`
```ts
(
values: FormValues,
form: FormApi,
callback: ?(errors: ?Object) => void
) => ?Object | Promise | void
```
**Required.**
Function to call when the form is submitted. There are three possible ways to
write an `onSubmit` function:
### 1. Synchronous
Returns `undefined` on success, or an `Object` of submission errors on failure.
### 2. Asynchronous with a callback
Returns `undefined`, calls `callback()` with no arguments on success, or with an `Object` of submission errors on failure.
### 3. Asynchronous with a `Promise`
Returns a `Promise` that resolves with no value on success or _resolves_ with an `Object` of submission errors on failure. The reason it _resolves_ with errors is to leave rejection for when there is a server or communications error.
### Submission Errors
Submission errors must be in the same shape as the values of the form. You may
return a generic error for the whole form (e.g. `'Login Failed'`) using the
special [`FORM_ERROR`](/docs/final-form/api#form_error) string key.
Related:
- [`FormApi`](/docs/final-form/types/FormApi)
## `render`
```ts
(props: FormRenderProps) => React.Node
```
Optional. (if you specify [`component`](#component) or [`children`](#children))
A render function that is given [`FormRenderProps`](FormRenderProps), as well as any non-API props passed into the ` ` component. For example, if you did...
```tsx
{
console.log(props.someArbitraryOtherProp) // would print 42
return ...
}}
/>
```
Note that if you specify `render` _and_ [`children`](#children), `render` will be called, with `children` injected as if it were an additional prop.
Related:
- [`FormRenderProps`](FormRenderProps)
## `subscription`
```ts
{ [string]: boolean }
```
Optional. _Advanced Usage_
An object of the parts of [`FormState`](/docs/final-form/types/FormState) to subscribe to. If a subscription is provided, the [` `](../api/Form) will only rerender when those parts of form state change.
If no `subscription` is provided, it will default to subscribing to _all_ form state changes. i.e. [` `](../api/Form) will rerender whenever any part of the form state changes.
Related:
- [`FormState`](/docs/final-form/types/FormState)
## `validate`
```ts
(values: FormValues) => Object | Promise
```
Optional.
A whole-record validation function that takes all the values of the form and returns any validation errors. There are two possible ways to write a `validate` function:
### 1. Synchronous
Returns `{}` or `undefined` when the values are valid, or an `Object` of validation errors when the values are invalid.
### 2. Asynchronous with a `Promise`
Returns a `Promise` that resolves with no value on success or _resolves_ with an `Object` of validation errors on failure. The reason it _resolves_ with errors is to leave _rejection_ for when there is a server or communications error.
### Validation Errors
Validation errors must be in the same shape as the values of the form. You may return a generic error for the whole form using the special [`FORM_ERROR`](/docs/final-form/api#form_error) string key.
## `validateOnBlur`
```ts
boolean
```
Optional.
If `true`, validation will happen on blur. If `false`, validation will happen on change. Defaults to `false`.
================================================
FILE: docs/types/FormRenderProps.md
================================================
# This documentation is meant to be read on [final-form.org](https://final-form.org/docs/react-final-form/types/FormRenderProps). Links may not work on Github.com.
# `FormRenderProps`
These are the props that [` `](../api/Form) provides to your render function or component. Keep in mind that the values you receive here are dependent upon which values of [`FormState`](/docs/final-form/types/FormState) you have subscribed to with the [`subscription` prop](FormProps#subscription).
This object contains everything in Final Form's [`FormState`](/docs/final-form/types/FormState) as well as:
## `form`
```ts
FormApi
```
The Final Form [`FormApi`](/docs/final-form/types/FormApi).
## `handleSubmit`
```ts
(?SyntheticEvent) => ?Promise
```
A function intended for you to give directly to the `` tag:
```jsx
... fields go here ...
```
The function's return type depends on the way the [`onSubmit` function is written](../types/FormProps#onsubmit).
Related:
- [`SyntheticEvent`](https://reactjs.org/docs/events.html)
================================================
FILE: docs/types/FormSpyProps.md
================================================
# This documentation is meant to be read on [final-form.org](https://final-form.org/docs/react-final-form/types/FormSpyProps). Links may not work on Github.com.
# `FormSpyProps`
These are the props that you pass to [` `](../api/FormSpy). If you do not provide an [`onChange`](#onchange) callback, you must provide one of the ways to render: [`component`](#component), [`render`](#render), or [`children`](#children).
## `children`
```ts
(props: FormRenderProps) => React.Node
```
Optional. (if you specify [`component`](#component) or [`render`](#render) or [`onChange`](#onchange))
A render function that is given [`FormSpyRenderProps`](FormSpyRenderProps), as well as any non-API props passed into the ` ` component. For example, if you did...
```tsx
{props => {
console.log(props.someArbitraryOtherProp) // would print 42
return {JSON.stringify(props.values, undefined, 2)}
}}
```
Note that if you specify [`render`](#render) _and_ `children`, `render` will be called, with `children` injected as if it were an additional prop.
**Will not be called if an [`onChange`](#onchange) callback is specified.**
Related:
- [`FormSpyRenderProps`](FormSpyRenderProps)
## `component`
```ts
React.ComponentType
```
Optional. It is recommended that you use [`children`](#children) or [`render`](#render).
A component that is given [`FormSpyRenderProps`](FormSpyRenderProps) as props, as well as any non-API props passed into the [` `](../api/FormSpy) component. For example, if you did...
```tsx
const MyFormSpyComp = props => {
console.log(props.someArbitraryOtherProp) // would print 42
return {JSON.stringify(props.values, undefined, 2)}
}
```
Note that your component will be rendered using [`React.createElement()`](https://reactjs.org/docs/react-api.html#createelement) resulting in your component actually being in the React node tree, i.e. inspectable in [DevTools](https://github.com/facebook/react-devtools#react-developer-tools-).
**Will not be called if an [`onChange`](#onchange) callback is specified.**
Related:
- [`FormSpyRenderProps`](FormSpyRenderProps)
## `onChange`
```ts
(formState: FormState) => void
```
Optional.
A change listener that will be called with form state whenever the form state, as subscribed to by the [`subscription`](#subscription) prop, has changed.
When an `onChange` prop is provided, the [` `](../api/FormSpy) will not render anything.
## `render`
```ts
(props: FormSpyRenderProps) => React.Node
```
Optional. (if you specify [`component`](#component) or [`children`](#children) or [`onChange`](#onchange))
A render function that is given [`FormSpyRenderProps`](FormSpyRenderProps), as well as any non-API props passed into the ` ` component. For example, if you did...
```tsx
{
console.log(props.someArbitraryOtherProp) // would print 42
return {JSON.stringify(props.values, undefined, 2)}
}}
/>
```
Note that if you specify `render` _and_ [`children`](#children), `render` will be called, with `children` injected as if it were an additional prop.
**Will not be called if an [`onChange`](#onchange) callback is specified.**
Related:
- [`FormSpyRenderProps`](FormSpyRenderProps)
## `subscription`
```ts
{ [string]: boolean }
```
Optional. _Advanced Usage_
An object of the parts of [`FormState`](/docs/final-form/types/FormState) to subscribe to. If a subscription is provided, the [` `](../api/FormSpy) will only rerender when those parts of form state change.
If no `subscription` is provided, it will default to subscribing to _all_ form state changes. i.e. [` `](../api/FormSpy) will rerender whenever any part of the form state changes.
Related:
- [`FormState`](/docs/final-form/types/FormState)
================================================
FILE: docs/types/FormSpyRenderProps.md
================================================
# This documentation is meant to be read on [final-form.org](https://final-form.org/docs/react-final-form/types/FormSpyRenderProps). Links may not work on Github.com.
# `FormSpyRenderProps`
These are the props that [` `](../api/FormSpy) provides to your render function or component. Keep in mind that the values you receive here are dependent upon which values of [`FormState`](/docs/final-form/types/FormState) you have subscribed to with the [`subscription`](FormSpyProps#subscription) prop.
This object contains everything in Final Form's [`FormState`](/docs/final-form/types/FormState) as well as:
## `form`
```ts
FormApi
```
The Final Form [`FormApi`](/docs/final-form/types/FormApi).
================================================
FILE: eslint.config.mjs
================================================
import js from "@eslint/js";
import typescriptParser from "@typescript-eslint/parser";
import typescriptPlugin from "@typescript-eslint/eslint-plugin";
import reactPlugin from "eslint-plugin-react";
import reactHooks from "eslint-plugin-react-hooks";
import jsxA11y from "eslint-plugin-jsx-a11y";
// Manually defined globals to avoid issues with the 'globals' package
const browserGlobals = {
window: "readonly",
document: "readonly",
navigator: "readonly",
console: "readonly",
fetch: "readonly",
setTimeout: "readonly",
HTMLInputElement: "readonly",
HTMLElement: "readonly",
};
const nodeGlobals = {
process: "readonly",
require: "readonly",
module: "readonly",
exports: "writable",
__dirname: "readonly",
__filename: "readonly",
global: "readonly",
console: "readonly",
};
const jestGlobals = {
jest: "readonly",
describe: "readonly",
it: "readonly",
expect: "readonly",
afterEach: "readonly",
beforeEach: "readonly",
test: "readonly",
beforeAll: "readonly",
afterAll: "readonly",
};
export default [
// Base config for all JS/TS files (can be overridden)
{
ignores: [
"node_modules/**",
"dist/**",
"coverage/**",
"*.min.js",
"examples/**",
],
},
js.configs.recommended, // Apply ESLint recommended rules globally (respecting ignores)
// Configuration for TypeScript files in src/
{
files: ["src/**/*.{ts,tsx}"],
ignores: ["**/*.test.ts", "**/*.test.tsx"],
languageOptions: {
parser: typescriptParser,
parserOptions: {
ecmaFeatures: { jsx: true },
ecmaVersion: "latest",
sourceType: "module",
project: "./tsconfig.json", // Project-aware linting for src files
},
globals: {
...browserGlobals,
...nodeGlobals,
...jestGlobals,
es2021: true,
},
},
plugins: {
"@typescript-eslint": typescriptPlugin,
react: reactPlugin,
"react-hooks": reactHooks,
"jsx-a11y": jsxA11y,
},
rules: {
...typescriptPlugin.configs.recommended.rules,
...reactPlugin.configs.recommended.rules,
"react/react-in-jsx-scope": "off",
"react/jsx-uses-react": "off",
"jsx-a11y/href-no-hash": "off",
"react-hooks/rules-of-hooks": "error",
"react-hooks/exhaustive-deps": "warn",
"@typescript-eslint/no-unused-vars": [
"warn",
{ argsIgnorePattern: "^_", varsIgnorePattern: "^_" },
],
"@typescript-eslint/no-explicit-any": "off",
"@typescript-eslint/no-non-null-assertion": "off",
"@typescript-eslint/explicit-module-boundary-types": "off",
"@typescript-eslint/no-unsafe-function-type": "off",
"react/no-children-prop": "off",
"@typescript-eslint/no-unused-expressions": "off",
"no-undef": "off", // TypeScript handles this for .ts/.tsx
},
settings: {
react: { version: "detect" },
},
},
// Configuration for TypeScript test files in typescript/ (for dtslint, no project parsing)
{
files: ["typescript/**/*.ts", "typescript/**/*.tsx"],
languageOptions: {
parser: typescriptParser,
parserOptions: {
ecmaFeatures: { jsx: true }, // Allow JSX in .tsx test files
ecmaVersion: "latest",
sourceType: "module",
},
globals: {
...browserGlobals,
...nodeGlobals,
...jestGlobals,
es2021: true,
}, // General globals for TS test files
},
plugins: {
"@typescript-eslint": typescriptPlugin,
// Add other plugins if relevant for these test files, e.g., react if they use React
},
rules: {
// Lighter ruleset for .d.ts test files or general TS syntax checking
"@typescript-eslint/no-explicit-any": "off",
"@typescript-eslint/no-unused-vars": "off",
"no-unused-vars": "off",
"no-undef": "off", // Disable no-undef for TypeScript files
},
},
// Configuration for JavaScript files (e.g., .js test files in src/, config files)
{
files: ["**/*.js", "**/*.jsx"],
ignores: ["examples/**"],
languageOptions: {
ecmaVersion: "latest",
sourceType: "module",
parserOptions: {
ecmaFeatures: {
jsx: true,
},
},
globals: {
...browserGlobals,
...nodeGlobals,
...jestGlobals,
es2021: true,
},
},
plugins: {
react: reactPlugin,
},
rules: {
"no-undef": "error",
"react/jsx-uses-vars": "warn",
"react/react-in-jsx-scope": "off",
"no-unused-vars": ["warn", { argsIgnorePattern: "^_" }], // Enforce _ prefix for unused vars in JS files
},
},
// Configuration for .mjs files (ES modules in Node.js environment)
{
files: ["**/*.mjs"],
languageOptions: {
ecmaVersion: "latest",
sourceType: "module",
globals: {
...nodeGlobals,
},
},
rules: {
"no-undef": "error",
},
},
// Specific overrides for ALL test files (JS and TS)
{
files: ["**/*.test.js", "**/*.test.jsx", "**/*.test.ts", "**/*.test.tsx"],
rules: {
"no-unused-vars": "off",
"@typescript-eslint/no-unused-vars": "off",
},
},
];
================================================
FILE: examples/async-field-level-validation/Spinner.js
================================================
import styled, { keyframes } from "styled-components";
const rotation = keyframes`
from {
-webkit-transform: rotate(0deg);
}
to {
-webkit-transform: rotate(359deg);
}
`;
export default styled.div`
height: 12px;
width: 12px;
margin-left: 5px;
position: absolute;
right: 0;
top: 0;
animation: ${rotation} 0.6s infinite linear;
border-left: 6px solid rgba(0, 174, 239, 0.15);
border-right: 6px solid rgba(0, 174, 239, 0.15);
border-bottom: 6px solid rgba(0, 174, 239, 0.15);
border-top: 6px solid rgba(0, 174, 239, 0.8);
border-radius: 100%;
`;
================================================
FILE: examples/async-field-level-validation/Styles.js
================================================
import styled, { css } from "styled-components";
const btn = (light, dark) => css`
white-space: nowrap;
display: inline-block;
border-radius: 5px;
padding: 5px 15px;
font-size: 16px;
color: white;
&:visited {
color: white;
}
background-image: linear-gradient(${light}, ${dark});
border: 1px solid ${dark};
&:hover {
background-image: linear-gradient(${light}, ${dark});
&[disabled] {
background-image: linear-gradient(${light}, ${dark});
}
}
&:visited {
color: black;
}
&[disabled] {
opacity: 0.6;
cursor: not-allowed;
}
`;
const btnDefault = css`
${btn("#ffffff", "#d5d5d5")} color: #555;
`;
const btnPrimary = btn("#4f93ce", "#285f8f");
export default styled.div`
font-family: sans-serif;
h1 {
text-align: center;
color: #222;
}
h2 {
text-align: center;
color: #222;
}
& > div {
text-align: center;
}
a {
display: block;
text-align: center;
color: #222;
margin-bottom: 10px;
}
form {
max-width: 500px;
margin: 10px auto;
border: 1px solid #ccc;
padding: 20px;
box-shadow: 2px 2px 5px rgba(0, 0, 0, 0.3);
border-radius: 3px;
position: relative;
& > div {
display: flex;
flex-flow: row nowrap;
line-height: 2em;
margin: 5px;
& > label {
color: #333;
width: 110px;
font-size: 1em;
line-height: 32px;
}
& > input,
& > select,
& > textarea {
flex: 1;
padding: 3px 5px;
font-size: 1em;
margin-left: 15px;
border: 1px solid #ccc;
border-radius: 3px;
}
& > input[type="checkbox"] {
margin-top: 7px;
}
& > div {
margin-left: 16px;
& > label {
display: block;
& > input {
margin-right: 3px;
}
}
}
& > span {
line-height: 32px;
margin-left: 10px;
color: #800;
font-weight: bold;
}
}
& > .buttons {
display: flex;
flex-flow: row nowrap;
justify-content: center;
margin-top: 15px;
}
button {
margin: 0 10px;
&[type="submit"] {
${btnPrimary};
}
&[type="button"] {
${btnDefault};
}
}
pre {
border: 1px solid #ccc;
background: rgba(0, 0, 0, 0.1);
box-shadow: inset 1px 1px 3px rgba(0, 0, 0, 0.2);
padding: 20px;
}
}
`;
================================================
FILE: examples/async-field-level-validation/index.js
================================================
import React from "react";
import { render } from "react-dom";
import Styles from "./Styles";
import Spinner from "./Spinner";
import { Form, Field } from "react-final-form";
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
const onSubmit = async (values) => {
await sleep(300);
window.alert(JSON.stringify(values, undefined, 2));
};
const required = (value) => (value ? undefined : "Required");
const mustBeNumber = (value) => (isNaN(value) ? "Must be a number" : undefined);
const minValue = (min) => (value) =>
isNaN(value) || value >= min ? undefined : `Should be greater than ${min}`;
const composeValidators =
(...validators) =>
(value) =>
validators.reduce(
(error, validator) => error || validator(value),
undefined,
);
const simpleMemoize = (fn) => {
let lastArg;
let lastResult;
return (arg) => {
if (arg !== lastArg) {
lastArg = arg;
lastResult = fn(arg);
}
return lastResult;
};
};
const usernameAvailable = simpleMemoize(async (value) => {
if (!value) {
return "Required";
}
await sleep(400);
if (
~["john", "paul", "george", "ringo"].indexOf(value && value.toLowerCase())
) {
return "Username taken!";
}
});
const App = () => (
🏁
{" "}
React Final Form Example
Asynchronous Field-Level Validation
Read Docs
Usernames John, Paul, George or Ringo will fail async validation.
(
{({ input, meta }) => (
Username
{meta.error && meta.touched && {meta.error} }
{meta.validating && }
)}
{({ input, meta }) => (
Last Name
{meta.error && meta.touched && {meta.error} }
)}
{({ input, meta }) => (
Age
{meta.error && meta.touched && {meta.error} }
)}
Submit
Reset
{JSON.stringify(values, 0, 2)}
)}
/>
);
render( , document.getElementById("root"));
================================================
FILE: examples/async-field-level-validation/package.json
================================================
{
"name": "react-final-form-asynchronous-field-level-validation-example",
"version": "1.0.0",
"description": "This example demonstrates how field-level validation functions may be asynchronous.",
"keywords": [
"react-final-form",
"final-form",
"react",
"validation",
"asynchronous"
],
"main": "index.js",
"dependencies": {
"final-form": "4.20.4",
"react": "17.0.2",
"react-dom": "17.0.2",
"react-final-form": "6.5.3",
"styled-components": "4.2.0"
}
}
================================================
FILE: examples/async-field-level-validation/readme.md
================================================
# Asynchronous Field-Level Validation
[](https://codesandbox.io/s/wy7z7q5zx5)
================================================
FILE: examples/async-redux-submission/Styles.js
================================================
import styled, { css } from "styled-components";
const btn = (light, dark) => css`
white-space: nowrap;
display: inline-block;
border-radius: 5px;
padding: 5px 15px;
font-size: 16px;
color: white;
&:visited {
color: white;
}
background-image: linear-gradient(${light}, ${dark});
border: 1px solid ${dark};
&:hover {
background-image: linear-gradient(${light}, ${dark});
&[disabled] {
background-image: linear-gradient(${light}, ${dark});
}
}
&:visited {
color: black;
}
&[disabled] {
opacity: 0.6;
cursor: not-allowed;
}
`;
const btnDefault = css`
${btn("#ffffff", "#d5d5d5")} color: #555;
`;
const btnPrimary = btn("#4f93ce", "#285f8f");
const btnDanger = btn("#e27c79", "#c9302c");
export default styled.div`
font-family: sans-serif;
h1 {
text-align: center;
color: #222;
}
h2 {
text-align: center;
color: #222;
}
& > div {
text-align: center;
}
a {
display: block;
text-align: center;
color: #222;
margin-bottom: 10px;
}
p {
max-width: 500px;
margin: 10px auto;
& > a {
display: inline;
}
}
.loading {
font-size: 2em;
font-weight: bold;
text-align: center;
margin: 50px;
}
form,
div.form {
text-align: left;
max-width: 500px;
margin: 10px auto;
border: 1px solid #ccc;
padding: 20px;
box-shadow: 2px 2px 5px rgba(0, 0, 0, 0.3);
border-radius: 3px;
position: relative;
& > div {
display: flex;
flex-flow: row nowrap;
line-height: 2em;
position: relative;
padding: 8px 5px;
border: 1px solid transparent;
&.active {
background-color: paleturquoise;
border-color: turquoise;
}
& > label {
color: #333;
width: 110px;
min-width: 60px;
font-size: 1em;
line-height: 32px;
}
& > input,
& > .downshift > input,
& > select,
& > textarea {
flex: 1;
padding: 6px 9px;
font-size: 1em;
margin-left: 15px;
border: 1px solid #ccc;
border-radius: 3px;
&[disabled] {
background: #eee;
}
}
& > input[type="checkbox"] {
margin-top: 7px;
}
& > div {
margin-left: 16px;
& > label {
margin-left: 0;
display: block;
& > input {
margin-right: 3px;
}
}
&.downshift {
margin-left: 0;
padding-left: 15px;
flex: 1;
& > input {
width: 100%;
padding: 6px 5px;
font-size: 1em;
margin-left: 0;
border: 1px solid #ccc;
border-radius: 3px;
}
}
}
& > span {
line-height: 32px;
margin-left: 10px;
color: #800;
font-weight: bold;
}
& > button.remove {
${btnDanger};
}
}
& > .buttons {
display: flex;
flex-flow: row nowrap;
justify-content: center;
margin-top: 15px;
}
.error {
display: flex;
font-weight: bold;
color: #800;
flex-flow: row nowrap;
justify-content: center;
}
pre {
position: relative;
border: 1px solid #ccc;
background: rgba(0, 0, 0, 0.1);
box-shadow: inset 1px 1px 3px rgba(0, 0, 0, 0.2);
padding: 20px;
}
.submitting {
display: block;
position: absolute;
top: -5px;
left: -5px;
right: -5px;
padding: 0;
text-align: center;
background: rgba(0, 0, 0, 0.4);
color: white;
z-index: 10;
font-weight: bold;
font-size: 0.8em;
}
.saving {
font-size: 0.8em;
font-weight: bold;
color: darkblue;
margin: 8px 0 0 7px;
}
}
button {
margin: 0 10px;
&[type="submit"] {
${btnPrimary};
}
&[type="button"] {
${btnDefault};
}
}
.downshift-options {
border: 1px solid #ddd;
box-shadow: 1px 1px 2px rgba(0, 0, 0, 0.3);
& > div {
padding: 3px 5px;
}
}
`;
================================================
FILE: examples/async-redux-submission/asyncSubmissionMiddleware.js
================================================
import { REGISTER, REGISTER_SUCCESS } from "./registrationDuck";
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
const submit = async (values) => {
await sleep(200);
if (values.firstName === "John") {
throw Error({ firstName: "No John's Allowed!" });
}
window.alert(JSON.stringify(values, 0, 2));
};
/** This is to mimic the behavior of one of the various Redux async middlewares */
const asyncSubmissionMiddleware = (store) => (next) => (action) => {
if (action && action.type === REGISTER) {
submit(action.payload).then(
() => store.dispatch({ type: REGISTER_SUCCESS }),
(errors) => {
// NOTE!! We are passing REGISTER_SUCCESS here because 🏁 Final Form expects
// submit errors to come back in a *resolved* promise.
store.dispatch({ type: REGISTER_SUCCESS, payload: errors });
},
);
}
return next(action);
};
export default asyncSubmissionMiddleware;
================================================
FILE: examples/async-redux-submission/index.js
================================================
import React from "react";
import { render } from "react-dom";
import { Provider } from "react-redux";
import Styles from "./Styles";
import { Form, Field } from "react-final-form";
import store, { promiseListener } from "./store";
import {
REGISTER,
REGISTER_SUCCESS,
REGISTER_FAILURE,
} from "./registrationDuck";
import MakeAsyncFunction from "react-redux-promise-listener";
const SubmitError = ({ name }) => (
{({ meta: { submitError, dirtySinceLastSubmit } }) =>
submitError && !dirtySinceLastSubmit ? {submitError} : null
}
);
const App = () => (
🏁
{" "}
React Final Form
Async Redux Submission
Read Docs
Demonstrates how to use an async Redux side-effects library, like{" "}
redux-saga to manage form submissions using{" "}
🏁
{" "}
React Final Form. Uses{" "}
redux-promise-listener
{" "}
and{" "}
react-redux-promise-listener
{" "}
libraries.
Dispatched Redux actions can be found in the console.
First name John will fail submit validation.
{(onSubmit) => (
(
First Name
Last Name
Email
Favorite Color
❤️
{" "}
Red
💚
{" "}
Green
💙
{" "}
Blue
Submit
Reset
{JSON.stringify(values, 0, 2)}
)}
/>
)}
);
render( , document.getElementById("root"));
================================================
FILE: examples/async-redux-submission/package.json
================================================
{
"name": "react-final-form-async-redux-submission",
"version": "1.0.0",
"description": "Demonstrates how to use react-redux-promise-listener to construct a promise out of Redux actions to give to 🏁 React Final Form's onSubmit.",
"keywords": [
"form",
"redux",
"async",
"submit",
"promise"
],
"homepage": "https://codesandbox.io/s/new",
"main": "src/index.js",
"dependencies": {
"final-form": "4.20.4",
"react": "17.0.2",
"react-dom": "17.0.2",
"react-final-form": "6.5.3",
"react-redux": "7.0.3",
"react-redux-promise-listener": "1.0.0",
"react-scripts": "3.0.1",
"redux": "4.0.1",
"redux-promise-listener": "1.1.1",
"styled-components": "4.2.0"
},
"devDependencies": {},
"scripts": {
"start": "react-scripts start",
"build": "react-scripts build",
"test": "react-scripts test --env=jsdom",
"eject": "react-scripts eject"
}
}
================================================
FILE: examples/async-redux-submission/readme.md
================================================
# Async Redux Submission
[](https://codesandbox.io/s/x71mx66z8w)
================================================
FILE: examples/async-redux-submission/registrationDuck.js
================================================
// QUACK! This is a duck. https://github.com/erikras/ducks-modular-redux
// Actions
export const REGISTER = "final-form-examples/registration/REGISTER";
export const REGISTER_SUCCESS =
"final-form-examples/registration/REGISTER_SUCCESS";
export const REGISTER_FAILURE =
"final-form-examples/registration/REGISTER_FAILURE";
// Reducer
export default function reducer(state = {}, action = {}) {
switch (action.type) {
case REGISTER:
return {
...state,
registering: true,
};
case REGISTER_SUCCESS:
return {
...state,
registering: false,
};
case REGISTER_FAILURE:
return {
...state,
registering: false,
};
default:
return state;
}
}
// Action Creators
// Selectors
================================================
FILE: examples/async-redux-submission/store.js
================================================
import { createStore, combineReducers, applyMiddleware, compose } from "redux";
import createReduxPromiseListener from "redux-promise-listener";
import registration from "./registrationDuck";
import asyncSubmissionMiddleware from "./asyncSubmissionMiddleware";
const reduxPromiseListener = createReduxPromiseListener();
const logger = (store) => (next) => (action) => {
console.log(action);
return next(action);
};
const reducer = combineReducers({
registration,
});
const composeEnhancers =
(typeof window !== "undefined" &&
window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__) ||
compose;
const store = createStore(
reducer,
{},
composeEnhancers(
applyMiddleware(
reduxPromiseListener.middleware,
asyncSubmissionMiddleware,
logger,
),
),
);
export const promiseListener = reduxPromiseListener; // <---------- IMPORTANT
export default store;
================================================
FILE: examples/async-typeahead-redux/GithubUserTypeahead.jsx
================================================
import React, { useCallback } from "react";
import { useSelector, useDispatch } from "react-redux";
import { Field } from "react-final-form";
import { AsyncTypeahead } from "react-bootstrap-typeahead";
import useKeyword from "./useKeyword";
import { compose, propOr, pathOr } from "ramda";
import arrify from "arrify";
import { searchGithubUsers } from "./actions";
const AdaptedTypeahead = ({ input, render, meta, ...rest }) => (
);
const GithubUserTypeahead = ({ name, ...props }) => {
const { keyword, updateKeyword } = useKeyword(name);
const dispatch = useDispatch();
const getOptions = useCallback(pathOr([], [keyword, "value"]), [keyword]);
const isLoading = useCallback(pathOr(false, [keyword, "loading"]), [keyword]);
const handleOnSearch = useCallback(compose(dispatch, searchGithubUsers), [
dispatch,
]);
const options = useSelector(getOptions);
const loading = useSelector(isLoading);
return (
);
};
export default GithubUserTypeahead;
================================================
FILE: examples/async-typeahead-redux/Styles.js
================================================
import styled, { css } from "styled-components";
const btn = (light, dark) => css`
white-space: nowrap;
display: inline-block;
border-radius: 5px;
padding: 5px 15px;
font-size: 16px;
color: white;
&:visited {
color: white;
}
background-image: linear-gradient(${light}, ${dark});
border: 1px solid ${dark};
&:hover {
background-image: linear-gradient(${light}, ${dark});
&[disabled] {
background-image: linear-gradient(${light}, ${dark});
}
}
&:visited {
color: black;
}
&[disabled] {
opacity: 0.6;
cursor: not-allowed;
}
`;
const btnDefault = css`
${btn("#ffffff", "#d5d5d5")} color: #555;
`;
const btnPrimary = btn("#4f93ce", "#285f8f");
const btnDanger = btn("#e27c79", "#c9302c");
export default styled.div`
font-family: sans-serif;
h1 {
text-align: center;
color: #222;
}
h2 {
text-align: center;
color: #222;
}
& > div {
text-align: center;
}
a {
display: block;
text-align: center;
color: #222;
margin-bottom: 10px;
}
p {
max-width: 500px;
margin: 10px auto;
& > a {
display: inline;
}
}
form,
div.form {
text-align: left;
max-width: 500px;
margin: 10px auto;
border: 1px solid #ccc;
padding: 20px;
box-shadow: 2px 2px 5px rgba(0, 0, 0, 0.3);
border-radius: 3px;
position: relative;
.loading {
text-align: center;
display: block;
position: absolute;
background: url("https://media.giphy.com/media/130AxGoOaR6t0I/giphy.gif")
center center;
background-size: fill;
font-size: 2em;
top: 0;
left: 0;
right: 0;
bottom: 0;
padding: 50px 0 0 0;
z-index: 2;
}
& > div {
display: flex;
flex-flow: row nowrap;
line-height: 2em;
margin: 5px;
position: relative;
& > label {
color: #333;
width: 110px;
min-width: 60px;
font-size: 1em;
line-height: 32px;
}
& > input,
& > select,
& > textarea {
flex: 1;
padding: 3px 5px;
font-size: 1em;
margin-left: 15px;
border: 1px solid #ccc;
border-radius: 3px;
}
& > input[type="checkbox"] {
margin-top: 7px;
}
& > div {
margin-left: 16px;
& > label {
display: block;
& > input {
margin-right: 3px;
}
}
}
& > span {
line-height: 32px;
margin-left: 10px;
color: #800;
font-weight: bold;
}
& > button.remove {
${btnDanger};
}
}
& > .buttons {
display: flex;
flex-flow: row nowrap;
justify-content: center;
margin-top: 15px;
}
.error {
display: flex;
font-weight: bold;
color: #800;
flex-flow: row nowrap;
justify-content: center;
}
pre {
position: relative;
border: 1px solid #ccc;
background: rgba(0, 0, 0, 0.1);
box-shadow: inset 1px 1px 3px rgba(0, 0, 0, 0.2);
padding: 20px;
}
.submitting {
display: block;
position: absolute;
top: -5px;
left: -5px;
right: -5px;
padding: 0;
text-align: center;
background: rgba(0, 0, 0, 0.4);
color: white;
z-index: 10;
font-weight: bold;
font-size: 0.8em;
}
.saving {
font-size: 0.8em;
font-weight: bold;
color: darkblue;
margin: 8px 0 0 7px;
}
}
button {
margin: 0 10px;
&[type="submit"] {
${btnPrimary};
}
&[type="button"] {
${btnDefault};
}
}
`;
================================================
FILE: examples/async-typeahead-redux/actions.js
================================================
export const requestGithubUsers = (query) => ({
type: "GITHUB_USERS_REQUEST",
query,
});
export const storeGithubUsers = (query, users) => ({
type: "GITHUB_USERS_RESPONSE",
query,
users,
});
export const searchGithubUsers = (query) => (dispatch) => {
dispatch(requestGithubUsers(query));
fetch(`https://api.github.com/search/users?q=${query}`)
.then((res) => res.json())
.then(({ items: users }) => dispatch(storeGithubUsers(query, users)));
};
================================================
FILE: examples/async-typeahead-redux/index.js
================================================
import React from "react";
import { render } from "react-dom";
import { Provider } from "react-redux";
import Styles from "./Styles";
import { Form } from "react-final-form";
import setFieldData from "final-form-set-field-data";
import configureStore from "./store";
import GithubUserTypeahead from "./GithubUserTypeahead";
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
const onSubmit = async (values) => {
await sleep(300);
window.alert(JSON.stringify(values, 0, 2));
};
const store = configureStore();
const App = () => (
🏁
{" "}
React Final Form
Read Docs
(
Github users
Github users 2
Submit
{JSON.stringify(values, 0, 2)}
)}
/>
);
render(
,
document.getElementById("root"),
);
================================================
FILE: examples/async-typeahead-redux/package.json
================================================
{
"name": "react-final-form-github-user-asynctypeahead",
"version": "1.0.0",
"description": "Demonstrates how extensible 🏁 React Final Form can be with the combination of FormSpy and setFieldData by creating a totally separate validation engine that runs when a field is blurred.",
"keywords": [],
"main": "index.js",
"dependencies": {
"arrify": "2.0.1",
"final-form": "4.20.4",
"final-form-set-field-data": "1.0.2",
"ramda": "0.25.0",
"react": "17.0.2",
"react-bootstrap-typeahead": "3.1.4",
"react-dom": "17.0.2",
"react-final-form": "6.5.3",
"react-redux": "7.1.0",
"redux": "4.0.4",
"redux-thunk": "2.3.0",
"styled-components": "2.4.0"
}
}
================================================
FILE: examples/async-typeahead-redux/readme.md
================================================
# Async Typeahead and Redux
[](https://codesandbox.io/s/5m4w2909k)
================================================
FILE: examples/async-typeahead-redux/store.js
================================================
import { createStore, applyMiddleware } from "redux";
import thunk from "redux-thunk";
const initialState = {};
const reducer = (state = initialState, action) => {
switch (action.type) {
case "GITHUB_USERS_REQUEST":
return {
...state,
[action.query]: {
...state[action.query],
loading: true,
},
};
case "GITHUB_USERS_RESPONSE":
return {
...state,
[action.query]: {
value: action.users,
loading: false,
},
};
default:
return state;
}
};
export default () => createStore(reducer, undefined, applyMiddleware(thunk));
================================================
FILE: examples/async-typeahead-redux/useKeyword.js
================================================
import { useForm, useField } from "react-final-form";
import { propOr } from "ramda";
const useKeyword = (name) => {
const {
mutators: { setFieldData },
} = useForm();
const { meta } = useField(name, { subscription: { data: true } });
return {
keyword: propOr(null, "keyword", meta.data),
updateKeyword: (keyword) => setFieldData(name, { keyword }),
};
};
export default useKeyword;
================================================
FILE: examples/auto-save-field-blur/AutoSave.js
================================================
import React from "react";
import { FormSpy } from "react-final-form";
import diff from "object-diff";
class AutoSave extends React.Component {
constructor(props) {
super(props);
this.state = { values: props.values, submitting: false };
}
componentWillReceiveProps(nextProps) {
if (this.props.active && this.props.active !== nextProps.active) {
// blur occurred
this.save(this.props.active);
}
}
save = async (blurredField) => {
if (this.promise) {
await this.promise;
}
const { values, setFieldData, save } = this.props;
// This diff step is totally optional
const difference = diff(this.state.values, values);
if (Object.keys(difference).length) {
// values have changed
this.setState({ submitting: true, values });
setFieldData(blurredField, { saving: true });
this.promise = save(difference);
await this.promise;
delete this.promise;
this.setState({ submitting: false });
setFieldData(blurredField, { saving: false });
}
};
render() {
// This component doesn't have to render anything, but it can render
// submitting state.
return null;
}
}
// Make a HOC
// This is not the only way to accomplish auto-save, but it does let us:
// - Use built-in React lifecycle methods to listen for changes
// - Maintain state of when we are submitting
// - Render a message when submitting
// - Pass in save prop nicely
export default (props) => (
);
================================================
FILE: examples/auto-save-field-blur/Styles.js
================================================
import styled, { css } from "styled-components";
const btn = (light, dark) => css`
white-space: nowrap;
display: inline-block;
border-radius: 5px;
padding: 5px 15px;
font-size: 16px;
color: white;
&:visited {
color: white;
}
background-image: linear-gradient(${light}, ${dark});
border: 1px solid ${dark};
&:hover {
background-image: linear-gradient(${light}, ${dark});
&[disabled] {
background-image: linear-gradient(${light}, ${dark});
}
}
&:visited {
color: black;
}
&[disabled] {
opacity: 0.6;
cursor: not-allowed;
}
`;
const btnDefault = css`
${btn("#ffffff", "#d5d5d5")} color: #555;
`;
const btnPrimary = btn("#4f93ce", "#285f8f");
const btnDanger = btn("#e27c79", "#c9302c");
export default styled.div`
font-family: sans-serif;
h1 {
text-align: center;
color: #222;
}
h2 {
text-align: center;
color: #222;
}
& > div {
text-align: center;
}
a {
display: block;
text-align: center;
color: #222;
margin-bottom: 10px;
}
p {
max-width: 500px;
margin: 10px auto;
& > a {
display: inline;
}
}
form,
div.form {
text-align: left;
max-width: 500px;
margin: 10px auto;
border: 1px solid #ccc;
padding: 20px;
box-shadow: 2px 2px 5px rgba(0, 0, 0, 0.3);
border-radius: 3px;
position: relative;
.loading {
text-align: center;
display: block;
position: absolute;
background: url("https://media.giphy.com/media/130AxGoOaR6t0I/giphy.gif")
center center;
background-size: fill;
font-size: 2em;
top: 0;
left: 0;
right: 0;
bottom: 0;
padding: 50px 0 0 0;
z-index: 2;
}
& > div {
display: flex;
flex-flow: row nowrap;
line-height: 2em;
margin: 5px;
position: relative;
& > label {
color: #333;
width: 110px;
min-width: 60px;
font-size: 1em;
line-height: 32px;
}
& > input,
& > select,
& > textarea {
flex: 1;
padding: 3px 5px;
font-size: 1em;
margin-left: 15px;
border: 1px solid #ccc;
border-radius: 3px;
}
& > input[type="checkbox"] {
margin-top: 7px;
}
& > div {
margin-left: 16px;
& > label {
display: block;
& > input {
margin-right: 3px;
}
}
}
& > span {
line-height: 32px;
margin-left: 10px;
color: #800;
font-weight: bold;
}
& > button.remove {
${btnDanger};
}
}
& > .buttons {
display: flex;
flex-flow: row nowrap;
justify-content: center;
margin-top: 15px;
}
.error {
display: flex;
font-weight: bold;
color: #800;
flex-flow: row nowrap;
justify-content: center;
}
pre {
position: relative;
border: 1px solid #ccc;
background: rgba(0, 0, 0, 0.1);
box-shadow: inset 1px 1px 3px rgba(0, 0, 0, 0.2);
padding: 20px;
}
.submitting {
display: block;
position: absolute;
top: -5px;
left: -5px;
right: -5px;
padding: 0;
text-align: center;
background: rgba(0, 0, 0, 0.4);
color: white;
z-index: 10;
font-weight: bold;
font-size: 0.8em;
}
.saving {
font-size: 0.8em;
font-weight: bold;
color: darkblue;
margin: 0 0 0 8px;
}
}
button {
margin: 0 10px;
&[type="submit"] {
${btnPrimary};
}
&[type="button"] {
${btnDefault};
}
}
`;
================================================
FILE: examples/auto-save-field-blur/index.js
================================================
import React from "react";
import { render } from "react-dom";
import Styles from "./Styles";
import { Form, Field } from "react-final-form";
import setFieldData from "final-form-set-field-data";
import AutoSave from "./AutoSave";
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
const save = async (values) => {
console.log("Saving", values);
await sleep(1000);
};
const SavingIndicator = ({ name }) => (
(saving ? Saving
: null)}
/>
);
const App = () => (
🏁
{" "}
React Final Form
Auto-Save on Field Blur
Read Docs
The AutoSave component uses{" "}
FormSpy
{" "}
to listen to changes to values and which field is currently active and
auto-saves changes when a field is blurred. Look in the console for the
save events.
{({ form }) => (
{/* Don't even need a
tag */}
{/* 👇 👇 👇 👇 */}
{/* ☝️ ️☝️ ️☝️ ️️️️☝️ ️️*/}
First Name
Last Name
Email
Favorite Color
❤️
{" "}
Red
💚
{" "}
Green
💙
{" "}
Blue
Employed?
Toppings
🐷
{" "}
Ham
🍄
{" "}
Mushrooms
🧀
{" "}
Cheese
🐓
{" "}
Chicken
🍍
{" "}
Pinapple
Best Stooge?
{" "}
Larry
{" "}
Moe
{" "}
Curly
Notes
)}
);
render( , document.getElementById("root"));
================================================
FILE: examples/auto-save-field-blur/package.json
================================================
{
"name": "react-final-form-auto-save-on-field-blur",
"version": "1.0.0",
"description": "Demonstrates how to create an AutoSave component to listen to form state and automatically save data when fields blur.",
"keywords": [],
"main": "index.js",
"dependencies": {
"final-form": "4.20.4",
"final-form-set-field-data": "1.0.2",
"object-diff": "0.0.4",
"react": "17.0.2",
"react-dom": "17.0.2",
"react-final-form": "6.5.3",
"styled-components": "4.2.0"
}
}
================================================
FILE: examples/auto-save-field-blur/readme.md
================================================
# Auto-Save on Field Blur
[](https://codesandbox.io/s/7k742qpo36)
================================================
FILE: examples/auto-save-selective-debounce/AutoSave.js
================================================
import React from "react";
import { FormSpy } from "react-final-form";
import diff from "object-diff";
const areObjectsIdentical = (a, b) =>
Object.keys(diff(a, b)).length === 0 && Object.keys(diff(b, a)).length === 0;
class AutoSave extends React.Component {
static defaultProps = {
debounced: [],
};
constructor(props) {
super(props);
this.state = {
submitting: false,
...this.splitValues(props.values),
};
}
componentDidUpdate() {
const { values, debounce } = this.props;
const { debouncedValues, immediateValues } = this.splitValues(values);
if (!areObjectsIdentical(this.state.immediateValues, immediateValues)) {
this.save();
}
if (!areObjectsIdentical(this.state.debouncedValues, debouncedValues)) {
if (this.timeout) {
clearTimeout(this.timeout);
}
this.timeout = setTimeout(() => {
this.save();
}, debounce);
}
}
splitValues = (values) => {
const { debounced } = this.props;
const debouncedValues = {};
const immediateValues = {};
Object.keys(values).forEach((key) => {
if (debounced.includes(key)) {
debouncedValues[key] = values[key];
} else {
immediateValues[key] = values[key];
}
});
return {
debouncedValues,
immediateValues,
};
};
save = async () => {
if (this.promise) {
await this.promise;
}
const { save, values } = this.props;
const { debouncedValues, immediateValues } = this.splitValues(values);
this.setState(
(state) => ({
submitting: true,
immediateValues: { ...immediateValues },
debouncedValues: { ...debouncedValues },
}),
async () => {
this.promise = save({
...this.state.immediateValues,
...this.state.debouncedValues,
});
await this.promise;
delete this.promise;
this.setState({ submitting: false });
},
);
};
render() {
// This component doesn't have to render anything, but it can render
// submitting state.
return (
this.state.submitting && Submitting...
);
}
}
// Make a HOC
// This is not the only way to accomplish auto-save, but it does let us:
// - Use built-in React lifecycle methods to listen for changes
// - Maintain state of when we are submitting
// - Render a message when submitting
// - Pass in debounce and save props nicely
export default (props) => (
);
================================================
FILE: examples/auto-save-selective-debounce/Styles.js
================================================
import styled, { css } from "styled-components";
const btn = (light, dark) => css`
white-space: nowrap;
display: inline-block;
border-radius: 5px;
padding: 5px 15px;
font-size: 16px;
color: white;
&:visited {
color: white;
}
background-image: linear-gradient(${light}, ${dark});
border: 1px solid ${dark};
&:hover {
background-image: linear-gradient(${light}, ${dark});
&[disabled] {
background-image: linear-gradient(${light}, ${dark});
}
}
&:visited {
color: black;
}
&[disabled] {
opacity: 0.6;
cursor: not-allowed;
}
`;
const btnDefault = css`
${btn("#ffffff", "#d5d5d5")} color: #555;
`;
const btnPrimary = btn("#4f93ce", "#285f8f");
const btnDanger = btn("#e27c79", "#c9302c");
export default styled.div`
font-family: sans-serif;
h1 {
text-align: center;
color: #222;
}
h2 {
text-align: center;
color: #222;
}
& > div {
text-align: center;
}
a {
display: block;
text-align: center;
color: #222;
margin-bottom: 10px;
}
p {
max-width: 500px;
margin: 10px auto;
& > a {
display: inline;
}
}
form,
div.form {
text-align: left;
max-width: 500px;
margin: 10px auto;
border: 1px solid #ccc;
padding: 20px;
box-shadow: 2px 2px 5px rgba(0, 0, 0, 0.3);
border-radius: 3px;
position: relative;
.loading {
text-align: center;
display: block;
position: absolute;
background: url("https://media.giphy.com/media/130AxGoOaR6t0I/giphy.gif")
center center;
background-size: fill;
font-size: 2em;
top: 0;
left: 0;
right: 0;
bottom: 0;
padding: 50px 0 0 0;
z-index: 2;
}
& > div {
display: flex;
flex-flow: row nowrap;
line-height: 2em;
margin: 5px;
position: relative;
& > label {
color: #333;
width: 110px;
min-width: 60px;
font-size: 1em;
line-height: 32px;
}
& > input,
& > select,
& > textarea {
flex: 1;
padding: 3px 5px;
font-size: 1em;
margin-left: 15px;
border: 1px solid #ccc;
border-radius: 3px;
}
& > input[type="checkbox"] {
margin-top: 7px;
}
& > div {
margin-left: 16px;
& > label {
display: block;
& > input {
margin-right: 3px;
}
}
}
& > span {
line-height: 32px;
margin-left: 10px;
color: #800;
font-weight: bold;
}
& > button.remove {
${btnDanger};
}
}
& > .buttons {
display: flex;
flex-flow: row nowrap;
justify-content: center;
margin-top: 15px;
}
.error {
display: flex;
font-weight: bold;
color: #800;
flex-flow: row nowrap;
justify-content: center;
}
pre {
position: relative;
border: 1px solid #ccc;
background: rgba(0, 0, 0, 0.1);
box-shadow: inset 1px 1px 3px rgba(0, 0, 0, 0.2);
padding: 20px;
}
.submitting {
display: block;
position: absolute;
top: -5px;
left: -5px;
right: -5px;
padding: 0;
text-align: center;
background: rgba(0, 0, 0, 0.4);
color: white;
z-index: 10;
font-weight: bold;
font-size: 0.8em;
}
}
button {
margin: 0 10px;
&[type="submit"] {
${btnPrimary};
}
&[type="button"] {
${btnDefault};
}
}
`;
================================================
FILE: examples/auto-save-selective-debounce/index.js
================================================
import React from "react";
import { render } from "react-dom";
import Styles from "./Styles";
import { Form, Field } from "react-final-form";
import AutoSave from "./AutoSave";
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
const save = async (values) => {
console.log("Saving", values);
await sleep(2000);
};
const App = () => (
🏁
{" "}
React Final Form
Auto-Save with Selective Debounce
Read Docs
The AutoSave component uses{" "}
FormSpy
{" "}
to listen to changes to values and auto-save changes. But it only
debounces changes to a specific list of fields given to the{" "}
AutoSave component, in this case, all the text inputs. Look
in the console for the save events.
{() => (
{/* Don't even need a
tag */}
{/* 👇 👇 👇 👇 */}
{/* ☝️ ️☝️ ️☝️ ️️️️☝️ ️️*/}
First Name
Last Name
Email
Favorite Color
❤️
{" "}
Red
💚
{" "}
Green
💙
{" "}
Blue
Employed?
Toppings
🐷
{" "}
Ham
🍄
{" "}
Mushrooms
🧀
{" "}
Cheese
🐓
{" "}
Chicken
🍍
{" "}
Pinapple
Best Stooge?
{" "}
Larry
{" "}
Moe
{" "}
Curly
Notes
)}
);
render( , document.getElementById("root"));
================================================
FILE: examples/auto-save-selective-debounce/package.json
================================================
{
"name": "react-final-form-auto-save-with-selective-debounce",
"version": "1.0.0",
"description": "Demonstrates how to use a FormSpy component to auto-save your form values with a debounce, restricting which fields are debounced.",
"keywords": [],
"main": "index.js",
"dependencies": {
"final-form": "4.20.4",
"object-diff": "0.0.4",
"react": "17.0.2",
"react-dom": "17.0.2",
"react-final-form": "6.5.3",
"styled-components": "4.2.0"
}
}
================================================
FILE: examples/auto-save-selective-debounce/readme.md
================================================
# Auto-Save with _Selective_ Debounce
[](https://codesandbox.io/s/98j0v46zj4)
================================================
FILE: examples/auto-save-with-debounce/AutoSave.js
================================================
import React from "react";
import { FormSpy } from "react-final-form";
import diff from "object-diff";
class AutoSave extends React.Component {
constructor(props) {
super(props);
this.state = { values: props.values, submitting: false };
}
componentWillReceiveProps(nextProps) {
if (this.timeout) {
clearTimeout(this.timeout);
}
this.timeout = setTimeout(this.save, this.props.debounce);
}
save = async () => {
if (this.promise) {
await this.promise;
}
const { values, save } = this.props;
// This diff step is totally optional
const difference = diff(this.state.values, values);
if (Object.keys(difference).length) {
// values have changed
this.setState({ submitting: true, values });
this.promise = save(difference);
await this.promise;
delete this.promise;
this.setState({ submitting: false });
}
};
render() {
// This component doesn't have to render anything, but it can render
// submitting state.
return (
this.state.submitting && Submitting...
);
}
}
// Make a HOC
// This is not the only way to accomplish auto-save, but it does let us:
// - Use built-in React lifecycle methods to listen for changes
// - Maintain state of when we are submitting
// - Render a message when submitting
// - Pass in debounce and save props nicely
export default (props) => (
);
================================================
FILE: examples/auto-save-with-debounce/Styles.js
================================================
import styled, { css } from "styled-components";
const btn = (light, dark) => css`
white-space: nowrap;
display: inline-block;
border-radius: 5px;
padding: 5px 15px;
font-size: 16px;
color: white;
&:visited {
color: white;
}
background-image: linear-gradient(${light}, ${dark});
border: 1px solid ${dark};
&:hover {
background-image: linear-gradient(${light}, ${dark});
&[disabled] {
background-image: linear-gradient(${light}, ${dark});
}
}
&:visited {
color: black;
}
&[disabled] {
opacity: 0.6;
cursor: not-allowed;
}
`;
const btnDefault = css`
${btn("#ffffff", "#d5d5d5")} color: #555;
`;
const btnPrimary = btn("#4f93ce", "#285f8f");
const btnDanger = btn("#e27c79", "#c9302c");
export default styled.div`
font-family: sans-serif;
h1 {
text-align: center;
color: #222;
}
h2 {
text-align: center;
color: #222;
}
& > div {
text-align: center;
}
a {
display: block;
text-align: center;
color: #222;
margin-bottom: 10px;
}
p {
max-width: 500px;
margin: 10px auto;
& > a {
display: inline;
}
}
form,
div.form {
text-align: left;
max-width: 500px;
margin: 10px auto;
border: 1px solid #ccc;
padding: 20px;
box-shadow: 2px 2px 5px rgba(0, 0, 0, 0.3);
border-radius: 3px;
position: relative;
.loading {
text-align: center;
display: block;
position: absolute;
background: url("https://media.giphy.com/media/130AxGoOaR6t0I/giphy.gif")
center center;
background-size: fill;
font-size: 2em;
top: 0;
left: 0;
right: 0;
bottom: 0;
padding: 50px 0 0 0;
z-index: 2;
}
& > div {
display: flex;
flex-flow: row nowrap;
line-height: 2em;
margin: 5px;
position: relative;
& > label {
color: #333;
width: 110px;
min-width: 60px;
font-size: 1em;
line-height: 32px;
}
& > input,
& > select,
& > textarea {
flex: 1;
padding: 3px 5px;
font-size: 1em;
margin-left: 15px;
border: 1px solid #ccc;
border-radius: 3px;
}
& > input[type="checkbox"] {
margin-top: 7px;
}
& > div {
margin-left: 16px;
& > label {
display: block;
& > input {
margin-right: 3px;
}
}
}
& > span {
line-height: 32px;
margin-left: 10px;
color: #800;
font-weight: bold;
}
& > button.remove {
${btnDanger};
}
}
& > .buttons {
display: flex;
flex-flow: row nowrap;
justify-content: center;
margin-top: 15px;
}
.error {
display: flex;
font-weight: bold;
color: #800;
flex-flow: row nowrap;
justify-content: center;
}
pre {
position: relative;
border: 1px solid #ccc;
background: rgba(0, 0, 0, 0.1);
box-shadow: inset 1px 1px 3px rgba(0, 0, 0, 0.2);
padding: 20px;
}
.submitting {
display: block;
position: absolute;
top: -5px;
left: -5px;
right: -5px;
padding: 0;
text-align: center;
background: rgba(0, 0, 0, 0.4);
color: white;
z-index: 10;
font-weight: bold;
font-size: 0.8em;
}
}
button {
margin: 0 10px;
&[type="submit"] {
${btnPrimary};
}
&[type="button"] {
${btnDefault};
}
}
`;
================================================
FILE: examples/auto-save-with-debounce/index.js
================================================
import React from "react";
import { render } from "react-dom";
import Styles from "./Styles";
import { Form, Field } from "react-final-form";
import AutoSave from "./AutoSave";
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
const save = async (values) => {
console.log("Saving", values);
await sleep(2000);
};
const App = () => (
🏁
{" "}
React Final Form
Auto-Save with Debounce
Read Docs
The AutoSave component uses{" "}
FormSpy
{" "}
to listen to changes to values and auto-save changes. Look in the console
for the save events.
{() => (
{/* Don't even need a
tag */}
{/* 👇 👇 👇 👇 */}
{/* ☝️ ️☝️ ️☝️ ️️️️☝️ ️️*/}
First Name
Last Name
Email
Favorite Color
❤️
{" "}
Red
💚
{" "}
Green
💙
{" "}
Blue
Employed?
Toppings
🐷
{" "}
Ham
🍄
{" "}
Mushrooms
🧀
{" "}
Cheese
🐓
{" "}
Chicken
🍍
{" "}
Pinapple
Best Stooge?
{" "}
Larry
{" "}
Moe
{" "}
Curly
Notes
)}
);
render( , document.getElementById("root"));
================================================
FILE: examples/auto-save-with-debounce/package.json
================================================
{
"name": "react-final-form-auto-save-with-debounce",
"version": "1.0.0",
"description": "Demonstrates how to use a FormSpy component to auto-save your form values with a debounce.",
"keywords": [
"final-form",
"autosave",
"form",
"debounce",
"react-final-form"
],
"main": "index.js",
"dependencies": {
"final-form": "4.20.4",
"object-diff": "0.0.4",
"react": "17.0.2",
"react-dom": "17.0.2",
"react-final-form": "6.5.3",
"styled-components": "4.2.0"
}
}
================================================
FILE: examples/auto-save-with-debounce/readme.md
================================================
# Auto-Save with Debounce
[](https://codesandbox.io/s/5w4yrpyo7k)
================================================
FILE: examples/calculated-fields/Styles.js
================================================
import styled, { css } from "styled-components";
const btn = (light, dark) => css`
white-space: nowrap;
display: inline-block;
border-radius: 5px;
padding: 5px 15px;
font-size: 16px;
color: white;
&:visited {
color: white;
}
background-image: linear-gradient(${light}, ${dark});
border: 1px solid ${dark};
&:hover {
background-image: linear-gradient(${light}, ${dark});
&[disabled] {
background-image: linear-gradient(${light}, ${dark});
}
}
&:visited {
color: black;
}
&[disabled] {
opacity: 0.6;
cursor: not-allowed;
}
`;
const btnDefault = css`
${btn("#ffffff", "#d5d5d5")} color: #555;
`;
const btnPrimary = btn("#4f93ce", "#285f8f");
const btnDanger = btn("#e27c79", "#c9302c");
export default styled.div`
font-family: sans-serif;
h1 {
text-align: center;
color: #222;
}
h2 {
text-align: center;
color: #222;
}
& > div {
text-align: center;
}
a {
display: block;
text-align: center;
color: #222;
margin-bottom: 10px;
}
p {
max-width: 500px;
margin: 10px auto;
& > a {
display: inline;
}
}
form {
max-width: 500px;
margin: 10px auto;
border: 1px solid #ccc;
padding: 20px;
box-shadow: 2px 2px 5px rgba(0, 0, 0, 0.3);
border-radius: 3px;
position: relative;
.loading {
text-align: center;
display: block;
position: absolute;
background: url("https://media.giphy.com/media/130AxGoOaR6t0I/giphy.gif")
center center;
background-size: fill;
font-size: 2em;
top: 0;
left: 0;
right: 0;
bottom: 0;
padding: 50px 0 0 0;
z-index: 2;
}
& > div {
display: flex;
flex-flow: row nowrap;
line-height: 2em;
margin: 5px;
position: relative;
& > label {
color: #333;
width: 110px;
min-width: 60px;
font-size: 1em;
line-height: 32px;
}
& > input,
& > select,
& > textarea {
flex: 1;
padding: 3px 5px;
font-size: 1em;
margin-left: 15px;
border: 1px solid #ccc;
border-radius: 3px;
}
& > input[type="checkbox"] {
margin-top: 7px;
}
& > div {
margin-left: 16px;
& > label {
display: block;
& > input {
margin-right: 3px;
}
}
}
& > span {
line-height: 32px;
margin-left: 10px;
color: #800;
font-weight: bold;
}
& > button.remove {
${btnDanger};
}
}
& > .buttons {
display: flex;
flex-flow: row nowrap;
justify-content: center;
margin-top: 15px;
}
button {
margin: 0 10px;
&[type="submit"] {
${btnPrimary};
}
&[type="button"] {
${btnDefault};
}
}
.error {
display: flex;
font-weight: bold;
color: #800;
flex-flow: row nowrap;
justify-content: center;
}
pre {
position: relative;
border: 1px solid #ccc;
background: rgba(0, 0, 0, 0.1);
box-shadow: inset 1px 1px 3px rgba(0, 0, 0, 0.2);
padding: 20px;
}
}
`;
================================================
FILE: examples/calculated-fields/index.js
================================================
import React from "react";
import { render } from "react-dom";
import Styles from "./Styles";
import { Form, Field } from "react-final-form";
import createDecorator from "final-form-calculate";
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
const onSubmit = async (values) => {
await sleep(300);
window.alert(JSON.stringify(values, 0, 2));
};
const calculator = createDecorator(
{
field: "minimum", // when minimum changes...
updates: {
// ...update maximum to the result of this function
maximum: (minimumValue, allValues) =>
Math.max(minimumValue || 0, allValues.maximum || 0),
},
},
{
field: "maximum", // when maximum changes...
updates: {
// update minimum to the result of this function
minimum: (maximumValue, allValues) =>
Math.min(maximumValue || 0, allValues.minimum || 0),
},
},
{
field: /day\[\d\]/, // when a field matching this pattern changes...
updates: {
// ...update the total to the result of this function
total: (ignoredValue, allValues) =>
(allValues.day || []).reduce(
(sum, value) => sum + Number(value || 0),
0,
),
},
},
);
const App = () => (
🏁
{" "}
React Final Form Example
Calculated Fields
Read Docs
Change the minimum and maximum values with the arrow keys and notice that
the other updates so that minimum is always <= maximum.
As you enter numbers for each day of the week, the total is calulated in
realtime.
(
Minimum
Maximum
Monday
Tuesday
Wednesday
Thursday
Friday
Total
Submit
Reset
{JSON.stringify(values, 0, 2)}
)}
/>
);
render( , document.getElementById("root"));
================================================
FILE: examples/calculated-fields/package.json
================================================
{
"name": "react-final-form-calculated-fields",
"version": "1.0.0",
"description": "Demonstrates how to use the final-form-calculate decorator to achieve realtime field calculations.",
"keywords": [],
"homepage": "https://codesandbox.io/s/oq52p6v96y",
"main": "index.js",
"dependencies": {
"final-form": "4.20.4",
"final-form-calculate": "1.3.1",
"react": "17.0.2",
"react-dom": "17.0.2",
"react-final-form": "6.5.3",
"styled-components": "4.2.0"
}
}
================================================
FILE: examples/calculated-fields/readme.md
================================================
# Calculated Fields
[](https://codesandbox.io/s/oq52p6v96y)
================================================
FILE: examples/chakra/index.js
================================================
/* eslint-disable jsx-a11y/accessible-emoji */
import React from "react";
import { render } from "react-dom";
import {
Box,
Button,
ButtonGroup,
CSSReset,
Heading,
Icon,
Link,
ThemeProvider,
theme,
FormControl,
FormLabel,
FormErrorMessage,
Input,
Checkbox,
Progress,
Radio,
RadioGroup,
Stack,
Textarea,
} from "@chakra-ui/core";
import { Form, Field, useField, useForm } from "react-final-form";
import validate from "./validate";
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
const onSubmit = async (values) => {
await sleep(300);
window.alert(JSON.stringify(values, 0, 2));
};
const App = () => (
React Final Form
Chakra Example
Example using React Final Form and{" "}
Chakra
.
Read Docs
(
{/*
This example uses a mixture of custom field components using useField()
and components adapted to take the { input, meta } structure
provides
*/}
Employed
Red
Green
Blue
Toppings
🐓 Chicken
🐷 Ham
🍄 Mushrooms
🧀 Cheese
🐟 Tuna
🍍 Pineapple
Submit
Reset
{JSON.stringify(values, 0, 2)}
)}
/>
);
const AdaptedTextarea = ({ input, meta, ...rest }) => (
);
const CheckboxControl = ({ name, value, children }) => {
const {
input: { checked, ...input },
meta: { error, touched, invalid },
} = useField(name, {
type: "checkbox", // important for RFF to manage the checked prop
});
return (
{children}
{error}
);
};
const CheckboxArrayControl = ({ name, value, children }) => {
const {
input: { checked, ...input },
meta: { error, touched },
} = useField(name, {
type: "checkbox", // important for RFF to manage the checked prop
value, // important for RFF to manage list of strings
});
return (
{children}
);
};
const AdaptedRadioGroup = ({ input, meta, label, children }) => (
{label}
{children}
{meta.error}
);
const Control = ({ name, ...rest }) => {
const {
meta: { error, touched },
} = useField(name, { subscription: { touched: true, error: true } });
return ;
};
const Error = ({ name }) => {
const {
meta: { error },
} = useField(name, { subscription: { error: true } });
return {error} ;
};
const InputControl = ({ name, label }) => {
const { input, meta } = useField(name);
return (
{label}
);
};
const TextareaControl = ({ name, label }) => (
{label}
);
const PercentComplete = (props) => {
const form = useForm();
const numFields = form.getRegisteredFields().length;
const numErrors = Object.keys(form.getState().errors).length;
return (
);
};
render( , document.getElementById("root"));
================================================
FILE: examples/chakra/package.json
================================================
{
"name": "Chakra-Example",
"version": "1.0.0",
"description": "An example form using Chakra UI",
"keywords": [
"state management",
"react-final-form",
"form",
"final-form",
"react"
],
"main": "index.js",
"dependencies": {
"@chakra-ui/core": "^0.2.6",
"@emotion/core": "^10.0.17",
"@emotion/styled": "^10.0.17",
"emotion-theming": "^10.0.17",
"final-form": "4.20.4",
"react": "17.0.2",
"react-dom": "17.0.2",
"react-final-form": "6.5.3",
"react-scripts": "3.2.0"
},
"scripts": {
"start": "react-scripts start",
"build": "react-scripts build"
},
"browserslist": [
">0.2%",
"not dead",
"not ie <= 11",
"not op_mini all"
]
}
================================================
FILE: examples/chakra/readme.md
================================================
# Chakra UI Example
[](https://codesandbox.io/s/github/final-form/react-final-form/tree/master/examples/chakra?fontsize=14)
================================================
FILE: examples/chakra/validate.js
================================================
const validate = (values) => {
const errors = {};
if (!values.firstName) {
errors.firstName = "Required";
}
if (!values.lastName) {
errors.lastName = "Required";
}
if (values.employed) {
errors.employed = "We're only accepted unemployed applicants at the moment";
}
if (!values.favoriteColor) {
errors.favoriteColor = "Required";
} else if (values.favoriteColor === "#00ff00") {
errors.favoriteColor = "Not green! Gross!";
}
if (!values.toppings || values.toppings.length < 2) {
errors.toppings = "You need at least two toppings";
} else if (values.toppings && values.toppings.length > 3) {
errors.toppings = "No more than three toppings";
}
if (!values.notes) {
errors.notes = "Required";
}
return errors;
};
export default validate;
================================================
FILE: examples/conditional-fields/Styles.js
================================================
import styled, { css } from "styled-components";
const btn = (light, dark) => css`
white-space: nowrap;
display: inline-block;
border-radius: 5px;
padding: 5px 15px;
font-size: 16px;
color: white;
&:visited {
color: white;
}
background-image: linear-gradient(${light}, ${dark});
border: 1px solid ${dark};
&:hover {
background-image: linear-gradient(${light}, ${dark});
&[disabled] {
background-image: linear-gradient(${light}, ${dark});
}
}
&:visited {
color: black;
}
&[disabled] {
opacity: 0.6;
cursor: not-allowed;
}
`;
const btnDefault = css`
${btn("#ffffff", "#d5d5d5")} color: #555;
`;
const btnPrimary = btn("#4f93ce", "#285f8f");
const btnDanger = btn("#e27c79", "#c9302c");
export default styled.div`
font-family: sans-serif;
h1 {
text-align: center;
color: #222;
}
h2 {
text-align: center;
color: #222;
}
& > div {
text-align: center;
}
a {
display: block;
text-align: center;
color: #222;
margin-bottom: 10px;
}
p {
max-width: 500px;
margin: 10px auto;
& > a {
display: inline;
}
}
.loading {
font-size: 2em;
font-weight: bold;
text-align: center;
margin: 50px;
}
form,
div.form {
text-align: left;
max-width: 500px;
margin: 10px auto;
border: 1px solid #ccc;
padding: 20px;
box-shadow: 2px 2px 5px rgba(0, 0, 0, 0.3);
border-radius: 3px;
position: relative;
& > div {
display: flex;
flex-flow: row nowrap;
line-height: 2em;
margin: 5px;
position: relative;
& > label {
color: #333;
width: 110px;
min-width: 60px;
font-size: 1em;
line-height: 32px;
}
& > input,
& > .downshift > input,
& > select,
& > textarea {
flex: 1;
padding: 3px 5px;
font-size: 1em;
margin-left: 15px;
border: 1px solid #ccc;
border-radius: 3px;
}
& > input[type="checkbox"] {
margin-top: 7px;
}
& > div {
margin-left: 16px;
& > label {
margin-left: 0;
display: block;
& > input {
margin-right: 3px;
}
}
&.downshift {
margin-left: 0;
padding-left: 15px;
flex: 1;
& > input {
width: 100%;
padding: 6px 5px;
font-size: 1em;
margin-left: 0;
border: 1px solid #ccc;
border-radius: 3px;
}
}
}
& > span {
line-height: 32px;
margin-left: 10px;
color: #800;
font-weight: bold;
}
& > button.remove {
${btnDanger};
}
}
& > .buttons {
display: flex;
flex-flow: row nowrap;
justify-content: center;
margin-top: 15px;
}
.error {
display: flex;
font-weight: bold;
color: #800;
flex-flow: row nowrap;
justify-content: center;
}
pre {
position: relative;
border: 1px solid #ccc;
background: rgba(0, 0, 0, 0.1);
box-shadow: inset 1px 1px 3px rgba(0, 0, 0, 0.2);
padding: 20px;
}
.submitting {
display: block;
position: absolute;
top: -5px;
left: -5px;
right: -5px;
padding: 0;
text-align: center;
background: rgba(0, 0, 0, 0.4);
color: white;
z-index: 10;
font-weight: bold;
font-size: 0.8em;
}
.saving {
font-size: 0.8em;
font-weight: bold;
color: darkblue;
margin: 8px 0 0 7px;
}
}
button {
margin: 0 10px;
&[type="submit"] {
${btnPrimary};
}
&[type="button"] {
${btnDefault};
}
}
.downshift-options {
border: 1px solid #ddd;
box-shadow: 1px 1px 2px rgba(0, 0, 0, 0.3);
& > div {
padding: 3px 5px;
}
}
`;
================================================
FILE: examples/conditional-fields/index.js
================================================
import React from "react";
import { render } from "react-dom";
import Styles from "./Styles";
import { Form, Field } from "react-final-form";
import pickupTimes from "./pickupTimes";
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
const onSubmit = async (values) => {
await sleep(300);
window.alert(JSON.stringify(values, 0, 2));
};
const Error = ({ name }) => (
{({ meta: { error, touched } }) =>
error && touched ? {error} : null
}
);
const Condition = ({ when, is, children }) => (
{({ input: { value } }) => (value === is ? children : null)}
);
const App = () => (
🏁
{" "}
React Final Form
Conditional Fields
Read Docs
Sometimes you might want to conditionally show or hide some parts of your
form depending on values the user has already provided for other form
inputs.{" "}
🏁
{" "}
React Final Form makes that very easy to do by creating a{" "}
Condition component out of a Field component.
{
const errors = {};
if (!values.firstName) {
errors.firstName = "Required";
}
if (!values.reception) {
errors.reception = "Required";
}
if (values.reception === "delivery") {
if (!values.street) {
errors.street = "Required";
}
} else if (values.reception === "pickup") {
if (!values.pickupTime) {
errors.pickupTime = "Required";
}
}
return errors;
}}
>
{({ handleSubmit, form, submitting, pristine, values }) => (
First Name
Transport
{" "}
Delivery
{" "}
Pickup
Street
Pickup Time
$
{pickupTimes.map((time) => (
{time}
))}
Is it a gift?
Gift Message
Submit
Reset
{JSON.stringify(values, 0, 2)}
)}
);
render( , document.getElementById("root"));
================================================
FILE: examples/conditional-fields/package.json
================================================
{
"name": "react-final-form-conditional-fields",
"version": "1.0.0",
"description": "Demonstrates how to conditionally show or hide fields based on the values of other fields.",
"keywords": [],
"homepage": "https://codesandbox.io/s/new",
"main": "src/index.js",
"dependencies": {
"final-form": "4.20.4",
"react": "17.0.2",
"react-dom": "17.0.2",
"react-final-form": "6.5.3",
"react-scripts": "3.0.1",
"styled-components": "4.2.0"
},
"devDependencies": {},
"scripts": {
"start": "react-scripts start",
"build": "react-scripts build",
"test": "react-scripts test --env=jsdom",
"eject": "react-scripts eject"
}
}
================================================
FILE: examples/conditional-fields/pickupTimes.js
================================================
// This is not the best way to do this. Don't think to hard about this.
const now = new Date();
let hours = now.getHours();
const times = [];
if (now.getMinutes() < 30) {
times.push(`${++hours}:30`);
} else {
hours++;
}
while (times.length < 6) {
times.push(`${hours}:00`);
times.push(`${hours}:30`);
hours = (hours + 1) % 24;
}
export default times;
================================================
FILE: examples/conditional-fields/readme.md
================================================
# Conditional Fields
[](https://codesandbox.io/s/lm4p3m92q)
================================================
FILE: examples/credit-card/Card.js
================================================
import React from "react";
import styled from "styled-components";
import Cards from "react-credit-cards";
const Container = styled.div`
.rccs {
margin: 0 auto;
-webkit-perspective: 1000px;
perspective: 1000px;
width: 290px;
}
.rccs__card {
height: 182.873px;
margin: 0 auto;
position: relative;
-webkit-transform-style: preserve-3d;
transform-style: preserve-3d;
-webkit-transition: all 0.4s linear;
transition: all 0.4s linear;
width: 290px;
}
.rccs__card--front,
.rccs__card--back {
-webkit-backface-visibility: hidden;
backface-visibility: hidden;
background: linear-gradient(25deg, #939393, #717171);
border-radius: 14.5px;
color: #fff;
height: 100%;
left: 0;
overflow: hidden;
position: absolute;
top: 0;
-webkit-transform-style: preserve-3d;
transform-style: preserve-3d;
width: 100%;
-webkit-box-shadow: 0 0 20px rgba(0, 0, 0, 0.2);
box-shadow: 0 0 20px rgba(0, 0, 0, 0.2);
}
.rccs__card--front {
z-index: 20;
}
.rccs__card--back {
-webkit-transform: rotateY(180deg);
transform: rotateY(180deg);
}
.rccs__card--back .rccs__issuer {
background-position: bottom center;
bottom: 5%;
left: 50%;
opacity: 0.6;
right: auto;
top: auto;
-webkit-transform: translateX(-50%);
transform: translateX(-50%);
}
.rccs__card__background {
height: 200%;
left: -170%;
position: absolute;
top: -60%;
-webkit-transform: rotate(25deg);
transform: rotate(25deg);
-webkit-transition: all 0.5s ease-out;
transition: all 0.5s ease-out;
width: 150%;
left: -170%;
}
.rccs__card--flipped {
-webkit-transform: rotateY(180deg);
transform: rotateY(180deg);
}
.rccs__card--flipped .rccs__card--front {
z-index: 10;
}
.rccs__card--flipped .rccs__card--back {
z-index: 20;
}
.rccs__card--unknown > div {
background: linear-gradient(25deg, #999, #999);
-webkit-box-shadow: none;
box-shadow: none;
}
.rccs__card--unknown .rccs__issuer {
visibility: hidden;
}
.rccs__card:not(.rccs__card--unknown) .rccs__card__background {
left: -22%;
}
.rccs__card--amex .rccs__card__background {
background: linear-gradient(25deg, #308c67, #a3f2cf);
}
.rccs__card--amex .rccs__issuer {
background-image: url("data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNTEyIiBoZWlnaHQ9IjUxMiIgdmlld0JveD0iMCAwIDUxMiA1MTIiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgcHJlc2VydmVBc3BlY3RSYXRpbz0ieE1pZFlNaWQiPjxkZWZzPjxyYWRpYWxHcmFkaWVudCBjeD0iMTcuNTQxJSIgY3k9IjE3LjQ2NiUiIGZ4PSIxNy41NDElIiBmeT0iMTcuNDY2JSIgcj0iOTEuMjM3JSIgaWQ9ImEiPjxzdG9wIHN0b3AtY29sb3I9IiM2NUJDRjEiIG9mZnNldD0iMCUiLz48c3RvcCBzdG9wLWNvbG9yPSIjMjNBREUzIiBvZmZzZXQ9IjQ1LjQ2JSIvPjxzdG9wIHN0b3AtY29sb3I9IiMwREE2RTAiIG9mZnNldD0iNTAlIi8+PHN0b3Agc3RvcC1jb2xvcj0iIzA1NTFDMyIgb2Zmc2V0PSIxMDAlIi8+PC9yYWRpYWxHcmFkaWVudD48L2RlZnM+PHBhdGggZmlsbD0idXJsKCNhKSIgZD0iTTAgMGg1MTJ2NTEyaC01MTJ6Ii8+PHBhdGggZD0iTTQ1Ljc5MSAyMjAuOTM1bC05Ljc3My0yMy44MTMtOS43MTcgMjMuODEzaDE5LjQ4OXptMjE1LjI4OS05LjQ4M2MtMS45NjIgMS4xOTEtNC4yODMgMS4yMzEtNy4wNjMgMS4yMzFoLTE3LjM0NXYtMTMuMjY4aDE3LjU4MWMyLjQ4OCAwIDUuMDg0LjExMiA2Ljc3MSAxLjA3NyAxLjg1Mi44NyAyLjk5OCAyLjcyMiAyLjk5OCA1LjI4MSAwIDIuNjExLTEuMDkgNC43MTItMi45NDIgNS42Nzl6bTEyMy43MzkgOS40ODNsLTkuODgxLTIzLjgxMy05LjgyNyAyMy44MTNoMTkuNzA3em0tMjMwLjY1OCAyNS43NzZoLTE0LjYzN2wtLjA1NC00Ni43ODQtMjAuNzA0IDQ2Ljc4NGgtMTIuNTM2bC0yMC43NTgtNDYuODI1djQ2LjgyNWgtMjkuMDRsLTUuNDg2LTEzLjMyNGgtMjkuNzI5bC01LjU0MiAxMy4zMjRoLTE1LjUwN2wyNS41NjgtNTkuNzM1aDIxLjIxNGwyNC4yODQgNTYuNTU2di01Ni41NTZoMjMuMzA0bDE4LjY4NiA0MC41MjMgMTcuMTY1LTQwLjUyM2gyMy43NzJ2NTkuNzM1aC4wMDJ6bTU4LjMzOCAwaC00Ny42OTd2LTU5LjczNWg0Ny42OTd2MTIuNDM5aC0zMy40MTl2MTAuNzY3aDMyLjYxN3YxMi4yNDVoLTMyLjYxN3YxMS45MjloMzMuNDE5djEyLjM1NHptNjcuMjUxLTQzLjY0N2MwIDkuNTI0LTYuMzU3IDE0LjQ0NC0xMC4wNjEgMTUuOTIyIDMuMTI0IDEuMTg5IDUuNzkzIDMuMjkgNy4wNjMgNS4wMyAyLjAxNiAyLjk3MSAyLjM2NCA1LjYyNSAyLjM2NCAxMC45NnYxMS43MzVoLTE0LjQwMWwtLjA1NC03LjUzM2MwLTMuNTk0LjM0NC04Ljc2NC0yLjI1NC0xMS42MzctMi4wODYtMi4xMDEtNS4yNjYtMi41NTctMTAuNDA3LTIuNTU3aC0xNS4zMjd2MjEuNzI3aC0xNC4yNzd2LTU5LjczNWgzMi44NGM3LjI5NyAwIDEyLjY3My4xOTMgMTcuMjg5IDIuODYxIDQuNTE3IDIuNjY4IDcuMjI1IDYuNTY0IDcuMjI1IDEzLjIyN3ptMjIuODUgNDMuNjQ3aC0xNC41Njl2LTU5LjczNWgxNC41Njl2NTkuNzM1em0xNjkuMDE3IDBoLTIwLjIzM2wtMjcuMDY0LTQ0LjgzNHY0NC44MzRoLTI5LjA3OGwtNS41NTctMTMuMzI0aC0yOS42NmwtNS4zOTEgMTMuMzI0aC0xNi43MDdjLTYuOTQgMC0xNS43MjctMS41MzUtMjAuNzA0LTYuNjA3LTUuMDE4LTUuMDcyLTcuNjI5LTExLjk0Mi03LjYyOS0yMi44MDUgMC04Ljg1OSAxLjU2LTE2Ljk1OCA3LjY5Ny0yMy4zNTggNC42MTYtNC43NjcgMTEuODQ1LTYuOTY1IDIxLjY4NC02Ljk2NWgxMy44MjN2MTIuNzk5aC0xMy41MzNjLTUuMjExIDAtOC4xNTMuNzc1LTEwLjk4NyAzLjUzOS0yLjQzNCAyLjUxNS00LjEwNCA3LjI3LTQuMTA0IDEzLjUzMSAwIDYuNCAxLjI3MiAxMS4wMTQgMy45MjYgMTQuMDI4IDIuMTk4IDIuMzY0IDYuMTkzIDMuMDgxIDkuOTUxIDMuMDgxaDYuNDEybDIwLjEyNC00Ni45NzdoMjEuMzk0bDI0LjE3NCA1Ni41di01Ni41aDIxLjc0bDI1LjA5OCA0MS42MDJ2LTQxLjYwMmgxNC42MjV2NTkuNzMzem0tNDcxLjYxNiAxMS43MzNoMjQuMzk1bDUuNTAxLTEzLjI2OGgxMi4zMTVsNS40ODYgMTMuMjY4aDQ4di0xMC4xNDRsNC4yODUgMTAuMTg3aDI0LjkxOGw0LjI4NS0xMC4zMzh2MTAuMjk1aDExOS4yODlsLS4wNTYtMjEuNzc5aDIuMzA4YzEuNjE2LjA1NiAyLjA4OC4yMDUgMi4wODggMi44NzR2MTguOTA2aDYxLjY5N3YtNS4wN2M0Ljk3NiAyLjY2NyAxMi43MTcgNS4wNyAyMi45MDIgNS4wN2gyNS45NTZsNS41NTUtMTMuMjY4aDEyLjMxNWw1LjQzMiAxMy4yNjhoNTAuMDE4di0xMi42MDNsNy41NzQgMTIuNjAzaDQwLjA4MXYtODMuMzEyaC0zOS42Njd2OS44MzlsLTUuNTU1LTkuODM5aC00MC43MDN2OS44MzlsLTUuMTAxLTkuODM5aC01NC45OGMtOS4yMDMgMC0xNy4yOTMgMS4yODUtMjMuODI4IDQuODY1di00Ljg2NWgtMzcuOTQxdjQuODY1Yy00LjE1OC0zLjY5LTkuODI1LTQuODY1LTE2LjEyNS00Ljg2NWgtMTM4LjYxM2wtOS4zMDEgMjEuNTE4LTkuNTUxLTIxLjUxOGgtNDMuNjZ2OS44MzlsLTQuNzk2LTkuODM5aC0zNy4yMzVsLTE3LjI5MSAzOS42MTF2NDMuNzAxaC4wMDJ6TTUxMiAzMDIuMDE0aC0yNi4wMzljLTIuNiAwLTQuMzI3LjA5Ny01Ljc4MiAxLjA4LTEuNTA3Ljk2OC0yLjA4OCAyLjQwNS0yLjA4OCA0LjMwMiAwIDIuMjU1IDEuMjczIDMuNzkgMy4xMjQgNC40NTMgMS41MDcuNTI1IDMuMTI2LjY3OCA1LjUwNi42NzhsNy43NDMuMjA3YzcuODE0LjE5MyAxMy4wMjkgMS41MzYgMTYuMjA5IDQuODEyLjU3OS40NTYuOTI3Ljk2OCAxLjMyNSAxLjQ4di0xNy4wMTJ6bTAgMzkuNDE2Yy0zLjQ3IDUuMDc1LTEwLjIzMyA3LjY0OC0xOS4zODggNy42NDhoLTI3LjU5MXYtMTIuODJoMjcuNDc5YzIuNzI2IDAgNC42MzMtLjM1OSA1Ljc4Mi0xLjQ4Ljk5NS0uOTI1IDEuNjg5LTIuMjY4IDEuNjg5LTMuOSAwLTEuNzQyLS42OTQtMy4xMjQtMS43NDUtMy45NTQtMS4wMzctLjkxMi0yLjU0Ni0xLjMyNy01LjAzNC0xLjMyNy0xMy40MTUtLjQ1Ni0zMC4xNTEuNDE1LTMwLjE1MS0xOC41MDQgMC04LjY3MiA1LjUwNi0xNy44IDIwLjQ5OC0xNy44aDI4LjQ1OHYtMTEuODk1aC0yNi40NDFjLTcuOTc5IDAtMTMuNzc2IDEuOTExLTE3Ljg4MSA0Ljg4MnYtNC44ODJoLTM5LjEwOWMtNi4yNTQgMC0xMy41OTUgMS41NS0xNy4wNjggNC44ODJ2LTQuODgyaC02OS44Mzl2NC44ODJjLTUuNTU4LTQuMDEtMTQuOTM3LTQuODgyLTE5LjI2NS00Ljg4MmgtNDYuMDY2djQuODgyYy00LjM5Ny00LjI1OC0xNC4xNzYtNC44ODItMjAuMTM2LTQuODgyaC01MS41NTZsLTExLjc5OCAxMi43NjgtMTEuMDUtMTIuNzY4aC03Ny4wMTR2ODMuNDIxaDc1LjU2NWwxMi4xNTctMTIuOTcgMTEuNDUyIDEyLjk3IDQ2LjU3OC4wNDF2LTE5LjYyNGg0LjU3OWM2LjE4LjA5NiAxMy40NjktLjE1MyAxOS45LTIuOTMzdjIyLjUxNGgzOC40MTl2LTIxLjc0MmgxLjg1M2MyLjM2NSAwIDIuNTk4LjA5NyAyLjU5OCAyLjQ2MXYxOS4yOGgxMTYuNzA5YzcuNDEgMCAxNS4xNTUtMS44OTcgMTkuNDQ0LTUuMzM4djUuMzM4aDM3LjAyYzcuNzA0IDAgMTUuMjI3LTEuMDggMjAuOTUxLTMuODQ1di0xNS41NDF6bS01Ni45OS0yMy44ODRjMi43ODIgMi44NzkgNC4yNzMgNi41MTQgNC4yNzMgMTIuNjY3IDAgMTIuODYyLTguMDM1IDE4Ljg2NS0yMi40NDQgMTguODY1aC0yNy44Mjd2LTEyLjgyaDI3LjcxNWMyLjcxIDAgNC42MzItLjM1OSA1LjgzNi0xLjQ4Ljk4My0uOTI1IDEuNjg3LTIuMjY4IDEuNjg3LTMuOSAwLTEuNzQyLS43NjMtMy4xMjQtMS43NDMtMy45NTQtMS4wOTMtLjkxMi0yLjYtMS4zMjctNS4wODgtMS4zMjctMTMuMzYxLS40NTYtMzAuMDkzLjQxNS0zMC4wOTMtMTguNTA0IDAtOC42NzIgNS40NDgtMTcuOCAyMC40MjYtMTcuOGgyOC42NDJ2MTIuNzI1aC0yNi4yMDhjLTIuNTk4IDAtNC4yODcuMDk3LTUuNzI0IDEuMDgtMS41NjUuOTY4LTIuMTQ1IDIuNDA1LTIuMTQ1IDQuMzAyIDAgMi4yNTUgMS4zMjkgMy43OSAzLjEyNiA0LjQ1MyAxLjUwNy41MjUgMy4xMjYuNjc4IDUuNTYuNjc4bDcuNjkxLjIwN2M3Ljc1Ni4xODkgMTMuMDggMS41MzEgMTYuMzE2IDQuODA4em0tMTI4LjkxOC0zLjY5MmMtMS45MTEgMS4xMzQtNC4yNzUgMS4yMzEtNy4wNTUgMS4yMzFoLTE3LjM1NnYtMTMuNDI4aDE3LjU5MmMyLjU0NCAwIDUuMDg5LjA1NCA2LjgxOCAxLjA4IDEuODUyLjk2OCAyLjk1OCAyLjgyIDIuOTU4IDUuMzc4IDAgMi41NTgtMS4xMDcgNC42MTktMi45NTggNS43Mzh6bTguNjI4IDcuNDRjMy4xOCAxLjE3NCA1Ljc4IDMuMjc4IDYuOTk5IDUuMDE5IDIuMDE3IDIuOTE5IDIuMzA5IDUuNjQzIDIuMzY3IDEwLjkxM3YxMS44NTJoLTE0LjM0M3YtNy40OGMwLTMuNTk3LjM0Ni04LjkyMi0yLjMwOS0xMS43MDItMi4wODgtMi4xNDItNS4yNy0yLjY1NC0xMC40ODItMi42NTRoLTE1LjI2OHYyMS44MzZoLTE0LjM1NnYtNTkuNzg2aDMyLjk4NWM3LjIzMyAwIDEyLjUwMS4zMTkgMTcuMTkgMi44MjEgNC41MDkgMi43MjQgNy4zNDUgNi40NTYgNy4zNDUgMTMuMjc2LS4wMDIgOS41NDItNi4zNjYgMTQuNDEyLTEwLjEyNyAxNS45MDV6bTE4LjA0OC0zMi4wMDJoNDcuNjg0djEyLjM2NGgtMzMuNDU1djEwLjg2OWgzMi42Mzl2MTIuMTk4aC0zMi42Mzl2MTEuODk1bDMzLjQ1NS4wNTR2MTIuNDA1aC00Ny42ODR2LTU5Ljc4NnptLTk2LjM5MyAyNy41OTFoLTE4LjQ2M3YtMTUuMjI1aDE4LjYyOWM1LjE1OCAwIDguNzM4IDIuMTAyIDguNzM4IDcuMzMgMCA1LjE3MS0zLjQxNSA3Ljg5NS04LjkwNCA3Ljg5NXptLTMyLjY5MyAyNi43NThsLTIxLjkzNS0yNC4zNTMgMjEuOTM1LTIzLjU3OXY0Ny45MzJ6bS01Ni42NDctNy4wMjJoLTM1LjEyN3YtMTEuODk1aDMxLjM2NnYtMTIuMTk4aC0zMS4zNjZ2LTEwLjg2OWgzNS44MTlsMTUuNjI3IDE3LjQyMy0xNi4zMTkgMTcuNTR6bTExMy41ODMtMjcuNjNjMCAxNi42MDgtMTIuMzkxIDIwLjAzNy0yNC44NzkgMjAuMDM3aC0xNy44Mjd2MjAuMDUzaC0yNy43NjlsLTE3LjU5Mi0xOS43OTItMTguMjgzIDE5Ljc5MmgtNTYuNTkxdi01OS43ODZoNTcuNDYybDE3LjU3OCAxOS41OTcgMTguMTczLTE5LjU5N2g0NS42NTJjMTEuMzM4IDAgMjQuMDc3IDMuMTM5IDI0LjA3NyAxOS42OTZ6IiBmaWxsPSIjZmZmIi8+PC9zdmc+");
}
.rccs__card--amex .rccs__cvc__front {
opacity: 0.5;
visibility: visible;
}
.rccs__card--dankort .rccs__card__background {
background: linear-gradient(25deg, #ccc, #999);
}
.rccs__card--dankort .rccs__issuer {
background-image: url("data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNTEyIiBoZWlnaHQ9IjMwOCIgdmlld0JveD0iMCAwIDUxMiAzMDgiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgcHJlc2VydmVBc3BlY3RSYXRpbz0ieE1pZFlNaWQiPjxkZWZzPjxsaW5lYXJHcmFkaWVudCB4MT0iNTAlIiB5MT0iMCUiIHgyPSI1MCUiIHkyPSIxMDAlIiBpZD0iYSI+PHN0b3Agc3RvcC1jb2xvcj0iI0Y1MDkxQSIgb2Zmc2V0PSIwJSIvPjxzdG9wIHN0b3AtY29sb3I9IiM5RTBDMTciIG9mZnNldD0iMTAwJSIvPjwvbGluZWFyR3JhZGllbnQ+PC9kZWZzPjxwYXRoIGQ9Ik0zNTguNDA3IDBoLTIwNC43OTVjLTg0Ljg0MiAwLTE1My42MTIgNjguNzcxLTE1My42MTIgMTUzLjU5MyAwIDg0Ljg1MSA2OC43NyAxNTMuNjA2IDE1My42MTIgMTUzLjYwNmgyMDQuNzk1Yzg0LjgyMiAwIDE1My41OTMtNjguNzU1IDE1My41OTMtMTUzLjYwNiAwLTg0LjgyMS02OC43Ny0xNTMuNTkzLTE1My41OTMtMTUzLjU5MyIgZmlsbD0iI0ZFRkVGRSIvPjxwYXRoIGQ9Ik0zOTUuNTkxIDE0NC40ODZsNjguMzI4IDgxLjI2MWMxNC4xNTEtMjAuNDUxIDIyLjQ2Mi00NS4yNDIgMjIuNDYyLTcxLjk0NSAwLTI4LjE4My05LjI1OS01NC4yNDgtMjQuODg0LTc1LjMxNWwtNjUuOTA2IDY1Ljk5OXptLTI0MS4zOTctMTE3LjM4NmMtMzguNTQ3IDAtNzMuMTIxIDE3LjI5My05Ni4zODMgNDQuNTI4aDE0NC4xNTljNTEuMDI3IDAgODkuNDc0IDEyLjk4MyA5My40MzEgNTUuOTc0bDUzLjcwMi01NS45ODFoMTA2LjkzMWMtMjMuMjYzLTI3LjIyNy01Ny44MzEtNDQuNTItOTYuMzYxLTQ0LjUyaC0yMDUuNDc5em0yOS4yMzYgMjA1LjQ3OWgtMTI4LjQyOWMyMy4yMzMgMjkuMTkzIDU5LjA2MiA0Ny45NDYgOTkuMTk0IDQ3Ljk0NmgyMDUuNDc5YzQwLjExNyAwIDc1LjkzNy0xOC43NDUgOTkuMTcxLTQ3LjkzOWgtMTA5LjI5NWwtNTYuNzk2LTcxLjc3NmMtMTAuNTA2IDQ4LjkzOS00NC44ODEgNzEuNzY5LTEwOS4zMjMgNzEuNzY5ek0xMTguNDQgMTI1Ljk5N2wtMTguMjIyIDUwLjFoNzMuNTE3YzIxLjczMyAwIDI4LjMxLTguOTk4IDMzLjE3My0yNi4zMTkgNC44MTUtMTcuMTMxLTcuMjc3LTIzLjc4LTIyLjEyOS0yMy43OGgtNjYuMzM5eiIgZmlsbD0idXJsKCNhKSIvPjwvc3ZnPg==");
}
.rccs__card--dinersclub > div {
color: #555;
}
.rccs__card--dinersclub .rccs__card__background {
background: linear-gradient(25deg, #fff, #eee);
}
.rccs__card--dinersclub .rccs__issuer {
background-image: url("data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNTEyIiBoZWlnaHQ9IjEzNCIgdmlld0JveD0iMCAwIDUxMiAxMzQiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgcHJlc2VydmVBc3BlY3RSYXRpbz0ieE1pZFlNaWQiPjxwYXRoIGQ9Ik05OS4yODUgMTMzLjg2YzM2LjQ0Ni4xNzcgNjkuNzE1LTI5LjY1OSA2OS43MTUtNjUuOTU1IDAtMzkuNjg5LTMzLjI2OS02Ny4xMjItNjkuNzE1LTY3LjExMWgtMzEuMzY1Yy0zNi44ODItLjAxMS02Ny4yNDEgMjcuNDI5LTY3LjI0MSA2Ny4xMTEgMCAzNi4zMDUgMzAuMzU4IDY2LjEzMyA2Ny4yNDEgNjUuOTU1aDMxLjM2NSIgZmlsbD0iIzAwNjA5NSIvPjxwYXRoIGQ9Ik04MS45MDkgMTAzLjI0N3YtNzIuMDcyYzE0LjUxNyA1LjU1NyAyNC44MjMgMTkuNTgzIDI0Ljg0NyAzNi4wMzMtLjAyNCAxNi40NTQtMTAuMzMgMzAuNDcxLTI0Ljg0NyAzNi4wMzhtLTUyLjUyMi0zNi4wMzhjLjAzMy0xNi40NDEgMTAuMzIyLTMwLjQ1OCAyNC44MzEtMzYuMDMydjcyLjA1NWMtMTQuNTA5LTUuNTY5LTI0Ljc5OC0xOS41NzgtMjQuODMxLTM2LjAyNG0zOC42NzktNjAuOTE1Yy0zMy43MDIuMDExLTYxLjAxMSAyNy4yNzMtNjEuMDIgNjAuOTE1LjAwOCAzMy42MzkgMjcuMzE4IDYwLjg5NSA2MS4wMiA2MC45MDUgMzMuNzEzLS4wMSA2MS4wMjgtMjcuMjY2IDYxLjAzMy02MC45MDUtLjAwNS0zMy42NDItMjcuMzE5LTYwLjkwNC02MS4wMzMtNjAuOTE1IiBmaWxsPSIjZmZmIi8+PHBhdGggZD0iTTE5MC4zNzMgMjkuNDIxYzAtNi4yOTQtMy4yOTEtNS44ODEtNi40NDQtNS45NDd2LTEuODE5YzIuNzMzLjEzMyA1LjUzNi4xMzMgOC4yNzUuMTMzIDIuOTQzIDAgNi45NDEtLjEzMyAxMi4xMzMtLjEzMyAxOC4xNTcgMCAyOC4wNDIgMTIuMTAzIDI4LjA0MiAyNC40OTYgMCA2LjkyOS00LjA2MyAyNC4zNTMtMjguODg4IDI0LjM1My0zLjU3MyAwLTYuODc0LS4xMzgtMTAuMTY3LS4xMzgtMy4xNTMgMC02LjI0Mi4wNjctOS4zOTUuMTM4di0xLjgyYzQuMjAzLS40MjEgNi4yNDItLjU2MSA2LjQ0NC01LjMydi0zMy45NDN6bTYuODczIDMyLjgyNWMwIDUuMzg4IDMuODYxIDYuMDE3IDcuMjk0IDYuMDE3IDE1LjE0OSAwIDIwLjEyNC0xMS40MDcgMjAuMTI0LTIxLjgzNiAwLTEzLjA4My04LjQxNS0yMi41My0yMS45NDctMjIuNTMtMi44NzkgMC00LjIwMy4yMDUtNS40NzIuMjh2MzguMDY4em0zNy41OTIgNi40NGgxLjMzMWMxLjk2MSAwIDMuMzY1IDAgMy4zNjUtMi4zMTV2LTE4Ljk2MmMwLTMuMDc3LTEuMDUtMy41MDQtMy42NDgtNC44OTZ2LTEuMTIyYzMuMjk2LS45ODUgNy4yMjYtMi4zMDggNy41MDMtMi41MTguNDkzLS4yOC45MTItLjM1NCAxLjI2NC0uMzU0LjM0NyAwIC40OTIuNDIxLjQ5Mi45ODV2MjYuODY2YzAgMi4zMTUgMS41NDIgMi4zMTUgMy41MDggMi4zMTVoMS4xODl2MS44MmMtMi4zODYgMC00Ljg0Mi0uMTQtNy4zNi0uMTQtMi41MjYgMC01LjA1Mi4wNjgtNy42NDMuMTR2LTEuODJ6bTcuNTAzLTQwLjk0Yy0xLjgyNyAwLTMuNDM2LTEuNjc5LTMuNDM2LTMuNDk5IDAtMS43NTIgMS42ODYtMy4zNjYgMy40MzYtMy4zNjYgMS44MTcgMCAzLjQzNSAxLjQ3NiAzLjQzNSAzLjM2NiAwIDEuODkyLTEuNTQ2IDMuNDk5LTMuNDM1IDMuNDk5em0xNC4xNjIgMjAuMDgyYzAtMi41ODgtLjc3NS0zLjI4NS00LjA2My00LjYxNXYtMS4zM2MzLjAxMS0uOTc4IDUuODg0LTEuODkyIDkuMjU0LTMuMzYzLjIwOCAwIC40MTQuMTQzLjQxNC42OTl2NC41NTFjNC4wMDMtMi44NjYgNy40NDEtNS4yNSAxMi4xNDQtNS4yNSA1Ljk1NiAwIDguMDU4IDQuMzQgOC4wNTggOS44djE4LjA1MmMwIDIuMzE1IDEuNTQxIDIuMzE1IDMuNTA2IDIuMzE1aDEuMjYydjEuODJjLTIuNDU5IDAtNC45MTMtLjE0LTcuNDMxLS4xNC0yLjUyOCAwLTUuMDUzLjA2OC03LjU3Ni4xNHYtMS44MmgxLjI2MWMxLjk2NyAwIDMuMzYzIDAgMy4zNjMtMi4zMTV2LTE4LjEyYzAtMy45OTQtMi40NDMtNS45NDktNi40NDYtNS45NDktMi4yNDMgMC01LjgxOSAxLjgxNy04LjE0MSAzLjM1OHYyMC43MTFjMCAyLjMxNSAxLjU1MiAyLjMxNSAzLjUxNiAyLjMxNWgxLjI2MXYxLjgyYy0yLjQ1NCAwLTQuOTEyLS4xNC03LjQzNi0uMTQtMi41MjEgMC01LjA1LjA2OC03LjU3MS4xNHYtMS44MmgxLjI2NWMxLjk2MiAwIDMuMzYxIDAgMy4zNjEtMi4zMTV2LTE4LjU0NHptMzUuOTA3IDMuMzY1Yy0uMTQ1LjYzMS0uMTQ1IDEuNjc3IDAgNC4wNTkuNDEyIDYuNjQ2IDQuNzAyIDEyLjEwMyAxMC4zMDIgMTIuMTAzIDMuODYzIDAgNi44ODItMi4xIDkuNDctNC42ODZsLjk4Ljk4Yy0zLjIyNiA0LjI3LTcuMjI0IDcuOTA4LTEyLjk2OCA3LjkwOC0xMS4xNTQgMC0xMy4zOTUtMTAuNzgtMTMuMzk1LTE1LjI1NiAwLTEzLjcxOCA5LjI1Mi0xNy43NzkgMTQuMTU3LTE3Ljc3OSA1LjY4NSAwIDExLjc5MSAzLjU2OCAxMS44NTMgMTAuOTg2IDAgLjQyNiAwIC44NC0uMDYyIDEuMjY1bC0uNjM3LjQyMWgtMTkuNzAxem0xMi40MTMtMi4yNDFjMS43NTIgMCAxLjk1Ni0uOTEyIDEuOTU2LTEuNzUxIDAtMy41NjgtMi4xNzEtNi40MzctNi4xMDEtNi40MzctNC4yNzUgMC03LjIxOSAzLjE0Mi04LjA2IDguMTg3aDEyLjIwNXptOS42MDQgMTkuNzM2aDEuODk0YzEuOTU2IDAgMy4zNiAwIDMuMzYtMi4zMTV2LTE5LjY2MWMwLTIuMTY4LTIuNTkxLTIuNTkxLTMuNjQ1LTMuMTV2LTEuMDQ3YzUuMTE5LTIuMTcyIDcuOTI1LTMuOTk0IDguNTY1LTMuOTk0LjQxMiAwIC42Mi4yMS42Mi45MTN2Ni4yOTdoLjE1YzEuNzQ2LTIuNzI5IDQuNjk3LTcuMjEgOC45NzItNy4yMSAxLjc1NCAwIDMuOTk1IDEuMTg4IDMuOTk1IDMuNzA5IDAgMS44OS0xLjMyOSAzLjU3NC0zLjI5MSAzLjU3NC0yLjE4MiAwLTIuMTgyLTEuNjg0LTQuNjMzLTEuNjg0LTEuMTk0IDAtNS4xMTkgMS42MTEtNS4xMTkgNS44MTJ2MTYuNDM5YzAgMi4zMTUgMS4zOTkgMi4zMTUgMy4zNjUgMi4zMTVoMy45MjV2MS44MmMtMy44NTgtLjA3Mi02Ljc5My0uMTQtOS44MTItLjE0LTIuODc0IDAtNS44MjEuMDY4LTguMzQ1LjE0di0xLjgyem0yNi45OTUtNy45MTFjLjkxNCA0LjYxOCAzLjcxNiA4LjU0IDguODM4IDguNTQgNC4xMyAwIDUuNjcyLTIuNTE5IDUuNjcyLTQuOTY3IDAtOC4yNi0xNS4yODEtNS42MDMtMTUuMjgxLTE2Ljg2NyAwLTMuOTIyIDMuMTU5LTguOTYxIDEwLjg2OS04Ljk2MSAyLjI0MSAwIDUuMjU1LjYzMiA3Ljk4OCAyLjAzM2wuNDk1IDcuMTMyaC0xLjYxNGMtLjctNC40MDUtMy4xNTMtNi45MjQtNy42NDMtNi45MjQtMi44MDggMC01LjQ2OSAxLjYwOS01LjQ2OSA0LjYxOCAwIDguMTk0IDE2LjI2OCA1LjY2OSAxNi4yNjggMTYuNjU1IDAgNC42MTgtMy43MTYgOS41MjItMTIuMDYzIDkuNTIyLTIuODA0IDAtNi4xMDEtLjk4Mi04LjU1Mi0yLjM4bC0uNzc0LTguMDQ5IDEuMjY0LS4zNTN6bTgzLjQ0Mi0yNi40NTVoLTEuNzQ5Yy0xLjMzNS04LjE4Mi03LjE1Ni0xMS40NzUtMTUuMDA2LTExLjQ3NS04LjA2OCAwLTE5Ljc4MSA1LjM4Ny0xOS43ODEgMjIuMTgyIDAgMTQuMTQ0IDEwLjEwNSAyNC4yODggMjAuOSAyNC4yODggNi45MzkgMCAxMi42OTUtNC43NTggMTQuMDk5LTEyLjEwOWwxLjYwOS40MTktMS42MDkgMTAuMjE3Yy0yLjk0NiAxLjgyNS0xMC44NzEgMy43MTItMTUuNTAyIDMuNzEyLTE2LjQwMyAwLTI2Ljc3OS0xMC41NjgtMjYuNzc5LTI2LjMxNSAwLTE0LjM0NyAxMi44MjgtMjQuNjM4IDI2LjU3Mi0yNC42MzggNS42NzcgMCAxMS4xNDkgMS44MjUgMTYuNTQ2IDMuNzE2bC43IDEwLjAwM3ptMi41MjkgMzQuMzY2aDEuMzI2YzEuOTY5IDAgMy4zNzMgMCAzLjM3My0yLjMxNXYtMzguOTc0YzAtNC41NTMtMS4wNTItNC42OTMtMy43MTYtNS40NjF2LTEuMTJjMi44MDMtLjkwOSA1Ljc0OS0yLjE2OCA3LjIyMS0zLjAxMi43NjUtLjQxNiAxLjMzMS0uNzcyIDEuNTM3LS43NzIuNDI3IDAgLjU2OC40MjQuNTY4Ljk4NXY0OC4zNTRjMCAyLjMxNSAxLjU0MSAyLjMxNSAzLjUwNiAyLjMxNWgxLjE4NHYxLjgyYy0yLjM3NiAwLTQuODMyLS4xNC03LjM1Ni0uMTQtMi41MjMgMC01LjA0Ny4wNjgtNy42NDMuMTR2LTEuODJ6bTQ1LjAyMi0yLjAzM2MwIDEuMjY1Ljc2OSAxLjMzMSAxLjk1OSAxLjMzMWwyLjgwNi0uMDY3djEuNDczYy0zLjAxOS4yNzgtOC43NjcgMS43NDctMTAuMSAyLjE2N2wtLjM1My0uMjExdi01LjY2NmMtNC4yIDMuNDI4LTcuNDMxIDUuODc3LTEyLjQxNSA1Ljg3Ny0zLjc4MyAwLTcuNzEtMi40NDktNy43MS04LjMyNXYtMTcuOTJjMC0xLjgyLS4yNzgtMy41NzEtNC4yMDItMy45MTl2LTEuMzMxYzIuNTI4LS4wNjggOC4xMjgtLjQ4OCA5LjA0My0uNDg4Ljc3NyAwIC43NzcuNDg4Ljc3NyAyLjAyOHYxOC4wNTJjMCAyLjEwMyAwIDguMTIgNi4wOTYgOC4xMiAyLjM4NCAwIDUuNTM5LTEuODE5IDguNDgtNC4yNjN2LTE4LjgzM2MwLTEuMzk2LTMuMzYzLTIuMTYzLTUuODgyLTIuODY0di0xLjI2YzYuMzA0LS40MjMgMTAuMjM3LS45OCAxMC45MzQtLjk4LjU2NyAwIC41NjcuNDg4LjU2NyAxLjI2MXYyNS44MTh6bTEzLjk1Mi0yMy4wOTJjMi44MDEtMi4zOCA2LjU4OS01LjA0IDEwLjQ0OS01LjA0IDguMTM1IDAgMTMuMDM3IDcuMDc0IDEzLjAzNyAxNC42OTggMCA5LjE2Ny02LjcyOCAxOC4zMzgtMTYuNzYxIDE4LjMzOC01LjE4NCAwLTcuOTE4LTEuNjgyLTkuNzQ0LTIuNDQ5bC0yLjEwMSAxLjYwNi0xLjQ2Ny0uNzY3Yy42Mi00LjEyOS45NzktOC4xODkuOTc5LTEyLjQ1N3YtMzAuMDkxYzAtNC41NTMtMS4wNTctNC42OTMtMy43Mi01LjQ2MXYtMS4xMmMyLjgxMS0uOTA5IDUuNzQ5LTIuMTY4IDcuMjI0LTMuMDEyLjc3LS40MTYgMS4zMjctLjc3MiAxLjU0Ni0uNzcyLjQyIDAgLjU1OS40MjQuNTU5Ljk4NXYyNS41NDN6bTAgMTkuMDMzYzAgMi42NTkgMi41MjMgNy4xNDQgNy4yMjEgNy4xNDQgNy41MDQgMCAxMC42NTctNy4zNTIgMTAuNjU3LTEzLjU4MyAwLTcuNTU2LTUuNzQ0LTEzLjg1Mi0xMS4yMTYtMTMuODUyLTIuNjAxIDAtNC43NyAxLjY4MS02LjY2MyAzLjI5MXYxN3ptLTMwMi41MTggNDguNjAxaC41MzdjMS4zNzEgMCAyLjgyMS0uMTg1IDIuODIxLTIuMTY4di0xOS45NDhjMC0xLjk4Ni0xLjQ0OS0yLjE3NS0yLjgyMS0yLjE3NWgtLjUzN3YtMS4xNDNjMS40ODkgMCAzLjc3OC4xNSA1LjY1My4xNSAxLjkwNSAwIDQuMTkyLS4xNSA1Ljk4NS0uMTV2MS4xNDNoLS41MzZjLTEuMzY4IDAtMi44MjEuMTg5LTIuODIxIDIuMTc1djE5Ljk0OGMwIDEuOTgzIDEuNDUzIDIuMTY4IDIuODIxIDIuMTY4aC41MzZ2MS4xNDhjLTEuODMxIDAtNC4xMjUtLjE1LTYuMDI2LS4xNS0xLjg3MSAwLTQuMTI0LjE1LTUuNjEyLjE1di0xLjE0OHptMzcuODUxLTYuMzU5bC4wNzctLjA3NXYtMTQuMjc3YzAtMy4xMjQtMi4xNzctMy41ODEtMy4zMjEtMy41ODFoLS44NHYtMS4xNDNsNS4zNDIuMTUxIDQuNjk0LS4xNTF2MS4xNDNoLS41NjhjLTEuNjA3IDAtMy40LjMwNi0zLjQgNC44MzZ2MTcuMzNjMCAxLjMzMS4wMzkgMi42NjIuMjI2IDMuODQyaC0xLjQ0OWwtMTkuNjQ3LTIxLjg2djE1LjY5MmMwIDMuMzEzLjY0MyA0LjQ1MiAzLjU4NSA0LjQ1MmguNjQ3djEuMTQ4bC00LjkyLS4xNS01LjE4Ny4xNXYtMS4xNDhoLjUzNGMyLjYzMiAwIDMuNDMxLTEuNzg2IDMuNDMxLTQuODI5di0xNi4wMzljMC0yLjEyOS0xLjc1OS0zLjQyMy0zLjQ2OS0zLjQyM2gtLjQ5N3YtMS4xNDNsNC4zODMuMTUxIDMuMzk5LS4xNTEgMTYuOTgzIDE5LjA3NnptMTEuNjY2LTE3LjE3MmMtMi44NjYgMC0yLjk3NC42ODYtMy41NDcgMy40NjJoLTEuMTQzYy4xNDgtMS4wNjUuMzQyLTIuMTI5LjQ1OC0zLjIzNi4xNTUtMS4wNjguMjI4LTIuMTI5LjIyOC0zLjIzM2guOTE1Yy4zMDggMS4xNDQgMS4yNjEgMS4xMDQgMi4yOTYgMS4xMDRoMTkuNjgxYzEuMDMzIDAgMS45ODQtLjAzNyAyLjA2My0xLjE4MmwuOTEuMTUzYy0uMTQ4IDEuMDI5LS4zMDMgMi4wNTUtLjQxOSAzLjA4NS0uMDcgMS4wMjktLjA3IDIuMDU4LS4wNyAzLjA4M2wtMS4xNDcuNDIzYy0uMDc4LTEuNDA4LS4yNy0zLjY1OS0yLjgyNC0zLjY1OWgtNi4yNTd2MjAuMjU5YzAgMi45MzcgMS4zMzcgMy4yNzIgMy4xNjQgMy4yNzJoLjcyNXYxLjE0OGMtMS40ODcgMC00LjE1OS0uMTUtNi4yMTYtLjE1LTIuMjkgMC00Ljk2Mi4xNS02LjQ1Mi4xNXYtMS4xNDhoLjcyNWMyLjEwNiAwIDMuMTY4LS4xODcgMy4xNjgtMy4xOTJ2LTIwLjMzOWgtNi4yNTl6bTIzLjA4MiAyMy41MzFoLjUzNmMxLjM3MyAwIDIuODI0LS4xODUgMi44MjQtMi4xNjh2LTE5Ljk0OGMwLTEuOTg2LTEuNDUxLTIuMTc1LTIuODI0LTIuMTc1aC0uNTM2di0xLjE0M2MyLjMyMiAwIDYuMjk5LjE1IDkuNDk4LjE1IDMuMjEgMCA3LjE3Mi0uMTUgOS43NjUtLjE1LS4wNjUgMS42MzUtLjAyOCA0LjE1My4wODMgNS44MjdsLTEuMTUuMzA2Yy0uMTg0LTIuNDc0LS42NC00LjQ1NC00LjY1LTQuNDU0aC01LjI5OHY5Ljk3M2g0LjUzNGMyLjI4OCAwIDIuNzg4LTEuMjkxIDMuMDE0LTMuMzUyaDEuMTQyYy0uMDc1IDEuNDg5LS4xMTQgMi45NzMtLjExNCA0LjQ1NSAwIDEuNDUxLjAzOSAyLjg5Ni4xMTQgNC4zNDNsLTEuMTQyLjIyNmMtLjIyNi0yLjI4Mi0uMzQyLTMuNzY4LTIuOTc5LTMuNzY4aC00LjU3djguODcxYzAgMi40NzUgMi4yMDMgMi40NzUgNC42NDYgMi40NzUgNC41ODEgMCA2LjYwMS0uMzA3IDcuNzQ3LTQuNjQybDEuMDY3LjI2MmMtLjQ5OCAyLjAyNC0uOTU0IDQuMDM2LTEuMjkzIDYuMDU4LTIuNDQ2IDAtNi44MzQtLjE1LTEwLjI2My0uMTUtMy40MzggMC03Ljk3OC4xNS0xMC4xNTMuMTV2LTEuMTQ4em0yNi44NTktMjEuNGMwLTIuNzc4LTEuNTI2LTIuODkyLTIuNzEyLTIuODkyaC0uNjg3di0xLjE0NGMxLjIyMSAwIDMuNTg2LjE1MyA1LjkxNS4xNTMgMi4yODcgMCA0LjEyLS4xNTMgNi4xNDEtLjE1MyA0LjgwNCAwIDkuMDgzIDEuMjk0IDkuMDgzIDYuNzA2IDAgMy40MjEtMi4yODggNS41MTctNS4zMDMgNi43MDNsNi41MjIgOS43NDFjMS4wNyAxLjYxMSAxLjgyOSAyLjA2MyAzLjcwMiAyLjI4NHYxLjE0OWwtMy43MzktLjE1LTMuNTg0LjE1Yy0yLjkzOC0zLjg0Ni01LjQ2Mi03Ljk1OC03LjkzOS0xMi4zNDNoLTIuNTEzdjguMTUzYzAgMi45MzUgMS4zNyAzLjA0MSAzLjEyMiAzLjA0MWguNjg5djEuMTQ5bC02LjU2Mi0uMTVjLTEuODM0IDAtMy42MjcuMTUtNS41MzQuMTV2LTEuMTQ5aC42ODdjMS40MTcgMCAyLjcxMi0uNjM5IDIuNzEyLTIuMDV2LTE5LjM0OXptNC44ODYgOC44MzJoMS44NjNjMy44MTkgMCA1Ljg3NC0xLjQ0MyA1Ljg3NC01LjkzOSAwLTMuMzg0LTIuMTczLTUuNTU2LTUuNTcxLTUuNTU2LTEuMTQ3IDAtMS42MzUuMTE3LTIuMTY2LjE1MXYxMS4zNDR6bTQzLjY4IDYuMjA5bC4wNjgtLjA3NXYtMTQuMjc3YzAtMy4xMjQtMi4xNzEtMy41ODEtMy4zMTQtMy41ODFoLS44Mzd2LTEuMTQzbDUuMzQuMTUxIDQuNy0uMTUxdjEuMTQzaC0uNTc3Yy0xLjYwMSAwLTMuMzk2LjMwNi0zLjM5NiA0LjgzNnYxNy4zM2MwIDEuMzMxLjAzNiAyLjY2Mi4yMjUgMy44NDJoLTEuNDQ2bC0xOS42NDktMjEuODZ2MTUuNjkyYzAgMy4zMTMuNjQ3IDQuNDUyIDMuNTg0IDQuNDUyaC42NDh2MS4xNDhsLTQuOTE4LS4xNS01LjE5Mi4xNXYtMS4xNDhoLjUyOWMyLjYzNyAwIDMuNDM4LTEuNzg2IDMuNDM4LTQuODI5di0xNi4wMzljMC0yLjEyOS0xLjc1Ni0zLjQyMy0zLjQ3LTMuNDIzaC0uNDk3di0xLjE0M2w0LjM4OS4xNTEgMy4zOTQtLjE1MSAxNi45OCAxOS4wNzZ6bTEyLjA5MiAyLjA2MWMtLjM4OCAxLjI5Mi0uODQ3IDIuMjg5LS44NDcgMi45NjggMCAxLjE0NCAxLjYwNCAxLjMzIDIuODYgMS4zM2guNDI3djEuMTQ4Yy0xLjUyOS0uMDgzLTMuMDg4LS4xNDgtNC42MjUtLjE0OC0xLjM3MSAwLTIuNzM2LjA2NS00LjExNS4xNDh2LTEuMTQ4aC4yMzFjMS40ODcgMCAyLjc0OS0uODc0IDMuMzEzLTIuNDc3bDYuMTEyLTE3LjQ3MWMuNDktMS40MDkgMS4xODQtMy4zMTQgMS40MTQtNC43MjggMS4yMTUtLjQxMyAyLjc0NC0xLjE3NCAzLjQ2OS0xLjYzMi4xMTctLjA0MS4xODQtLjA4LjMwNS0uMDguMTE0IDAgLjE4NCAwIC4yNy4xMTcuMTE0LjMwNC4yMjEuNjQ3LjM0NS45NTFsNy4wMTkgMTkuOTE4Yy40NTYgMS4zMzEuOTEgMi43MzkgMS40MDQgMy44ODYuNDYzIDEuMDY2IDEuMjY0IDEuNTE2IDIuNTI0IDEuNTE2aC4yMjh2MS4xNDhjLTEuNzE3LS4wODMtMy40MzYtLjE0OC01LjI2NC0uMTQ4LTEuODY4IDAtMy43ODIuMDY1LTUuNzI4LjE0OHYtMS4xNDhoLjQyM2MuODczIDAgMi4zNjYtLjE0OCAyLjM2Ni0xLjEwMiAwLS40OTItLjM0NC0xLjUyLS43Ny0yLjc0MmwtMS40ODQtNC40MTZoLTguNjYxbC0xLjIxNyAzLjk2M3ptNS41NjgtMTYuODcyaC0uMDc4bC0zLjU0OSAxMC43NzhoNy4xM2wtMy41MDMtMTAuNzc4em0xNi40NDQtMi4zNjJjLTIuODYzIDAtMi45NzcuNjg2LTMuNTUgMy40NjJoLTEuMTQ3Yy4xNS0xLjA2NS4zNDQtMi4xMjkuNDYzLTMuMjM2LjE1MS0xLjA2OC4yMjMtMi4xMjkuMjIzLTMuMjMzaC45MmMuMzAzIDEuMTQ0IDEuMjYxIDEuMTA0IDIuMjg3IDEuMTA0aDE5LjY5MWMxLjAyNiAwIDEuOTc5LS4wMzcgMi4wNTctMS4xODJsLjkxNC4xNTNjLS4xNDcgMS4wMjktLjMgMi4wNTUtLjQxNCAzLjA4NS0uMDg1IDEuMDI5LS4wODUgMi4wNTgtLjA4NSAzLjA4M2wtMS4xNDIuNDIzYy0uMDctMS40MDgtLjI2Mi0zLjY1OS0yLjgxOS0zLjY1OWgtNi4yNTl2MjAuMjU5YzAgMi45MzcgMS4zMzkgMy4yNzIgMy4xNjYgMy4yNzJoLjcyNnYxLjE0OGMtMS40ODkgMC00LjE1OC0uMTUtNi4yMi0uMTUtMi4yODUgMC00Ljk2Mi4xNS02LjQ0OS4xNXYtMS4xNDhoLjcyNWMyLjEwMiAwIDMuMTY5LS4xODcgMy4xNjktMy4xOTJ2LTIwLjMzOWgtNi4yNTd6bTIzLjI2NCAyMy41MzFoLjUzNGMxLjM3OCAwIDIuODIxLS4xODUgMi44MjEtMi4xNjh2LTE5Ljk0OGMwLTEuOTg2LTEuNDQzLTIuMTc1LTIuODIxLTIuMTc1aC0uNTM0di0xLjE0M2MxLjQ5MiAwIDMuNzc3LjE1IDUuNjQzLjE1IDEuOTE0IDAgNC4yMDItLjE1IDUuOTk4LS4xNXYxLjE0M2gtLjUzNGMtMS4zNzYgMC0yLjgyOS4xODktMi44MjkgMi4xNzV2MTkuOTQ4YzAgMS45ODMgMS40NTMgMi4xNjggMi44MjkgMi4xNjhoLjUzNHYxLjE0OGMtMS44MzQgMC00LjEyNC0uMTUtNi4wMjktLjE1LTEuODczIDAtNC4xMi4xNS01LjYxMi4xNXYtMS4xNDh6bTI2Ljg1Ny0yNi4wMDNjOC4xMzUgMCAxNC42MTYgNS4wMjkgMTQuNjE2IDEzLjE0IDAgOC43NTktNi4yOTYgMTQuNTgxLTE0LjQxOSAxNC41ODEtOC4wOTYgMC0xNC4yNzMtNS40ODItMTQuMjczLTEzLjY3IDAtNy45MTkgNi4xNDEtMTQuMDUyIDE0LjA3Ni0xNC4wNTJ6bS41NzcgMjYuMDQ1YzcuNDA0IDAgOC42OTItNi41MTYgOC42OTItMTIuMDY3IDAtNS41NjktMy4wMDUtMTIuMzA2LTkuMzQyLTEyLjMwNi02LjY3OSAwLTguNjYxIDUuOTQyLTguNjYxIDExLjA0IDAgNi44MTcgMy4xMjggMTMuMzMzIDkuMzExIDEzLjMzM3ptMzkuNzUtNi40MDFsLjA3OC0uMDc1di0xNC4yNzdjMC0zLjEyNC0yLjE4MS0zLjU4MS0zLjMyNi0zLjU4MWgtLjgyN3YtMS4xNDNsNS4zMy4xNTEgNC43MDItLjE1MXYxLjE0M2gtLjU3N2MtMS41OTkgMC0zLjM5Ni4zMDYtMy4zOTYgNC44MzZ2MTcuMzNjMCAxLjMzMS4wMzYgMi42NjIuMjMgMy44NDJoLTEuNDQ4bC0xOS42NTQtMjEuODZ2MTUuNjkyYzAgMy4zMTMuNjUgNC40NTIgMy41ODQgNC40NTJoLjY1MXYxLjE0OGwtNC45Mi0uMTUtNS4xOTUuMTV2LTEuMTQ4aC41MzljMi42MzcgMCAzLjQzLTEuNzg2IDMuNDMtNC44Mjl2LTE2LjAzOWMwLTIuMTI5LTEuNzQ2LTMuNDIzLTMuNDctMy40MjNoLS40OTh2LTEuMTQzbDQuMzg5LjE1MSAzLjM5Ni0uMTUxIDE2Ljk4MSAxOS4wNzZ6bTEyLjA4NyAyLjA2MWMtLjM3NSAxLjI5Mi0uODM3IDIuMjg5LS44MzcgMi45NjggMCAxLjE0NCAxLjYwNyAxLjMzIDIuODU4IDEuMzNoLjQyN3YxLjE0OGMtMS41MjgtLjA4My0zLjA5NC0uMTQ4LTQuNjItLjE0OC0xLjM3OCAwLTIuNzQ2LjA2NS00LjExNC4xNDh2LTEuMTQ4aC4yMTdjMS40OTMgMCAyLjc1Ny0uODc0IDMuMzIxLTIuNDc3bDYuMTE0LTE3LjQ3MWMuNDkzLTEuNDA5IDEuMTg0LTMuMzE0IDEuNDA1LTQuNzI4IDEuMjIzLS40MTMgMi43NDctMS4xNzQgMy40NzktMS42MzIuMTA5LS4wNDEuMTg2LS4wOC4zMDEtLjA4LjExNCAwIC4xODcgMCAuMjY0LjExN2wuMzQ5Ljk1MSA3LjAxNiAxOS45MThjLjQ1OCAxLjMzMS45MTQgMi43MzkgMS40MTUgMy44ODYuNDU4IDEuMDY2IDEuMjYyIDEuNTE2IDIuNTE4IDEuNTE2aC4yMzN2MS4xNDhjLTEuNzItLjA4My0zLjQzOC0uMTQ4LTUuMjcyLS4xNDgtMS44NjUgMC0zLjc3Ny4wNjUtNS43MjMuMTQ4di0xLjE0OGguNDIyYy44NzEgMCAyLjM3LS4xNDggMi4zNy0xLjEwMiAwLS40OTItLjM0Ny0xLjUyLS43NzItMi43NDJsLTEuNDgyLTQuNDE2aC04LjY2MmwtMS4yMjUgMy45NjN6bTUuNTc2LTE2Ljg3MmgtLjA3N2wtMy41NTQgMTAuNzc4aDcuMTQxbC0zLjUxMS0xMC43Nzh6bTI0LjM4MSAxOC41ODNjMCAxLjUyNiAxLjA2IDEuOTggMi4yODIgMi4xMzYgMS41NjUuMTE1IDMuMjgyLjExNSA1LjA0Mi0uMDc4IDEuNTk5LS4xOTIgMi45NzItMS4xMDQgMy42NTgtMi4wNTguNjA5LS44MzkuOTUxLTEuOTAzIDEuMTg0LTIuNzQyaDEuMTA2Yy0uNDE5IDIuMTctLjk1NCA0LjMwNy0xLjQxNSA2LjQ3NmwtMTAuMDY2LS4xNDgtMTAuMDc0LjE0OHYtMS4xNDhoLjUyNmMxLjM3OSAwIDIuODcxLS4xODUgMi44NzEtMi41NDl2LTE5LjU3YzAtMS45ODUtMS40OTItMi4xNzMtMi44NzEtMi4xNzNoLS41MjZ2LTEuMTQzbDYuMDI0LjE1MSA1LjgwNC0uMTUxdjEuMTQzaC0uOTU5Yy0xLjQ0NiAwLTIuNTg1LjA0Mi0yLjU4NSAyLjA1NnYxOS42NDl6IiBmaWxsPSIjMUExOTE4Ii8+PC9zdmc+");
}
.rccs__card--discover > div {
color: #555;
}
.rccs__card--discover .rccs__card__background {
background: linear-gradient(25deg, #fff, #eee);
}
.rccs__card--discover .rccs__issuer {
background-image: url("data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNTEyIiBoZWlnaHQ9Ijg2IiB2aWV3Qm94PSIwIDAgNTEyIDg2IiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHhtbG5zOnhsaW5rPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5L3hsaW5rIiBwcmVzZXJ2ZUFzcGVjdFJhdGlvPSJ4TWlkWU1pZCI+PGRlZnM+PGxpbmVhckdyYWRpZW50IHgxPSIyMC40NDIlIiB5MT0iMTAuNTk5JSIgeDI9Ijg5LjI0NSUiIHkyPSI4My41MyUiIGlkPSJhIj48c3RvcCBzdG9wLWNvbG9yPSIjRTI1NDI5IiBvZmZzZXQ9IjAlIi8+PHN0b3Agc3RvcC1jb2xvcj0iI0Y5OUQzRSIgb2Zmc2V0PSIxMDAlIi8+PC9saW5lYXJHcmFkaWVudD48cGF0aCBkPSJNMjcwLjM1Ni4zNjVjLTIzLjk4MiAwLTQzLjQ0IDE4LjczNS00My40NCA0MS44NTggMCAyNC41ODMgMTguNjEyIDQyLjk2IDQzLjQ0IDQyLjk2IDI0LjIwOCAwIDQzLjMyMS0xOC42MiA0My4zMjEtNDIuNDc4IDAtMjMuNzE2LTE4Ljk4Ni00Mi4zNC00My4zMjEtNDIuMzR6IiBpZD0iYiIvPjxmaWx0ZXIgeD0iLTUwJSIgeT0iLTUwJSIgd2lkdGg9IjIwMCUiIGhlaWdodD0iMjAwJSIgZmlsdGVyVW5pdHM9Im9iamVjdEJvdW5kaW5nQm94IiBpZD0iYyI+PGZlTW9ycGhvbG9neSByYWRpdXM9IjIiIGluPSJTb3VyY2VBbHBoYSIgcmVzdWx0PSJzaGFkb3dTcHJlYWRJbm5lcjEiLz48ZmVHYXVzc2lhbkJsdXIgc3RkRGV2aWF0aW9uPSIyLjUiIGluPSJzaGFkb3dTcHJlYWRJbm5lcjEiIHJlc3VsdD0ic2hhZG93Qmx1cklubmVyMSIvPjxmZU9mZnNldCBkeD0iMiIgZHk9IjIiIGluPSJzaGFkb3dCbHVySW5uZXIxIiByZXN1bHQ9InNoYWRvd09mZnNldElubmVyMSIvPjxmZUNvbXBvc2l0ZSBpbj0ic2hhZG93T2Zmc2V0SW5uZXIxIiBpbjI9IlNvdXJjZUFscGhhIiBvcGVyYXRvcj0iYXJpdGhtZXRpYyIgazI9Ii0xIiBrMz0iMSIgcmVzdWx0PSJzaGFkb3dJbm5lcklubmVyMSIvPjxmZUNvbG9yTWF0cml4IHZhbHVlcz0iMCAwIDAgMCAwIDAgMCAwIDAgMCAwIDAgMCAwIDAgMCAwIDAgMC4zMDE2NTg3NDEgMCIgaW49InNoYWRvd0lubmVySW5uZXIxIi8+PC9maWx0ZXI+PC9kZWZzPjx1c2UgZmlsbD0idXJsKCNhKSIgZmlsbC1ydWxlPSJldmVub2RkIiB4bGluazpocmVmPSIjYiIvPjx1c2UgZmlsdGVyPSJ1cmwoI2MpIiB4bGluazpocmVmPSIjYiIvPjxwYXRoIGQ9Ik0yMy43NDYgMS44OTFoLTIzLjM1M3Y4MS40NTRoMjMuMjMyYzEyLjMyNSAwIDIxLjI0LTIuOTIxIDI5LjA1OS05LjM5OCA5LjI3OC03LjY5NSAxNC43ODEtMTkuMjk4IDE0Ljc4MS0zMS4yODkgMC0yNC4wNDgtMTcuOTY1LTQwLjc2Ni00My43MTktNDAuNzY2em0xOC41NzMgNjEuMTc2Yy01LjAyMiA0LjUzMS0xMS40ODYgNi40ODgtMjEuNzYgNi40ODhoLTQuMjY4di01My44NzNoNC4yNjhjMTAuMjc0IDAgMTYuNDkxIDEuODM0IDIxLjc2IDYuNTkzIDUuNDk1IDQuODg2IDguNzcyIDEyLjQ1MiA4Ljc3MiAyMC4yNjUgMCA3LjgyOS0zLjI3NyAxNS42Ni04Ljc3MiAyMC41Mjd6bTMyLjQ4IDIwLjI3OGgxNS44NzF2LTgxLjQ1NGgtMTUuODcxdjgxLjQ1NHptNTQuNzI3LTUwLjIwOWMtOS41MzktMy41MzQtMTIuMzQ2LTUuODY1LTEyLjM0Ni0xMC4yNDcgMC01LjEzNCA0Ljk5OC05LjAzOSAxMS44NDktOS4wMzkgNC43NjMgMCA4LjY3MSAxLjk1MyAxMi44MzYgNi41OGw4LjI5NC0xMC44NTJjLTYuODM5LTUuOTk4LTE1LjAyMS05LjA0Ny0yMy45NDYtOS4wNDctMTQuMzk4IDAtMjUuMzk5IDEwLjAyLTI1LjM5OSAyMy4zMiAwIDExLjI0NyA1LjEyNiAxNi45ODEgMjAuMDMxIDIyLjM2OSA2LjIzMyAyLjE4OCA5LjQwMSAzLjY0NiAxMC45OTMgNC42NDMgMy4xNzUgMi4wNzcgNC43NjkgNC45OTggNC43NjkgOC40MTYgMCA2LjYwNS01LjI1NyAxMS40ODMtMTIuMzUxIDExLjQ4My03LjU3NCAwLTEzLjY3NC0zLjc4Mi0xNy4zNDEtMTAuODY1bC0xMC4yNDcgOS45MDVjNy4zMTMgMTAuNzMzIDE2LjEwOSAxNS41MTEgMjguMjE0IDE1LjUxMSAxNi40ODggMCAyOC4wODQtMTEuMDA3IDI4LjA4NC0yNi43NTggMC0xMi45NDgtNS4zNjEtMTguODE1LTIzLjQ0My0yNS40MTl6bTI4LjQ0OSA5LjUyMWMwIDIzLjk2NSAxOC44MTUgNDIuNTI1IDQzLjAwNiA0Mi41MjUgNi44MzkgMCAxMi43MDEtMS4zNTIgMTkuOTE1LTQuNzU4di0xOC42OTdjLTYuMzYxIDYuMzU4LTExLjk4IDguOTE2LTE5LjE4OSA4LjkxNi0xNS45OTcgMC0yNy4zNjQtMTEuNjA2LTI3LjM2NC0yOC4xMDIgMC0xNS42MjYgMTEuNzIxLTI3Ljk2NSAyNi42MzgtMjcuOTY1IDcuNTYxIDAgMTMuMzExIDIuNjg2IDE5LjkxNSA5LjE1OXYtMTguNjk2Yy02Ljk2Mi0zLjUzMS0xMi43MTItNC45ODUtMTkuNTUyLTQuOTg1LTI0LjA2NyAwLTQzLjM2OSAxOC45MzUtNDMuMzY5IDQyLjYwNHptMTkxLjY1MSAxMy45NDhsLTIxLjc0My01NC43MTVoLTE3LjM0NmwzNC41NzkgODMuNTM0aDguNTQzbDM1LjE4My04My41MzRoLTE3LjIxOGwtMjIgNTQuNzE1em00Ni40MzkgMjYuNzM5aDQ1LjA2NnYtMTMuNzg5aC0yOS4xODh2LTIyLjAwMWgyOC4wNzJ2LTEzLjc5MmgtMjguMDcydi0xOC4wNzloMjkuMTg4di0xMy43OTJoLTQ1LjA2NnY4MS40NTR6bTEwNy45NTUtNTcuNDE1YzAtMTUuMjU5LTEwLjQ5LTI0LjAzOS0yOC44MjMtMjQuMDM5aC0yMy41Nzd2ODEuNDU0aDE1Ljg5NXYtMzIuNzM3aDIuMDhsMjEuOTc1IDMyLjczN2gxOS41NDRsLTI1LjY2Ny0zNC4zMTFjMTEuOTg4LTIuNDUxIDE4LjU3My0xMC42MzggMTguNTczLTIzLjEwNHptLTMxLjg4MiAxMy40NTJoLTQuNjIzdi0yNC42ODNoNC44NzdjOS45MTYgMCAxNS4yODcgNC4xNjUgMTUuMjg3IDEyLjA5MiAwIDguMTc4LTUuMzcyIDEyLjU5LTE1LjU0MSAxMi41OXoiIGZpbGw9IiMwQjEwMTUiLz48L3N2Zz4=");
}
.rccs__card--elo .rccs__card__background {
background: linear-gradient(25deg, #211c18, #aaa7a2);
}
.rccs__card--elo .rccs__issuer {
background-image: url("data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNTEyIiBoZWlnaHQ9IjUxMiIgdmlld0JveD0iMCAwIDUxMiA1MTIiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PGcgZmlsbC1ydWxlPSJldmVub2RkIj48cGF0aCBkPSJNMjU2IDBjMTQxLjM4NSAwIDI1NiAxMTQuNjE1IDI1NiAyNTYgMCAxNDEuMzg2LTExNC42MTUgMjU2LTI1NiAyNTZzLTI1Ni0xMTQuNjE0LTI1Ni0yNTZjMC0xNDEuMzg1IDExNC42MTUtMjU2IDI1Ni0yNTYiIGZpbGw9IiMwRTBFMTEiLz48cGF0aCBkPSJNMTgwLjA0MiAyMzcuNTgzbC03OC41MjQgMzMuODM3Yy0uMTIyLTEuMzUzLS4xODktMi43MjEtLjE4OS00LjEwNiAwLTI0LjgzOSAyMC4xMzUtNDQuOTc0IDQ0Ljk3NC00NC45NzQgMTMuNDM4IDAgMjUuNDk5IDUuODk4IDMzLjczOSAxNS4yNDN6bS0zMy43MzktNDguODc2YzM3LjA1MyAwIDY4LjExMiAyNS42MzggNzYuNDIgNjAuMTQzbC0zMS42ODIgMTMuODUzLS4wMDctLjA2Ni0zMi40MTMgMTQuMjQxLTc3Ljc1NSAzMy45OTdjLTguMzE4LTEyLjQ3LTEzLjE3LTI3LjQ0OC0xMy4xNy00My41NjEgMC00My40MTQgMzUuMTkzLTc4LjYwNyA3OC42MDctNzguNjA3em01NC45MjggMTM0LjgzOGMtMjguMTQ1IDI2LjcxMy02NS4zNzkgMjkuMzM1LTk4LjA5NiA5LjQ3M2wxOC40ODUtMjguMTA0YzE4LjYxNyAxMS4xMjggMzcuMzE5IDkuMzIzIDU2LjEwNy01LjQyOGwyMy41MDQgMjQuMDZ6bTMyLjAwNy0xOS40NTdsLS4xOTgtMTQ4LjY1MmgyOC4xNjN2MTQ0LjYzOGMwIDEuMzg4LjE3MiAyLjYxIDEuOTkyIDMuMzE3bDI0LjUgOS41MjgtMTEuMDM4IDI4LjctMjguNy0xMi4xNDNjLTEwLjg4My00LjYwNS0xNC43LTExLjI3Ny0xNC43MTktMjUuMzg5eiIgZmlsbD0iI2ZmZiIvPjxwYXRoIGQ9Ik0zMzkuMjggMzAxLjU4N2MtMTAuNTU3LTguMjA4LTE3LjM1NC0yMS4wMTgtMTcuMzU0LTM1LjQyNiAwLTEyLjgwOSA1LjM3Ny0yNC4zNTQgMTMuOTg1LTMyLjUyOWwtMTguMTktMjkuNDhjLTE4LjI4NSAxNC40ODctMzAuMDI4IDM2Ljg3LTMwLjAyOCA2Mi4wMDkgMCAyNy4wOTYgMTMuNjMyIDUxLjAwMiAzNC40MDcgNjUuMjU0bDE3LjE3OS0yOS44MjgiIGZpbGw9IiMyMDkxQzMiLz48cGF0aCBkPSJNMzUxLjYxIDIyMy45MzhjNC43NC0xLjcwNSA5Ljg1LTIuNjM5IDE1LjE3OC0yLjYzOSAxOS4yMDggMCAzNS41ODggMTIuMDc3IDQxLjk4NSAyOS4wNDhsMzQuODU2LTIuOTEyYy04LjQxNC0zNC42NC0zOS42MTMtNjAuMzY2LTc2Ljg0MS02MC4zNjYtMTAuNTE5IDAtMjAuNTQ5IDIuMDcxLTI5LjczIDUuNzk5bDE0LjU1MiAzMS4wNzEiIGZpbGw9IiNGQUVDMzIiLz48cGF0aCBkPSJNNDExLjU5NSAyNjguMzM1Yy0xLjEzNSAyMy43NjYtMjAuNzYgNDIuNjg4LTQ0LjgwNyA0Mi42ODgtMy43NyAwLTcuNDI4LS40NzItMTAuOTI1LTEuMzQ3bC0xMy42NzMgMzEuNjU3YzcuNzQ1IDIuNTMxIDE2LjAwNyAzLjkyIDI0LjU5OCAzLjkyIDQyLjU5MSAwIDc3LjMwMi0zMy42NyA3OS4wMDktNzUuODQybC0zNC4yMDMtMS4wNzciIGZpbGw9IiNEMDM1MkEiLz48L2c+PC9zdmc+");
}
.rccs__card--hipercard .rccs__card__background {
background: linear-gradient(25deg, #8b181b, #de1f27);
}
.rccs__card--hipercard .rccs__issuer {
background-image: url("data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNTEyIiBoZWlnaHQ9IjEyMyIgdmlld0JveD0iMCAwIDUxMiAxMjMiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgcHJlc2VydmVBc3BlY3RSYXRpbz0ieE1pZFlNaWQiPjxwYXRoIGQ9Ik0zNzQuMTE4IDgwLjg0MmMtNi45NDMgNi43OTctMjYuNDM0IDguNzI4LTI0LjQ0LTcuNTIgMS42NTYtMTMuNDk1IDE2LjM0OC0xNi4zNjMgMzIuMjczLTE0LjQxNC0xLjE4NCA3LjM4MS0yLjU0MiAxNi43NTUtNy44MzMgMjEuOTM0em0tMjEuOTM0LTUyLjY0MWMtLjY2MyAzLjcyMy0xLjY4MSA3LjA5Mi0yLjUwNiAxMC42NTMgNy45NTEtMS45OTMgMzIuNzA5LTguMTA1IDM1LjA5NSAyLjUwNi43OTIgMy41MjYtLjU3NCA3LjI3Mi0xLjU2NyAxMC4wMjgtMjIuMzg5LTIuMTIxLTQwLjYzNiAxLjU5OS00NS40MzUgMTcuNTQ3LTMuMjE0IDEwLjY4Mi4zNiAyMS4xOTQgNy4yMDcgMjQuMTI5IDEzLjE4NyA1LjY0OSAyOS4yMjgtLjgyMiAzNC43ODEtOS43MTQtLjU3NyAzLjA3OC0xLjEyMyA2LjE4Ny0uOTQgMTAuMDI2aDExLjU5NGMuMTIzLTExLjEwMyAxLjczOS0yMC4wOTEgMy40NDctMzAuMDgyIDEuNDU2LTguNTA4IDQuMTk1LTE2LjkzMSAzLjc1OS0yNC40NC0uOTk1LTE3LjE5Ny0yOS40ODYtMTEuMTE0LTQ1LjQzNC0xMC42NTN6bTExNi4yNDkgNTcuOTY4Yy05LjA3LjE5NS0xMy41OC01LjQxOS0xMy43ODctMTQuNzI2LS4zNjEtMTYuMzAxIDYuNzg5LTM0LjQxMSAyMS4zMDctMzYuMDM1IDYuNzYtLjc1NiAxMS42NTQuODE2IDE2LjYwNyAyLjUwOC00LjU0NyAxOC4zMS0yLjkwNSA0Ny43OTktMjQuMTI4IDQ4LjI1NHptMzAuMDgxLTg2LjE2OWMtMS4xNTUgMTAuMTI1LTIuNjk5IDE5Ljg2Mi00LjY5OSAyOS4xNC0zMy4wNTEtMTAuNDYxLTUzLjMyIDEzLjg1NS01Mi45NTYgNDMuODY4LjA3MiA1LjgwNSAxLjA3MSAxMS41NjEgNC43MDEgMTUuNjY3IDYuMjU2IDcuMDc2IDI0LjE3IDguNzY4IDMzLjIxNCAyLjgyMSAxLjc1Mi0xLjE1MSAzLjU0LTMuMjQ2IDQuNzAxLTQuNy44NzEtMS4wOTUgMi4yNTYtMy45NTkgMi41MDUtMy4xMzUtLjQ3NCAzLjE4Mi0xLjE4NSA2LjEyNy0xLjI1MiA5LjcxNWgxMi4yMmMyLjM1NC0zMy43ODQgOS42MzctNjIuNjQxIDE1LjA0LTkzLjM3NmgtMTMuNDc1em0tMzQ0Ljk4OSA4MS4xNTZjLTcuMjA5IDcuNjM1LTI0LjkzMSA3LjUxOC0yNi4zMi01LjMyOC0uNjA2LTUuNTg5IDEuNDc3LTExLjQ1IDIuNTA2LTE3LjIzMyAxLjA0MS01Ljg1MyAxLjc5MS0xMS40NjkgMi44MTktMTYuNjA5IDcuMS04LjY3IDI3Ljk2NS05LjcxNiAzMC4wODIgNC43MDEgMS44MzcgMTIuNTE3LTMuMTE3IDI4LjE0NS05LjA4NyAzNC40Njh6bTEwLjY1NC01Mi45NTVjLTExLjQwNC00LjI4NC0yNS4zMDMuODMtMzEuMzI4IDUuNjk0LjAyMS4yMTQtLjE0NC4yNDMtLjMyMS4yNTlsLjMyMS0uMjU5LS4wMDctLjA1NC45NC01LjY0aC0xMS41OTRjLTQuODMxIDMyLjE0My0xMC41NjEgNjMuMzg3LTE2LjYwNyA5NC4zMTZoMTMuNDczYzEuOTUtMTIuMDQ2IDMuMjM5LTI0Ljc1MyA1Ljk1NC0zNi4wMzQgMy4wNzcgMTEuODY1IDIzLjE2OSA5LjU5NyAzMS42NDcgNS4wMTQgMTcuNDk0LTkuNDYgMzAuOTg3LTU0LjQ4MSA3LjUyMS02My4yOTZ6bTYzLjkyMSAyMi41NjFoLTMxLjY0N2MxLjAwMS03LjI3NiA3LjU0Ny0xNS4yNzMgMTcuODU5LTE1LjY2NyA5LjcwNy0uMzcxIDE2LjY1NiAzLjU2NCAxMy43ODcgMTUuNjY3em0tMTIuODQ3LTIzLjgxNGMtOS43MzYuNzQ5LTE3Ljk2NSAzLjU1MS0yMy44MTQgOS43MTMtNy4xNzMgNy41NTgtMTIuOTgzIDI0LjI2Ni0xMS4yODEgMzkuNDgyIDIuNDI4IDIxLjcwOCAyOS40NTcgMjAuOTI5IDUxLjA3NSAxNS42NjcuMzY3LTMuODA5IDEuMjktNy4wNjYgMS44OC0xMC42NTMtOC45MDYgMy4zMzItMjQuMzY4IDcuOTg0LTMzLjUyOCAyLjE5NC02LjkxNS00LjM3My02Ljk1Ni0xNS40NDYtNC43LTI1LjA2OCAxNC41MjktLjQ2MiAyOS42MzEtLjM3NCA0NC4xODEgMCAuOTIzLTYuODIxIDMuNTY1LTE0LjI1NyAxLjI1My0yMC45OTMtMy4wNDktOC44OTQtMTMuOTYxLTExLjE5Ni0yNS4wNjctMTAuMzR6bS0xMTguNDQzIDEuMjUzYy0uMzY1LjA1Mi0uMzQxLjQ5NS0uMzE0LjkzOS0yLjk2OSAyMi4yMDMtNi45ODYgNDMuMzU4LTExLjI4IDY0LjIzNmgxMy40NzNjMy4yMzgtMjIuMzUyIDYuOTQ2LTQ0LjIzMyAxMS41OTQtNjUuMTc1aC0xMy40NzN6bTM1MS41Ny4zMTNjLTExLjkwOS01Ljk1My0yMS44MjIgNC4wMzctMjUuNjkzIDEwLjAyNyAxLjEwMi0zLjA3MyAxLjE2Ni03LjE4OCAyLjE5Mi0xMC4zMzloLTExLjkwN2MtMy4yMSAyMi4zNzktNy4wODEgNDQuMDk4LTExLjU5NCA2NS4xNzVoMTMuNzg3Yy4wODgtOC42MzggMS43ODQtMTUuMDI4IDMuMTMzLTIzLjUwMSAyLjg4Mi0xOC4wODggNy4xMTEtMzcuOTI2IDI4LjIwMi0zMS45Ni43LTMuMDYuOTktNi41MyAxLjg4LTkuNDAxem0tMTUxLjM0NSA0OS41MDhjLTEuMjQxLTMuMjA4LTEuNTYzLTguNTA3LTEuMjUzLTEyLjUzNC42OTctOS4wNTQgMy45OTYtMjAuMDc3IDkuMDg3LTI1LjA2NyA3LjAyNi02Ljg4OCAyMC44OTctNS43NDggMzEuOTYtMS44ODEuMzQzLTMuNzI5IDEuMDkzLTcuMDU0IDEuNTY3LTEwLjY1Mi0xOC4xNDUtMi45NjQtMzUuMzY1LTEuMTIxLTQ0LjQ5NCA4LjQ1OS04LjkzNiA5LjM3OC0xNC43OTYgMzAuOTMyLTEwLjY1NCA0NC40OTQgNC44NDggMTUuODcgMjYuNTgzIDE2LjczMyA0NC4xODMgMTAuNjU0Ljc3Ny0zLjE5IDEuMTktNi43NDYgMS44OC0xMC4wMjgtOS42MTYgNC45OTctMjguMDAxIDcuNTkzLTMyLjI3Ni0zLjQ0N3ptLTcuNTE5LTQ5LjgyMWMtMTEuOTQ4LTQuODI1LTIxLjMzOSAzLjMyOS0yNS42OTMgMTAuOTY3Ljk4Ny0zLjM5OCAxLjM5NC03LjM3OCAyLjE5Mi0xMC45NjdoLTExLjkwN2MtMi45MSAyMi41NzUtNy4yMDEgNDMuNzY5LTExLjI4IDY1LjE3NWgxMy40NzVjMS44ODgtMTIuNzA3IDIuNzAzLTI5LjgzNCA2Ljg5Mi00MS45ODggMy4zNDgtOS43MTMgMTIuMTExLTE3Ljk4NyAyNC43NTQtMTMuNDczLjE3OC0zLjU4MyAxLjIwMy02LjMxOCAxLjU2Ny05LjcxNHptLTIxNy4xNDYtMjYuMzIxYy0xLjkyNSAxMi40ODktNC4wOTMgMjQuNzM1LTYuMjY3IDM2Ljk3NC0xMy45NTkuMTQ3LTI4LjIxMS42OS00MS42NzQtLjMxNCAyLjU0My0xMS45NzMgNC4zNjctMjQuNjY4IDYuODkyLTM2LjY2aC0xNS4wMzljLTUuMzg1IDMwLjU0Ny0xMC4yODIgNjEuNTc5LTE2LjI5NCA5MS40OTdoMTUuMzUzYzIuNDA5LTE1LjM0OCA0LjY2MS0zMC44NTIgNy44MzMtNDUuNDM1IDEzLjA5MS0uMzIyIDI4Ljc4MS0uODg1IDQxLjM2Mi4zMTQtMi41OTQgMTUuMTYyLTUuNzI4IDI5Ljc4My04LjE0NyA0NS4xMjFoMTUuMzU0YzQuOTMyLTMwLjk5NyAxMC4xMzEtNjEuNzI4IDE2LjI5NC05MS40OTdoLTE1LjY2N3ptMzguMjI3IDEzLjc4N2MyLjY4Ni0xLjg0OSA2LjE0NS0xMC4yNzggMi4xOTQtMTMuNzg3LTEuMjUyLTEuMTExLTMuMzQ3LTEuNDMzLTYuMjY3LS45MzktMi43MS40NTctNC4yNjggMS4zNzctNS4zMjcgMi44MTktMS43MDMgMi4zMTktMy4yNjMgOS4zMS0uNjI4IDExLjkwNyAyLjU2NyAyLjUyOSA4LjMxOCAxLjE3NyAxMC4wMjggMHoiIGZpbGw9IiNmZmYiLz48L3N2Zz4=");
}
.rccs__card--jcb .rccs__issuer {
background-image: url("data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNTEyIiBoZWlnaHQ9IjM5NSIgdmlld0JveD0iMCAwIDUxMiAzOTUiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgcHJlc2VydmVBc3BlY3RSYXRpbz0ieE1pZFlNaWQiPjxkZWZzPjxsaW5lYXJHcmFkaWVudCB4MT0iLTU3LjUyNyUiIHkxPSI1MC4xMjQlIiB4Mj0iMjMyLjM5MSUiIHkyPSI1MC4xMjQlIiBpZD0iYSI+PHN0b3Agc3RvcC1jb2xvcj0iIzAwNzk0MCIgb2Zmc2V0PSIwJSIvPjxzdG9wIHN0b3AtY29sb3I9IiMwMDg3M0YiIG9mZnNldD0iMjIuODUlIi8+PHN0b3Agc3RvcC1jb2xvcj0iIzQwQTczNyIgb2Zmc2V0PSI3NC4zMyUiLz48c3RvcCBzdG9wLWNvbG9yPSIjNUNCNTMxIiBvZmZzZXQ9IjEwMCUiLz48L2xpbmVhckdyYWRpZW50PjxsaW5lYXJHcmFkaWVudCB4MT0iLjE4MyUiIHkxPSI0OS45NiUiIHgyPSIxMDAuMjczJSIgeTI9IjQ5Ljk2JSIgaWQ9ImIiPjxzdG9wIHN0b3AtY29sb3I9IiMwMDc5NDAiIG9mZnNldD0iMCUiLz48c3RvcCBzdG9wLWNvbG9yPSIjMDA4NzNGIiBvZmZzZXQ9IjIyLjg1JSIvPjxzdG9wIHN0b3AtY29sb3I9IiM0MEE3MzciIG9mZnNldD0iNzQuMzMlIi8+PHN0b3Agc3RvcC1jb2xvcj0iIzVDQjUzMSIgb2Zmc2V0PSIxMDAlIi8+PC9saW5lYXJHcmFkaWVudD48bGluZWFyR3JhZGllbnQgeDE9Ii02Mi44MDIlIiB5MT0iNDkuODU4JSIgeDI9IjI1My42NzElIiB5Mj0iNDkuODU4JSIgaWQ9ImMiPjxzdG9wIHN0b3AtY29sb3I9IiMwMDc5NDAiIG9mZnNldD0iMCUiLz48c3RvcCBzdG9wLWNvbG9yPSIjMDA4NzNGIiBvZmZzZXQ9IjIyLjg1JSIvPjxzdG9wIHN0b3AtY29sb3I9IiM0MEE3MzciIG9mZnNldD0iNzQuMzMlIi8+PHN0b3Agc3RvcC1jb2xvcj0iIzVDQjUzMSIgb2Zmc2V0PSIxMDAlIi8+PC9saW5lYXJHcmFkaWVudD48bGluZWFyR3JhZGllbnQgeDE9Ii4xNzYlIiB5MT0iNTAuMDA2JSIgeDI9IjEwMS44MDglIiB5Mj0iNTAuMDA2JSIgaWQ9ImQiPjxzdG9wIHN0b3AtY29sb3I9IiMxRjI4NkYiIG9mZnNldD0iMCUiLz48c3RvcCBzdG9wLWNvbG9yPSIjMDA0RTk0IiBvZmZzZXQ9IjQ3LjUxJSIvPjxzdG9wIHN0b3AtY29sb3I9IiMwMDY2QjEiIG9mZnNldD0iODIuNjElIi8+PHN0b3Agc3RvcC1jb2xvcj0iIzAwNkZCQyIgb2Zmc2V0PSIxMDAlIi8+PC9saW5lYXJHcmFkaWVudD48bGluZWFyR3JhZGllbnQgeDE9Ii0uNTc2JSIgeTE9IjQ5LjkxNCUiIHgyPSI5OC4xMzMlIiB5Mj0iNDkuOTE0JSIgaWQ9ImUiPjxzdG9wIHN0b3AtY29sb3I9IiM2QzJDMkYiIG9mZnNldD0iMCUiLz48c3RvcCBzdG9wLWNvbG9yPSIjODgyNzMwIiBvZmZzZXQ9IjE3LjM1JSIvPjxzdG9wIHN0b3AtY29sb3I9IiNCRTE4MzMiIG9mZnNldD0iNTcuMzElIi8+PHN0b3Agc3RvcC1jb2xvcj0iI0RDMDQzNiIgb2Zmc2V0PSI4NS44NSUiLz48c3RvcCBzdG9wLWNvbG9yPSIjRTYwMDM5IiBvZmZzZXQ9IjEwMCUiLz48L2xpbmVhckdyYWRpZW50PjwvZGVmcz48cGF0aCBkPSJNNTEyIDMxNC44MzZjMCA0My44MTQtMzUuNjc3IDc5LjQ5MS03OS40OTEgNzkuNDkxaC00MzIuNTA5di0zMTQuODM2YzAtNDMuODE0IDM1LjY3Ny03OS40OTEgNzkuNDkxLTc5LjQ5MWg0MzIuNTA5djMxNC44MzZ6IiBmaWxsPSIjZmZmIi8+PHBhdGggZD0iTTM3MS4xNjkgMjM0LjA5M2gzMi44NjFsNC4wNjgtLjMxM2M2LjI1OS0xLjI1MiAxMS41NzktNi44ODUgMTEuNTc5LTE0LjcwOSAwLTcuNTExLTUuMzItMTMuMTQ0LTExLjU3OS0xNC43MDktLjkzOS0uMzEzLTIuODE3LS4zMTMtNC4wNjgtLjMxM2gtMzIuODYxdjMwLjA0NHoiIGZpbGw9InVybCgjYSkiLz48cGF0aCBkPSJNNDAwLjI3NCAyNi42MDFjLTMxLjI5NiAwLTU2Ljk1OCAyNS4zNS01Ni45NTggNTYuOTU4djU5LjE0OWg4MC40M2MxLjg3OCAwIDQuMDY4IDAgNS42MzMuMzEzIDE4LjE1Mi45MzkgMzEuNjA5IDEwLjMyOCAzMS42MDkgMjYuNjAxIDAgMTIuODMxLTkuMDc2IDIzLjc4NS0yNS45NzYgMjUuOTc2di42MjZjMTguNDY1IDEuMjUyIDMyLjU0OCAxMS41NzkgMzIuNTQ4IDI3LjU0IDAgMTcuMjEzLTE1LjY0OCAyOC40NzktMzYuMzAzIDI4LjQ3OWgtODguMjU0djExNS43OTVoODMuNTZjMzEuMjk2IDAgNTYuOTU4LTI1LjM1IDU2Ljk1OC01Ni45NTh2LTI4NC40NzloLTgzLjI0N3oiIGZpbGw9InVybCgjYikiLz48cGF0aCBkPSJNNDE1LjYwOSAxNzMuMzc5YzAtNy41MTEtNS4zMi0xMi41MTgtMTEuNTc5LTEzLjQ1Ny0uNjI2IDAtMi4xOTEtLjMxMy0zLjEzLS4zMTNoLTI5LjczMXYyNy41NGgyOS43MzFjLjkzOSAwIDIuODE3IDAgMy4xMy0uMzEzIDYuMjU5LS45MzkgMTEuNTc5LTUuOTQ2IDExLjU3OS0xMy40NTd6IiBmaWxsPSJ1cmwoI2MpIi8+PHBhdGggZD0iTTg1LjQzOCAyNi42MDFjLTMxLjI5NiAwLTU2Ljk1OCAyNS4zNS01Ni45NTggNTYuOTU4djE0MC41MThjMTUuOTYxIDcuODI0IDMyLjU0OCAxMi44MzEgNDkuMTM0IDEyLjgzMSAxOS43MTYgMCAzMC4zNTctMTEuODkyIDMwLjM1Ny0yOC4xNjZ2LTY2LjM0N2g0OC44MjJ2NjYuMDM0YzAgMjUuNjYzLTE1Ljk2MSA0Ni42MzEtNzAuMTAzIDQ2LjYzMS0zMi44NjEgMC01OC41MjMtNy4xOTgtNTguNTIzLTcuMTk4djExOS44NjNoODMuNTZjMzEuMjk2IDAgNTYuOTU4LTI1LjM1IDU2Ljk1OC01Ni45NTh2LTI4NC4xNjZoLTgzLjI0N3oiIGZpbGw9InVybCgjZCkiLz48cGF0aCBkPSJNMjQyLjg1NiAyNi42MDFjLTMxLjI5NiAwLTU2Ljk1OCAyNS4zNS01Ni45NTggNTYuOTU4djc0LjQ4NGMxNC4zOTYtMTIuMjA1IDM5LjQzMy0yMC4wMjkgNzkuODA0LTE4LjE1MiAyMS41OTQuOTM5IDQ0Ljc1MyA2Ljg4NSA0NC43NTMgNi44ODV2MjQuMDk4Yy0xMS41NzktNS45NDYtMjUuMzUtMTEuMjY3LTQzLjE4OC0xMi41MTgtMzAuNjctMi4xOTEtNDkuMTM0IDEyLjgzMS00OS4xMzQgMzkuMTIgMCAyNi42MDEgMTguNDY1IDQxLjYyMyA0OS4xMzQgMzkuMTIgMTcuODM5LTEuMjUyIDMxLjYwOS02Ljg4NSA0My4xODgtMTIuNTE4djI0LjA5OHMtMjIuODQ2IDUuOTQ2LTQ0Ljc1MyA2Ljg4NWMtNDAuMzcyIDEuODc4LTY1LjQwOC01Ljk0Ni03OS44MDQtMTguMTUydjEzMS40NDNoODMuNTZjMzEuMjk2IDAgNTYuOTU4LTI1LjM1IDU2Ljk1OC01Ni45NTh2LTI4NC43OTJoLTgzLjU2eiIgZmlsbD0idXJsKCNlKSIvPjwvc3ZnPg==");
}
.rccs__card--laser .rccs__issuer {
background-image: url("data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNTEyIiBoZWlnaHQ9IjM2OCIgdmlld0JveD0iMCAwIDUxMiAzNjgiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgcHJlc2VydmVBc3BlY3RSYXRpbz0ieE1pZFlNaWQiPjxwYXRoIGZpbGw9IiMyODM0N0QiIGQ9Ik0wIDM2Ny4yMTJoNTEydi0zNjcuMjExaC01MTJ6Ii8+PHBhdGggZD0iTTI1My45MSAyOTEuMDQzaC0yNTMuOTF2Mi4wOTZoMjUyLjY5bC4xMTYgNy42MjIgNC43NjQtNy42MjJoMjU0LjQzdi0yLjA5NmgtMjUzLjI3MWwzLjY2My02LjExMi04LjQ4MiA2LjExMnptMi41NTYtMjAuMDg0aC0yNTYuNDY2djIuMDk2aDI1NS40Mmw5LjE0NyA4LjM5OCA1LjExMi04LjM5OGgyNDIuMzIxdi0yLjA5NmgtMjQxLjA0NWw0LjY3Ny03LjY4NC0xOS4xNjYgNy42ODR6bTIuMTUtMjAuMDgzaC0yNTguNjE2djIuMDk2aDI1NS45OTlsMjAuODgyIDguMjUyIDUuMDIyLTguMjUyaDIzMC4wOTd2LTIuMDk2aC0yMjguODJsNC4xMS02Ljc1Mi0yOC42NzQgNi43NTJ6bS02Ljk3Mi0yMC4wODNoLTI1MS42NDR2Mi4wOTZoMjUwLjAxN2wzOS4zNDIgNy44MzYgNC43Ny03LjgzNmgyMTcuODcxdi0yLjA5NmgtMjE2LjU5NGw0LjEzNC03LjI1Mi00Ny44OTYgNy4yNTJ6bS0yMi43NzUtMTcuMDU3aC0yMjguODY4djIuMDk2aDIzNy43bDYzLjEgNS45OTYgMy43MTEtNS45OTZoMjA3LjQ4OXYtMi4wOTZoLTIwNi4yMTRsNS4xMy04LjMyNC04Mi4wNDggOC4zMjR6bS0zLjMxLTIwLjA4M2gtMjI1LjU1OHYyLjA5NmgyMjYuMDgxbDg2Ljg4OSA2LjExMiAzLjc2Ni02LjExMmgxOTUuMjY0di0yLjA5NmgtMTkzLjk4OGw0LjUzNS03LjQ1MS05Ni45ODkgNy40NTF6bS0zLjEzOC0yMC4wODNoLTIyMi40MnYyLjA5NmgyMjEuOTU1bDEwMy4xMjEgNi4zNDQgMy44ODYtNi4zNDRoMTgzLjAzOHYtMi4wOTZoLTE4MS43NjNsNC40MzQtNy4yMTgtMTEyLjI1MSA3LjIxOHptLTguNzcyLTIwLjA4M2gtMjEzLjY0N3YyLjA5NWgyMTMuMjk4bDEyMy40ODIgNy42NjcgNC42MDUtNy42NjdoMTcwLjYxNXYtMi4wOTVoLTE2OS4zNTZsNC4wNzgtNi43ODgtMTMzLjA3NSA2Ljc4OHptLTE5LjA1Ny0xNy4yODloLTE5NC41OTF2Mi4wOTZoMTk0LjQxNmwxNTMuNjY3IDYuMTM4IDMuNjg5LTYuMTM4aDE2MC4yMjh2LTIuMDk2aC0xNTguOTdsNC42MzEtNy43MDktMTYzLjA3IDcuNzA5em00Ljc2NS0yMC4wODRoLTE5OS4zNTV2Mi4wOTZoMTk4LjMwOGwxNjEuNDQzIDYuODEgNC4wODQtNi44MWgxNDguMTY1di0yLjA5NmgtMTQ2LjkwNmw0LjAxNS02LjY4NC0xNjkuNzU0IDYuNjg0em0tMTEuMzMtMjAuMDgzaC0xODguMDI1djIuMDk2aDE4Ni45NzlsMTg0LjY3NiA3LjEwMiA0LjI0NC03LjEwMmgxMzYuMTAxdi0yLjA5NmgtMTM0Ljg0MmwzLjY3Ny02LjE3MS0xOTIuODEgNi4xNzF6bS04Ljg4OS0yMC4wODNoLTE3OS4xMzZ2Mi4wOTZoMTY0Ljg0NGwyMTkuMDg2IDYuNzEzIDQuMDMzLTYuNzEzaDEyNC4wMzd2LTIuMDk2aC0xMjIuNzc4bDMuNjc4LTYuMTIxLTIxMy43NjQgNi4xMjF6bS0xLjIyLTIwLjA4M2gtMTc3LjkxNnYyLjA5NmgxNzUuNjVsMjE5Ljc0OCA3LjcwNiA0LjYzLTcuNzA2aDExMS45NzJ2LTIuMDk2aC0xMTAuNzE0bDMuOTIyLTYuNTItMjI3LjI5MiA2LjUyem0tMTIuMzc1LTIwLjA4M2gtMTY1LjU0MXYyLjA5NWgxMzIuNTQybDI3NS4yOTkgNy4wNzggNC4yNTEtNy4wNzhoOTkuOTA4di0yLjA5NWgtOTguNjQ5bDMuODE3LTYuMzU2LTI1MS42MjcgNi4zNTZ6bS04Mi4yMjctMTYuNjQ5aC04My4zMTR2Mi4wOTZoMjAwLjA1MmwyMTkuNTU0IDQuMTM4IDIuNDg3LTQuMTM4aDg5LjkwN3YtMi4wOTZoLTg4LjY0OGw1LjU4OC05LjMwMy0zNDUuNjI1IDkuMzAzeiIgZmlsbD0iI0VCMEQ3RiIvPjxwYXRoIGQ9Ik0xMzQuNzEgMzQ5LjE0MXYtMzcuNjE4aDcuNjQzdjMxLjIyN2gxOS4wMDV2Ni4zOTFoLTI2LjY0OHptODMuMTU1IDBoLTguMzE0bC0zLjMwNi04LjYxNWgtMTUuMTMybC0zLjEyNCA4LjYxNWgtOC4xMDhsMTQuNzQ0LTM3LjkyOWg4LjA4MmwxNS4xNTcgMzcuOTI5em0tMTQuMDczLTE1LjAwNmwtNS4yMTYtMTQuMDc1LTUuMTEzIDE0LjA3NWgxMC4zMjl6bTMzLjExMiAyLjY2NWw3LjQzNi0uNzI1Yy40NDcgMi41MDIgMS4zNTYgNC4zMzkgMi43MjUgNS41MTEgMS4zNjggMS4xNzQgMy4yMTUgMS43NTkgNS41MzkgMS43NTkgMi40NiAwIDQuMzE3LS41MjEgNS41NjQtMS41NjYgMS4yNDgtMS4wNDIgMS44NzItMi4yNjMgMS44NzItMy42NiAwLS44OTYtLjI2My0xLjY2LS43ODctMi4yOS0uNTI1LS42MjktMS40NDQtMS4xNzYtMi43NTItMS42NDItLjg5NC0uMzExLTIuOTM1LS44NjItNi4xMTktMS42NTYtNC4wOTctMS4wMTctNi45NzEtMi4yNjctOC42MjQtMy43NTItMi4zMjMtMi4wODYtMy40ODUtNC42MzEtMy40ODUtNy42MzIgMC0xLjkzMS41NDYtMy43MzkgMS42MzktNS40MTkgMS4wOTMtMS42ODMgMi42NjgtMi45NjQgNC43MjYtMy44NDMgMi4wNTctLjg4IDQuNTM5LTEuMzE5IDcuNDUtMS4zMTkgNC43NTEgMCA4LjMyOCAxLjA0NCAxMC43MjggMy4xMyAyLjQwMyAyLjA4NyAzLjY2MSA0Ljg3NCAzLjc4NCA4LjM1NmwtNy42NDMuMzM3Yy0uMzI5LTEuOTQ4LTEuMDMxLTMuMzUtMi4xMDQtNC4yMDUtMS4wNzctLjg1Mi0yLjY5MS0xLjI4LTQuODQ0LTEuMjgtMi4yMjEgMC0zLjk1OS40NTgtNS4yMTYgMS4zNzEtLjgwOS41ODctMS4yMTQgMS4zNzItMS4yMTQgMi4zNTUgMCAuODk3LjM3OCAxLjY2NSAxLjEzNyAyLjMwMi45NjMuODEyIDMuMzA1IDEuNjU2IDcuMDI0IDIuNTM2IDMuNzE4Ljg3OSA2LjQ2OCAxLjc4OSA4LjI0OSAyLjczIDEuNzgzLjkzOSAzLjE3OCAyLjIyNSA0LjE4MyAzLjg1MyAxLjAwOCAxLjYzMiAxLjUxMSAzLjY0NiAxLjUxMSA2LjA0MiAwIDIuMTc0LS42MDMgNC4yMDktMS44MDcgNi4xMDYtMS4yMDcgMS44OTgtMi45MSAzLjMwOC01LjExMiA0LjIzMS0yLjIwNS45MjItNC45NSAxLjM4NC04LjIzOSAxLjM4NC00Ljc4NiAwLTguNDYtMS4xMDktMTEuMDI1LTMuMzI2LTIuNTY1LTIuMjE1LTQuMDk3LTUuNDQ1LTQuNTk2LTkuNjg4em01NC4xMzIgMTIuMzQxdi0zNy45MjloMjguMDY5djYuNDE3aC0yMC40MjZ2OC40MDhoMTkuMDA1djYuMzkxaC0xOS4wMDV2MTAuMzIyaDIxLjE0OXY2LjM5MWgtMjguNzkyem01Mi4yMiAwdi0zNy45MjloMTYuMDg3YzQuMDQ1IDAgNi45ODQuMzQgOC44MTkgMS4wMjEgMS44MzIuNjgzIDMuMzAxIDEuODk1IDQuNDAyIDMuNjM2IDEuMTAxIDEuNzQzIDEuNjUzIDMuNzM1IDEuNjUzIDUuOTc3IDAgMi44NDUtLjgzNiA1LjE5Ni0yLjUwNSA3LjA0OS0xLjY3IDEuODU1LTQuMTY3IDMuMDI1LTcuNDg4IDMuNTA2IDEuNjUzLjk2NyAzLjAxNyAyLjAyOCA0LjA5NCAzLjE4MyAxLjA3NSAxLjE1NiAyLjUyNCAzLjIwOCA0LjM1MSA2LjE1N2w0LjYyMSA3LjRoLTkuMTRsLTUuNTI2LTguMjU0Yy0xLjk2Mi0yLjk0OS0zLjMwNS00LjgwOC00LjAyOC01LjU3Ni0uNzIzLS43NjYtMS40ODktMS4yOTMtMi4yOTktMS41NzctLjgwOS0uMjg2LTIuMDkxLS40MjctMy44NDYtLjQyN2gtMS41NXYxNS44MzRoLTcuNjQzem03LjY0My0yMS44ODhoNS42NTVjMy42NjcgMCA1Ljk1NS0uMTU1IDYuODY5LS40NjYuOTEyLS4zMSAxLjYyNy0uODQ0IDIuMTQ0LTEuNjA0LjUxNi0uNzU5Ljc3NC0xLjcwNy43NzQtMi44NDYgMC0xLjI3Ni0uMzQtMi4zMDctMS4wMTktMy4wOTEtLjY4Mi0uNzg1LTEuNjQxLTEuMjgxLTIuODgtMS40ODgtLjYyLS4wODYtMi40NzgtLjEzLTUuNTc3LS4xM2gtNS45NjV2OS42MjV6IiBmaWxsPSIjZmZmIi8+PC9zdmc+");
}
.rccs__card--maestro .rccs__card__background,
.rccs__card--mastercard .rccs__card__background {
background: linear-gradient(25deg, #f37b26, #fdb731);
}
.rccs__card--maestro .rccs__issuer {
background-image: url("data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNTEyIiBoZWlnaHQ9IjM5OCIgdmlld0JveD0iMCAwIDUxMiAzOTgiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgcHJlc2VydmVBc3BlY3RSYXRpbz0ieE1pZFlNaWQiPjxwYXRoIGZpbGw9IiM2QzZCQkQiIGQ9Ik0zMjUuMjIzIDI4Mi42MjloLTEzOC40NDV2LTI0OC44MDNoMTM4LjQ0NXoiLz48cGF0aCBkPSJNMTk1LjU2NSAxNTguMjMzYzAtNTAuNDcxIDIzLjYzMS05NS40MjkgNjAuNDMtMTI0LjQwMS0yNi45MS0yMS4xODYtNjAuODczLTMzLjgzMi05Ny43ODMtMzMuODMyLTg3LjM4MSAwLTE1OC4yMTMgNzAuODQyLTE1OC4yMTMgMTU4LjIzM3M3MC44MzIgMTU4LjIzMyAxNTguMjEzIDE1OC4yMzNjMzYuOTEgMCA3MC44NzItMTIuNjQ1IDk3Ljc4My0zMy44MzItMzYuNzk5LTI4Ljk3My02MC40My03My45MzEtNjAuNDMtMTI0LjQwMSIgZmlsbD0iI0QzMjAxMSIvPjxwYXRoIGQ9Ik01MTIgMTU4LjIzM2MwIDg3LjM5MS03MC44MzIgMTU4LjIzMy0xNTguMjEzIDE1OC4yMzMtMzYuOTEgMC03MC44NzItMTIuNjQ1LTk3Ljc5My0zMy44MzIgMzYuODA5LTI4Ljk3MyA2MC40NC03My45MzEgNjAuNDQtMTI0LjQwMSAwLTUwLjQ3MS0yMy42MzEtOTUuNDI5LTYwLjQ0LTEyNC40MDEgMjYuOTItMjEuMTg2IDYwLjg4My0zMy44MzIgOTcuNzkzLTMzLjgzMiA4Ny4zODEgMCAxNTguMjEzIDcwLjg0MiAxNTguMjEzIDE1OC4yMzMiIGZpbGw9IiMwMDk5REYiLz48cGF0aCBkPSJNMzcyLjA2NSAzNTIuOTM0YzEuODQxIDAgNC40ODcuMzUyIDYuNTA5IDEuMTQ3bC0yLjgxNyA4LjYxMWMtMS45MzItLjc5NS0zLjg2My0xLjA1Ni01LjcxNC0xLjA1Ni01Ljk3NiAwLTguOTYzIDMuODYzLTguOTYzIDEwLjgwNHYyMy41NmgtOS4xNDR2LTQyLjAxaDkuMDU0djUuMWMyLjM3NC0zLjY5MiA1LjgwNS02LjE1NyAxMS4wNzYtNi4xNTd6bS0zMy43ODEgOS40MDZoLTE0LjkzOXYxOC45ODNjMCA0LjIxNSAxLjQ4OSA3LjAzMiA2LjA2NiA3LjAzMiAyLjM3NCAwIDUuMzYyLS43OTUgOC4wNzgtMi4zNzRsMi42MzYgNy44MTdjLTIuODk3IDIuMDIyLTcuNDY0IDMuMjU5LTExLjQxOCAzLjI1OS0xMC44MTQgMC0xNC41ODctNS44MDUtMTQuNTg3LTE1LjU2M3YtMTkuMTU0aC04LjUzMXYtOC4zNWg4LjUzMXYtMTIuNzQ2aDkuMjI1djEyLjc0NmgxNC45Mzl2OC4zNXptLTExNi45ODcgOC45NjNjLjk3Ni02LjA2NiA0LjY1OC0xMC4yMDEgMTEuMTY3LTEwLjIwMSA1Ljg4NSAwIDkuNjY4IDMuNjkyIDEwLjYzMyAxMC4yMDFoLTIxLjh6bTMxLjI5NyAzLjY5MmMtLjA5MS0xMy4wOTgtOC4xNzktMjIuMDYxLTE5Ljk1OS0yMi4wNjEtMTIuMzAzIDAtMjAuOTE1IDguOTYzLTIwLjkxNSAyMi4wNjEgMCAxMy4zNSA4Ljk2MyAyMi4wNTEgMjEuNTM4IDIyLjA1MSA2LjMyOCAwIDEyLjEyMi0xLjU3OSAxNy4yMjMtNS44ODVsLTQuNDg3LTYuNzdjLTMuNTExIDIuODE3LTcuOTk4IDQuMzk2LTEyLjIxMyA0LjM5Ni01Ljg4NSAwLTExLjI0Ny0yLjcyNi0xMi41NjUtMTAuMjgxaDMxLjE5NmMuMDkxLTEuMTQ3LjE4MS0yLjI4NC4xODEtMy41MTF6bTQwLjE1OS0xMC4yODFjLTIuNTQ1LTEuNTg5LTcuNzI2LTMuNjEyLTEzLjA4OC0zLjYxMi01LjAxIDAtNy45OTggMS44NTEtNy45OTggNC45MjkgMCAyLjgwNyAzLjE1OSAzLjYwMSA3LjExMiA0LjEyNWw0LjMwNi42MTRjOS4xNDQgMS4zMjggMTQuNjc3IDUuMTkxIDE0LjY3NyAxMi41NzUgMCA3Ljk5OC03LjAzMiAxMy43MTItMTkuMTU0IDEzLjcxMi02Ljg2MSAwLTEzLjE4OS0xLjc2LTE4LjE5OC01LjQ1Mmw0LjMwNi03LjEyMmMzLjA3OCAyLjM3NCA3LjY1NiA0LjM5NiAxMy45ODMgNC4zOTYgNi4yMzcgMCA5LjU3Ny0xLjg0MSA5LjU3Ny01LjEgMC0yLjM2NC0yLjM3NC0zLjY5Mi03LjM4NC00LjM4NmwtNC4zMDYtLjYxNGMtOS40MDYtMS4zMjgtMTQuNTA2LTUuNTQzLTE0LjUwNi0xMi4zOTQgMC04LjM1IDYuODYxLTEzLjQ1IDE3LjQ5NC0xMy40NSA2LjY4IDAgMTIuNzQ2IDEuNDk5IDE3LjEzMiA0LjM5NmwtMy45NTQgNy4zODR6bTExMi43MjItMy4wOThjLTEuODgxIDAtMy42MjIuMzMyLTUuMjQxLjk4Ni0xLjYxLjY2NC0zLjAwOCAxLjU4OS00LjE4NSAyLjc3Ny0xLjE3NyAxLjE4Ny0yLjEwMyAyLjYxNi0yLjc3NyA0LjI3NS0uNjc0IDEuNjYtMS4wMDYgMy40OTEtMS4wMDYgNS40ODMgMCAyLjAwMi4zMzIgMy44MjMgMS4wMDYgNS40ODMuNjc0IDEuNjYgMS42IDMuMDg4IDIuNzc3IDQuMjc1IDEuMTc3IDEuMTg3IDIuNTc1IDIuMTEzIDQuMTg1IDIuNzc3IDEuNjIuNjY0IDMuMzYuOTg2IDUuMjQxLjk4NiAxLjg4MSAwIDMuNjMyLS4zMjIgNS4yNDEtLjk4NiAxLjYyLS42NjQgMy4wMjgtMS41ODkgNC4yMDUtMi43NzcgMS4xOTctMS4xODcgMi4xMjMtMi42MTYgMi44MDctNC4yNzUuNjc0LTEuNjYgMS4wMDYtMy40ODEgMS4wMDYtNS40ODMgMC0xLjk5Mi0uMzMyLTMuODIzLTEuMDA2LTUuNDgzLS42ODQtMS42Ni0xLjYxLTMuMDg4LTIuODA3LTQuMjc1LTEuMTc3LTEuMTg3LTIuNTg1LTIuMTEzLTQuMjA1LTIuNzc3LTEuNjEtLjY1NC0zLjM2LS45ODYtNS4yNDEtLjk4NnptMC04LjY4MmMzLjI1OSAwIDYuMjc3LjU2MyA5LjA1NCAxLjcgMi43NzcgMS4xMjcgNS4xODEgMi42ODYgNy4yMDMgNC42NjggMi4wMzIgMS45ODIgMy42MTIgNC4zMjYgNC43NTggNy4wMjIgMS4xNDcgMi43MDYgMS43MiA1LjY0NCAxLjcyIDguODEzIDAgMy4xNjktLjU3MyA2LjEwNi0xLjcyIDguODEzLTEuMTQ3IDIuNjk2LTIuNzI2IDUuMDUtNC43NTggNy4wMzItMi4wMjIgMS45ODItNC40MjYgMy41MzEtNy4yMDMgNC42NjgtMi43NzcgMS4xMjctNS43OTUgMS42OS05LjA1NCAxLjY5LTMuMjU5IDAtNi4yNzctLjU2My05LjA1NC0xLjY5LTIuNzc3LTEuMTM3LTUuMTYxLTIuNjg2LTcuMTczLTQuNjY4LTIuMDEyLTEuOTgyLTMuNTkxLTQuMzM2LTQuNzM4LTcuMDMyLTEuMTQ3LTIuNzA2LTEuNzItNS42NDQtMS43Mi04LjgxMyAwLTMuMTY5LjU3My02LjEwNiAxLjcyLTguODEzIDEuMTQ3LTIuNjk2IDIuNzI2LTUuMDQgNC43MzgtNy4wMjIgMi4wMTItMS45ODIgNC4zOTYtMy41NDEgNy4xNzMtNC42NjggMi43NzctMS4xMzcgNS43OTUtMS43IDkuMDU0LTEuN3ptLTIzNy41NzYgMjIuMDYxYzAtNy4zODQgNC44MzktMTMuNDUgMTIuNzQ2LTEzLjQ1IDcuNTU1IDAgMTIuNjU1IDUuODA1IDEyLjY1NSAxMy40NSAwIDcuNjQ2LTUuMSAxMy40NC0xMi42NTUgMTMuNDQtNy45MDcgMC0xMi43NDYtNi4wNTYtMTIuNzQ2LTEzLjQ0em0zNC4wMTMgMHYtMjEuMDA1aC05LjEzNHY1LjFjLTIuOTA3LTMuNzgzLTcuMjkzLTYuMTU3LTEzLjI2OS02LjE1Ny0xMS43OCAwLTIxLjAwNSA5LjIyNS0yMS4wMDUgMjIuMDYxIDAgMTIuODI2IDkuMjI1IDIyLjA2MSAyMS4wMDUgMjIuMDYxIDUuOTc2IDAgMTAuMzYyLTIuMzc0IDEzLjI2OS02LjE1N3Y1LjFoOS4xMzR2LTIxLjAwNXptLTUxLjQ5NyAyMS4wMDV2LTI2LjM2N2MwLTkuOTI5LTYuMzI4LTE2LjYwOS0xNi41MTgtMTYuNy01LjM2Mi0uMDkxLTEwLjkwNSAxLjU3OS0xNC43NjggNy40NzUtMi44OTctNC42NTgtNy40NzUtNy40NzUtMTMuODkzLTcuNDc1LTQuNDc3IDAtOC44NzMgMS4zMTgtMTIuMzAzIDYuMjM3di01LjE4MWgtOS4xMzR2NDIuMDFoOS4yMjV2LTIzLjI4OWMwLTcuMjkzIDQuMDQ0LTExLjE2NyAxMC4yODEtMTEuMTY3IDYuMDY2IDAgOS4xNDQgMy45NTQgOS4xNDQgMTEuMDc2djIzLjM3OWg5LjIyNXYtMjMuMjg5YzAtNy4yOTMgNC4yMjUtMTEuMTY3IDEwLjI4MS0xMS4xNjcgNi4yNDcgMCA5LjIzNSAzLjk1NCA5LjIzNSAxMS4wNzZ2MjMuMzc5aDkuMjI1eiIgZmlsbD0iIzExMEYwRCIvPjwvc3ZnPg==");
}
.rccs__card--mastercard .rccs__issuer {
background-image: url("data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNTEyIiBoZWlnaHQ9IjM5NyIgdmlld0JveD0iMCAwIDUxMiAzOTciIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgcHJlc2VydmVBc3BlY3RSYXRpbz0ieE1pZFlNaWQiPjxwYXRoIGQ9Ik05My4wNzkgMzk2LjAyM3YtMjYuMzQzYzAtMTAuMDk4LTYuMTQ3LTE2LjY4NC0xNi42ODQtMTYuNjg0LTUuMjY5IDAtMTAuOTc2IDEuNzU2LTE0LjkyOCA3LjQ2NC0zLjA3My00LjgzLTcuNDY0LTcuNDY0LTE0LjA1LTcuNDY0LTQuMzkgMC04Ljc4MSAxLjMxNy0xMi4yOTMgNi4xNDd2LTUuMjY5aC05LjIydjQyLjE0OWg5LjIydi0yMy4yN2MwLTcuNDY0IDMuOTUxLTEwLjk3NiAxMC4wOTgtMTAuOTc2czkuMjIgMy45NTEgOS4yMiAxMC45NzZ2MjMuMjdoOS4yMnYtMjMuMjdjMC03LjQ2NCA0LjM5LTEwLjk3NiAxMC4wOTgtMTAuOTc2IDYuMTQ3IDAgOS4yMiAzLjk1MSA5LjIyIDEwLjk3NnYyMy4yN2gxMC4wOTh6bTEzNi41NDQtNDIuMTQ5aC0xNC45Mjh2LTEyLjczMmgtOS4yMnYxMi43MzJoLTguMzQydjguMzQyaDguMzQydjE5LjMxOGMwIDkuNjU5IDMuOTUxIDE1LjM2NyAxNC40ODkgMTUuMzY3IDMuOTUxIDAgOC4zNDItMS4zMTcgMTEuNDE1LTMuMDczbC0yLjYzNC03LjkwM2MtMi42MzQgMS43NTYtNS43MDggMi4xOTUtNy45MDMgMi4xOTUtNC4zOSAwLTYuMTQ3LTIuNjM0LTYuMTQ3LTcuMDI1di0xOC44NzloMTQuOTI4di04LjM0MnptNzguMTUxLS44NzhjLTUuMjY5IDAtOC43ODEgMi42MzQtMTAuOTc2IDYuMTQ3di01LjI2OWgtOS4yMnY0Mi4xNDloOS4yMnYtMjMuNzA5YzAtNy4wMjUgMy4wNzMtMTAuOTc2IDguNzgxLTEwLjk3NiAxLjc1NiAwIDMuOTUxLjQzOSA1LjcwOC44NzhsMi42MzQtOC43ODFjLTEuNzU2LS40MzktNC4zOS0uNDM5LTYuMTQ3LS40Mzl6bS0xMTguMTA0IDQuMzljLTQuMzktMy4wNzMtMTAuNTM3LTQuMzktMTcuMTIzLTQuMzktMTAuNTM3IDAtMTcuNTYyIDUuMjY5LTE3LjU2MiAxMy42MTEgMCA3LjAyNSA1LjI2OSAxMC45NzYgMTQuNDg5IDEyLjI5M2w0LjM5LjQzOWM0LjgzLjg3OCA3LjQ2NCAyLjE5NSA3LjQ2NCA0LjM5IDAgMy4wNzMtMy41MTIgNS4yNjktOS42NTkgNS4yNjlzLTEwLjk3Ni0yLjE5NS0xNC4wNS00LjM5bC00LjM5IDcuMDI1YzQuODMgMy41MTIgMTEuNDE1IDUuMjY5IDE4LjAwMSA1LjI2OSAxMi4yOTMgMCAxOS4zMTgtNS43MDggMTkuMzE4LTEzLjYxMSAwLTcuNDY0LTUuNzA4LTExLjQxNS0xNC40ODktMTIuNzMybC00LjM5LS40MzljLTMuOTUxLS40MzktNy4wMjUtMS4zMTctNy4wMjUtMy45NTEgMC0zLjA3MyAzLjA3My00LjgzIDcuOTAzLTQuODMgNS4yNjkgMCAxMC41MzcgMi4xOTUgMTMuMTcxIDMuNTEybDMuOTUxLTcuNDY0em0yNDQuOTktNC4zOWMtNS4yNjkgMC04Ljc4MSAyLjYzNC0xMC45NzYgNi4xNDd2LTUuMjY5aC05LjIydjQyLjE0OWg5LjIydi0yMy43MDljMC03LjAyNSAzLjA3My0xMC45NzYgOC43ODEtMTAuOTc2IDEuNzU2IDAgMy45NTEuNDM5IDUuNzA4Ljg3OGwyLjYzNC04Ljc4MWMtMS43NTYtLjQzOS00LjM5LS40MzktNi4xNDctLjQzOXptLTExNy42NjUgMjEuOTUyYzAgMTIuNzMyIDguNzgxIDIxLjk1MiAyMi4zOTIgMjEuOTUyIDYuMTQ3IDAgMTAuNTM3LTEuMzE3IDE0LjkyOC00LjgzbC00LjM5LTcuNDY0Yy0zLjUxMiAyLjYzNC03LjAyNSAzLjk1MS0xMC45NzYgMy45NTEtNy40NjQgMC0xMi43MzItNS4yNjktMTIuNzMyLTEzLjYxMSAwLTcuOTAzIDUuMjY5LTEzLjE3MSAxMi43MzItMTMuNjExIDMuOTUxIDAgNy40NjQgMS4zMTcgMTAuOTc2IDMuOTUxbDQuMzktNy40NjRjLTQuMzktMy41MTItOC43ODEtNC44My0xNC45MjgtNC44My0xMy42MTEgMC0yMi4zOTIgOS4yMi0yMi4zOTIgMjEuOTUyem04NS4xNzYgMHYtMjEuMDc0aC05LjIydjUuMjY5Yy0zLjA3My0zLjk1MS03LjQ2NC02LjE0Ny0xMy4xNzEtNi4xNDctMTEuODU0IDAtMjEuMDc0IDkuMjItMjEuMDc0IDIxLjk1MiAwIDEyLjczMiA5LjIyIDIxLjk1MiAyMS4wNzQgMjEuOTUyIDYuMTQ3IDAgMTAuNTM3LTIuMTk1IDEzLjE3MS02LjE0N3Y1LjI2OWg5LjIydi0yMS4wNzR6bS0zMy44MDcgMGMwLTcuNDY0IDQuODMtMTMuNjExIDEyLjczMi0xMy42MTEgNy40NjQgMCAxMi43MzIgNS43MDggMTIuNzMyIDEzLjYxMSAwIDcuNDY0LTUuMjY5IDEzLjYxMS0xMi43MzIgMTMuNjExLTcuOTAzLS40MzktMTIuNzMyLTYuMTQ3LTEyLjczMi0xMy42MTF6bS0xMTAuMjAxLTIxLjk1MmMtMTIuMjkzIDAtMjEuMDc0IDguNzgxLTIxLjA3NCAyMS45NTIgMCAxMy4xNzEgOC43ODEgMjEuOTUyIDIxLjUxMyAyMS45NTIgNi4xNDcgMCAxMi4yOTMtMS43NTYgMTcuMTIzLTUuNzA4bC00LjM5LTYuNTg2Yy0zLjUxMiAyLjYzNC03LjkwMyA0LjM5LTEyLjI5MyA0LjM5LTUuNzA4IDAtMTEuNDE1LTIuNjM0LTEyLjczMi0xMC4wOThoMzEuMTczdi0zLjUxMmMuNDM5LTEzLjYxMS03LjQ2NC0yMi4zOTItMTkuMzE4LTIyLjM5MnptMCA3LjkwM2M1LjcwOCAwIDkuNjU5IDMuNTEyIDEwLjUzNyAxMC4wOThoLTIxLjk1MmMuODc4LTUuNzA4IDQuODMtMTAuMDk4IDExLjQxNS0xMC4wOTh6bTIyOC43NDUgMTQuMDV2LTM3Ljc1OGgtOS4yMnYyMS45NTJjLTMuMDczLTMuOTUxLTcuNDY0LTYuMTQ3LTEzLjE3MS02LjE0Ny0xMS44NTQgMC0yMS4wNzQgOS4yMi0yMS4wNzQgMjEuOTUyIDAgMTIuNzMyIDkuMjIgMjEuOTUyIDIxLjA3NCAyMS45NTIgNi4xNDcgMCAxMC41MzctMi4xOTUgMTMuMTcxLTYuMTQ3djUuMjY5aDkuMjJ2LTIxLjA3NHptLTMzLjgwNyAwYzAtNy40NjQgNC44My0xMy42MTEgMTIuNzMyLTEzLjYxMSA3LjQ2NCAwIDEyLjczMiA1LjcwOCAxMi43MzIgMTMuNjExIDAgNy40NjQtNS4yNjkgMTMuNjExLTEyLjczMiAxMy42MTEtNy45MDMtLjQzOS0xMi43MzItNi4xNDctMTIuNzMyLTEzLjYxMXptLTMwOC4yMTMgMHYtMjEuMDc0aC05LjIydjUuMjY5Yy0zLjA3My0zLjk1MS03LjQ2NC02LjE0Ny0xMy4xNzEtNi4xNDctMTEuODU0IDAtMjEuMDc0IDkuMjItMjEuMDc0IDIxLjk1MiAwIDEyLjczMiA5LjIyIDIxLjk1MiAyMS4wNzQgMjEuOTUyIDYuMTQ3IDAgMTAuNTM3LTIuMTk1IDEzLjE3MS02LjE0N3Y1LjI2OWg5LjIydi0yMS4wNzR6bS0zNC4yNDYgMGMwLTcuNDY0IDQuODMtMTMuNjExIDEyLjczMi0xMy42MTEgNy40NjQgMCAxMi43MzIgNS43MDggMTIuNzMyIDEzLjYxMSAwIDcuNDY0LTUuMjY5IDEzLjYxMS0xMi43MzIgMTMuNjExLTcuOTAzLS40MzktMTIuNzMyLTYuMTQ3LTEyLjczMi0xMy42MTF6Ii8+PHBhdGggZmlsbD0iI0ZGNUYwMCIgZD0iTTE4Ni41OTYgMzMuODA3aDEzOC4zMDF2MjQ4LjUwMmgtMTM4LjMwMXoiLz48cGF0aCBkPSJNMTk1LjM3NyAxNTguMDU4YzAtNTAuNDkxIDIzLjcwOS05NS4yNzQgNjAuMTUtMTI0LjI1MS0yNi43ODItMjEuMDc0LTYwLjU4OS0zMy44MDctOTcuNDY5LTMzLjgwNy04Ny4zNzEgMC0xNTguMDU4IDcwLjY4Ny0xNTguMDU4IDE1OC4wNThzNzAuNjg3IDE1OC4wNTggMTU4LjA1OCAxNTguMDU4YzM2Ljg4IDAgNzAuNjg3LTEyLjczMiA5Ny40NjktMzMuODA3LTM2LjQ0MS0yOC41MzgtNjAuMTUtNzMuNzYtNjAuMTUtMTI0LjI1MXoiIGZpbGw9IiNFQjAwMUIiLz48cGF0aCBkPSJNNTExLjQ5MyAxNTguMDU4YzAgODcuMzcxLTcwLjY4NyAxNTguMDU4LTE1OC4wNTggMTU4LjA1OC0zNi44OCAwLTcwLjY4Ny0xMi43MzItOTcuNDY5LTMzLjgwNyAzNi44OC0yOC45NzcgNjAuMTUtNzMuNzYgNjAuMTUtMTI0LjI1MXMtMjMuNzA5LTk1LjI3NC02MC4xNS0xMjQuMjUxYzI2Ljc4Mi0yMS4wNzQgNjAuNTg5LTMzLjgwNyA5Ny40NjktMzMuODA3IDg3LjM3MSAwIDE1OC4wNTggNzEuMTI2IDE1OC4wNTggMTU4LjA1OHoiIGZpbGw9IiNGNzlFMUIiLz48L3N2Zz4=");
}
.rccs__card--unionpay .rccs__issuer {
background-image: url("data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNTEyIiBoZWlnaHQ9IjMyMCIgdmlld0JveD0iMCAwIDUxMiAzMjAiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgcHJlc2VydmVBc3BlY3RSYXRpbz0ieE1pZFlNaWQiPjxwYXRoIGQ9Ik0xMDAuMDgzLjAwMWgxMjcuNTU3YzE3LjgwNiAwIDI4Ljg4IDE0LjUxMyAyNC43MjcgMzIuMzc4bC01OS4zODcgMjU0Ljk3MWMtNC4xOTEgMTcuODAzLTIyLjAxOSAzMi4zMjgtMzkuODM3IDMyLjMyOGgtMTI3LjU0NWMtMTcuNzgxIDAtMjguODgtMTQuNTI2LTI0LjcyNy0zMi4zMjhsNTkuNDEyLTI1NC45NzFjNC4xNTQtMTcuODY1IDIxLjk3Mi0zMi4zNzggMzkuOC0zMi4zNzgiIGZpbGw9IiNFMjE4MzYiLz48cGF0aCBkPSJNMjE3LjAyNy4wMDFoMTQ2LjY4N2MxNy44MDMgMCA5Ljc3NSAxNC41MTMgNS41ODcgMzIuMzc4bC01OS4zNzggMjU0Ljk3MWMtNC4xNjYgMTcuODAzLTIuODY3IDMyLjMyOC0yMC43MDcgMzIuMzI4aC0xNDYuNjg3Yy0xNy44NCAwLTI4Ljg4LTE0LjUyNi0yNC42ODktMzIuMzI4bDU5LjM3NS0yNTQuOTcxYzQuMjE2LTE3Ljg2NSAyMS45OTctMzIuMzc4IDM5LjgxMi0zMi4zNzgiIGZpbGw9IiMwMDQ0N0MiLz48cGF0aCBkPSJNMzU3Ljg5Ni4wMDFoMTI3LjU1N2MxNy44MzEgMCAyOC45MDUgMTQuNTEzIDI0LjcxNyAzMi4zNzhsLTU5LjM3OCAyNTQuOTcxYy00LjE5MSAxNy44MDMtMjIuMDMxIDMyLjMyOC0zOS44NTkgMzIuMzI4aC0xMjcuNDk4Yy0xNy44NCAwLTI4LjkwNS0xNC41MjYtMjQuNzI3LTMyLjMyOGw1OS4zODctMjU0Ljk3MWM0LjE1NC0xNy44NjUgMjEuOTYtMzIuMzc4IDM5LjgtMzIuMzc4IiBmaWxsPSIjMDA3Qjg0Ii8+PHBhdGggZD0iTTEzMy4zOTcgODEuNzEyYy0xMy4xMTcuMTM0LTE2Ljk5MSAwLTE4LjIyOS0uMjkyLS40NzYgMi4yNi05LjMyNyA0My4wOTItOS4zNTIgNDMuMTI2LTEuOTA2IDguMjYxLTMuMjkzIDE0LjE1LTguMDAzIDE3Ljk1Mi0yLjY3NCAyLjIxMS01Ljc5NSAzLjI3Ny05LjQxNCAzLjI3Ny01LjgxNyAwLTkuMjA2LTIuODg4LTkuNzc1LTguMzY3bC0uMTA5LTEuODgxczEuNzcyLTExLjA2NSAxLjc3Mi0xMS4xMjdjMCAwIDkuMjktMzcuMjEgMTAuOTUzLTQyLjEyOC4wODctLjI4LjExMi0uNDI2LjEzNC0uNTYtMTguMDgzLjE1OS0yMS4yODggMC0yMS41MDktLjI5Mi0uMTIxLjQwMS0uNTY5IDIuNzA4LS41NjkgMi43MDhsLTkuNDg2IDQxLjkzOS0uODE1IDMuNTU3LTEuNTc2IDExLjYzNGMwIDMuNDUxLjY3OCA2LjI2OCAyLjAyNyA4LjY1IDQuMzIyIDcuNTUyIDE2LjY0OSA4LjY4NCAyMy42MjMgOC42ODQgOC45ODUgMCAxNy40MTQtMS45MDkgMjMuMTEtNS4zOTQgOS44ODctNS44NDIgMTIuNDc0LTE0Ljk3MyAxNC43ODEtMjMuMDg4bDEuMDctNC4xNjNzOS41Ny0zOC42NDYgMTEuMTk2LTQzLjY3NGMuMDYyLS4yOC4wODctLjQyNi4xNzEtLjU2em0zMi41NjEgMzEuMTc2Yy0yLjMwNyAwLTYuNTIzLjU2LTEwLjMxIDIuNDE2LTEuMzc0LjcwNi0yLjY3NCAxLjUyLTQuMDQ1IDIuMzMybDEuMjM3LTQuNDY4LS42NzgtLjc1MmMtOC4wMzEgMS42MjYtOS44MjggMS44NDQtMTcuMjQ2IDIuODg4bC0uNjIyLjQxNGMtLjg2MSA3LjE0Mi0xLjYyNiAxMi41MTEtNC44MTkgMjYuNTQ5LTEuMjE2IDUuMTc0LTIuNDc4IDEwLjM5Ny0zLjc0MyAxNS41NThsLjM0Mi42NTZjNy42MDItLjQwMSA5LjkwOS0uNDAxIDE2LjUxNi0uMjkybC41MzUtLjU4MWMuODM5LTQuMy45NDgtNS4zMDcgMi44MDgtMTQuMDE2Ljg3NC00LjEyOSAyLjY5Ni0xMy4yMDEgMy41OTQtMTYuNDMyIDEuNjUxLS43NjUgMy4yOC0xLjUxNyA0LjgzNS0xLjUxNyAzLjcwMyAwIDMuMjUyIDMuMjMgMy4xMDkgNC41MTgtLjE1OSAyLjE2MS0xLjUwOCA5LjIxOC0yLjg5MSAxNS4yNzhsLS45MjMgMy45MTFjLS42NDQgMi44ODgtMS4zNDkgNS42OTYtMS45OTMgOC41NTlsLjI4LjU3MmM3LjQ5My0uNDAxIDkuNzc4LS40MDEgMTYuMTc3LS4yOTJsLjc1Mi0uNTgxYzEuMTU3LTYuNzE2IDEuNDk1LTguNTEzIDMuNTQ3LTE4LjI5MWwxLjAzMi00LjQ5M2MyLjAwNS04Ljc5MyAzLjAxMy0xMy4yNTEgMS40OTUtMTYuODgyLTEuNjA0LTQuMDctNS40NTMtNS4wNTItOC45ODgtNS4wNTJ6bTM2LjM3NCA5LjIwNmMtMy45ODMuNzY1LTYuNTIzIDEuMjc1LTkuMDQ3IDEuNjA0LTIuNTAzLjQwMS00Ljk0My43NjUtOC43OTMgMS4zbC0uMzA1LjI3Ny0uMjguMjIxYy0uNDAxIDIuODY3LS42ODEgNS4zNDUtMS4yMTMgOC4yNTgtLjQ1MSAzLjAxMy0xLjE0NCA2LjQzNi0yLjI3MyAxMS4zNTQtLjg3NCAzLjc2NS0xLjMyNCA1LjA3Ny0xLjgyMiA2LjQwMi0uNDg1IDEuMzI0LTEuMDIgMi42MTItMi4wMDIgNi4zMTVsLjIzLjM0Mi4xOTMuMzE0YzMuNTk3LS4xNzEgNS45NTEtLjI5MiA4LjM3LS4zMTQgMi40MTYtLjA4NyA0LjkxOSAwIDguNzkzLjAyMmwuMzM5LS4yNzcuMzY0LS4zMDVjLjU2LTMuMzM5LjY0NC00LjIzOC45ODYtNS44NjcuMzM5LTEuNzQ3LjkyMy00LjE2NiAyLjM1Ny0xMC42MjcuNjc4LTMuMDM0IDEuNDMzLTYuMDYgMi4xMzYtOS4xNTYuNzMxLTMuMDg0IDEuNDk1LTYuMTIyIDIuMjIzLTkuMTU2bC0uMTA5LS4zNjctLjE0Ni0uMzM5em0uMDg1LTEyLjQxMmMtMy42MTktMi4xMzYtOS45NzEtMS40NTgtMTQuMjQ2IDEuNDkyLTQuMjYzIDIuODkxLTQuNzQ4IDYuOTk1LTEuMTQxIDkuMTU5IDMuNTU3IDIuMDc3IDkuOTM0IDEuNDU4IDE0LjE3MS0xLjUxNyA0LjI1My0yLjk1NCA0Ljc4NS03LjAyIDEuMjE2LTkuMTM1em0yMS44ODcgNDkuNDY3YzcuMzIyIDAgMTQuODI3LTIuMDE4IDIwLjQ3Ny04LjAwNiA0LjM0Ny00Ljg1NiA2LjMzOS0xMi4wODIgNy4wMy0xNS4wNTcgMi4yNDgtOS44NjIuNDk3LTE0LjQ2Ny0xLjcwMS0xNy4yNzEtMy4zMzktNC4yNzUtOS4yNC01LjY0Ni0xNS4zNjItNS42NDYtMy42ODEgMC0xMi40NDkuMzY0LTE5LjI5OCA2LjY3OC00LjkxOSA0LjU1NS03LjE5MSAxMC43MzYtOC41NjIgMTYuNjYyLTEuMzg0IDYuMDM4LTIuOTc1IDE2LjkwNyA3LjAyIDIwLjk1MiAzLjA4NCAxLjMyNCA3LjUzIDEuNjg4IDEwLjM5NyAxLjY4OHptLS41NzItMjIuMTljMS42ODgtNy40NjggMy42ODEtMTMuNzM2IDguNzY4LTEzLjczNiAzLjk4NiAwIDQuMjc1IDQuNjY0IDIuNTAzIDEyLjE1Ny0uMzE3IDEuNjYzLTEuNzcyIDcuODQ3LTMuNzQgMTAuNDgxLTEuMzc0IDEuOTQzLTMgMy4xMjItNC43OTcgMy4xMjItLjUzNSAwLTMuNzE1IDAtMy43NjUtNC43MjMtLjAyNS0yLjMzMi40NTEtNC43MTMgMS4wMzItNy4zem00Ni4zODIgMjEuMjI5bC41NzItLjU4MWMuODExLTQuMy45NDUtNS4zMSAyLjc0Mi0xNC4wMTYuODk5LTQuMTI5IDIuNzU4LTEzLjIwMSAzLjYzMS0xNi40MzIgMS42NTQtLjc2OCAzLjI1NS0xLjUyIDQuODYtMS41MiAzLjY3OCAwIDMuMjMgMy4yMyAzLjA4NCA0LjUxOC0uMTM0IDIuMTY0LTEuNDgzIDkuMjE4LTIuODkxIDE1LjI3OGwtLjg3NCAzLjkxMWMtLjY2OCAyLjg5MS0xLjM5NiA1LjY5Ni0yLjA0IDguNTYybC4yOC41NzJjNy41MTgtLjQwMSA5LjcxNi0uNDAxIDE2LjE1Mi0uMjkybC43NzctLjU4MWMxLjEyOS02LjcxOSAxLjQzMy04LjUxNiAzLjU0Ny0xOC4yOTFsMS4wMDctNC40OTZjMi4wMTUtOC43OTMgMy4wMzQtMTMuMjQ4IDEuNTQyLTE2Ljg3OS0xLjY1MS00LjA3LTUuNTI1LTUuMDUyLTkuMDEtNS4wNTItMi4zMSAwLTYuNTQ4LjU1Ny0xMC4zMTMgMi40MTYtMS4zNDYuNzA2LTIuNjk2IDEuNTE3LTQuMDIgMi4zMzJsMS4xNTMtNC40NjgtLjYxOS0uNzU2Yy04LjAyOCAxLjYyOS05Ljg2MiAxLjg0Ny0xNy4yNzEgMi44OTFsLS41NjkuNDE0Yy0uODk5IDcuMTQyLTEuNjI5IDEyLjUwOC00LjgyMiAyNi41NDktMS4yMTYgNS4xNzQtMi40NzggMTAuMzk3LTMuNzQgMTUuNTU4bC4zMzkuNjU2YzcuNjE0LS40MDEgOS44ODctLjQwMSAxNi40ODEtLjI5MnptNTUuMjM1LjI5MWMuNDczLTIuMzA3IDMuMjgtMTUuOTgxIDMuMzA1LTE1Ljk4MSAwIDAgMi4zOTEtMTAuMDMzIDIuNTM3LTEwLjM5NyAwIDAgLjc1Mi0xLjA0NSAxLjUwNS0xLjQ1OGgxLjEwN2MxMC40NDMgMCAyMi4yMzYgMCAzMS40OC02LjggNi4yOS00LjY2NCAxMC41OS0xMS41NSAxMi41MDgtMTkuOTIuNDk3LTIuMDUyLjg2NC00LjQ5My44NjQtNi45MzMgMC0zLjIwNS0uNjQ0LTYuMzc3LTIuNTAzLTguODU1LTQuNzEzLTYuNTk0LTE0LjEtNi43MTYtMjQuOTM1LTYuNzY1bC01LjM0MS4wNWMtMTMuODcuMTcxLTE5LjQzMi4xMjEtMjEuNzE3LS4xNTktLjE5MyAxLjAxLS41NTcgMi44MDgtLjU1NyAyLjgwOHMtNC45NjggMjMuMDI2LTQuOTY4IDIzLjA2M2MwIDAtMTEuODg5IDQ4Ljk1Ni0xMi40NDkgNTEuMjYzIDEyLjExLS4xNDYgMTcuMDc1LS4xNDYgMTkuMTY1LjA4NHptOS4yMDYtNDAuOTAzczUuMjgyLTIyLjk3OSA1LjI1Ny0yMi44OTJsLjE3MS0xLjE3OC4wNzUtLjg5OSAyLjExMS4yMThzMTAuODk0LjkzNiAxMS4xNDkuOTYxYzQuMyAxLjY2MyA2LjA3MiA1Ljk1MSA0LjgzNSAxMS41NDctMS4xMjkgNS4xMTQtNC40NDYgOS40MTQtOC43MDkgMTEuNDkxLTMuNTEgMS43Ni03LjgxIDEuOTA2LTEyLjI0MSAxLjkwNmgtMi44NjdsLjIxOC0xLjE1M3ptMzIuODg5IDE5LjgwOWMtMS4zOTYgNS45NTEtMyAxNi44MiA2Ljk0NiAyMC42OTQgMy4xNzEgMS4zNDkgNi4wMTMgMS43NSA4LjkwMSAxLjYwNCAzLjA1LS4xNjUgNS44NzYtMS42OTQgOC40OTQtMy44OTZsLS43MDkgMi43MTcuNDUxLjU4MWM3LjE1NC0uMzAyIDkuMzc0LS4zMDIgMTcuMTI1LS4yNDNsLjcwMy0uNTM1YzEuMTMyLTYuNjUzIDIuMTk4LTEzLjExNCA1LjEzOS0yNS44NDMgMS40MzMtNi4wOTcgMi44NjMtMTIuMTM1IDQuMzM0LTE4LjIwN2wtLjIzLS42NjhjLTguMDAzIDEuNDgzLTEwLjE0MiAxLjgtMTcuODQgMi44OTFsLS41ODUuNDc2LS4yMzMgMS44MDZjLTEuMTk3LTEuOTM0LTIuOTMyLTMuNTg1LTUuNjA5LTQuNjE0LTMuNDIzLTEuMzQ2LTExLjQ2My4zODktMTguMzc1IDYuNjgxLTQuODU2IDQuNDkzLTcuMTg4IDEwLjY0OS04LjUxMyAxNi41NTN6bTE2LjgwOC4zNjRjMS43MTMtNy4zMzQgMy42ODEtMTMuNTQgOC43OC0xMy41NCAzLjIyNCAwIDQuOTIyIDIuOTc1IDQuNTc3IDguMDQ5LS4yNzQgMS4yNjUtLjU2OSAyLjU5OS0uOTIgNC4xMDctLjUxIDIuMTc5LTEuMDYzIDQuMzQtMS42MDEgNi41MDQtLjU0NyAxLjQ4LTEuMTg1IDIuODc2LTEuODg0IDMuODA2LTEuMzEyIDEuODU5LTQuNDM0IDMuMDEzLTYuMjMxIDMuMDEzLS41MSAwLTMuNjU2IDAtMy43NjUtNC42MzktLjAyNS0yLjMxLjQ1MS00LjY4OSAxLjA0NS03LjN6bTg3Ljc3Mi0yNC4yMTdsLS42MTktLjcwNmMtNy45MTkgMS42MDQtOS4zNTIgMS44NTktMTYuNjI3IDIuODQybC0uNTM1LjUzNS0uMDg0LjM0Mi0uMDI1LS4xMjFjLTUuNDE2IDEyLjQ5NS01LjI1NyA5LjgtOS42NjYgMTkuNjM3bC0uMDUtMS4yMDMtMS4xMDQtMjEuMzI1LS42OTMtLjcwNmMtOC4yOTUgMS42MDQtOC40OTEgMS44NTktMTYuMTUyIDIuODQybC0uNTk3LjUzNWMtLjA4NC4yNTUtLjA4NC41MzUtLjEzNC44MzlsLjA1LjEwOWMuOTU4IDQuODk0LjcyOCAzLjgwMiAxLjY4OCAxMS41MjUuNDQ4IDMuNzkgMS4wNDUgNy42MDIgMS40OTIgMTEuMzQ1Ljc1NiA2LjI2NSAxLjE3OCA5LjM0OSAyLjEwMiAxOC45MS01LjE3NCA4LjUzOC02LjM5OSAxMS43NjgtMTEuMzc5IDE5LjI2MWwuMDM0LjA3NS0zLjUwNyA1LjU0N2MtLjQwMS41ODUtLjc2NS45ODYtMS4yNzUgMS4xNTctLjU2LjI3Ny0xLjI4Ny4zMjYtMi4yOTguMzI2aC0xLjk0M2wtMi44ODggOS42MDcgOS45MDkuMTcxYzUuODE3LS4wMjUgOS40NzMtMi43NDUgMTEuNDQxLTYuNDAybDYuMjMxLTEwLjY3N2gtLjA5OWwuNjU2LS43NTJjNC4xOTEtOS4wMjMgMzYuMDcyLTYzLjcxMiAzNi4wNzItNjMuNzEyem0tMTA0LjU4IDEyNi4xNzVoLTQuMjA0bDE1LjU1OC01MS40NTloNS4xNjFsMS42MzgtNS4zMDEuMTU5IDUuODk1Yy0uMTkzIDMuNjQ0IDIuNjc0IDYuODc0IDEwLjIwNCA2LjMzOWg4LjcwOWwyLjk5Ny05LjkwOWgtMy4yNzdjLTEuODg0IDAtMi43NTgtLjQ3Ni0yLjY0OS0xLjQ5NWwtLjE1OS01Ljk5N2gtMTYuMTI3di4wMzFjLTUuMjE0LjEwOS0yMC43ODQuNTAxLTIzLjkzNyAxLjM0LTMuODE1Ljk4Mi03LjgzNSAzLjg3NC03LjgzNSAzLjg3NGwxLjU3OS01LjMwN2gtMTUuMDg1bC0zLjE0MyAxMC41MzEtMTUuNzY2IDUyLjI0NWgtMy4wNTlsLTMgOS44MzdoMzAuMDQ2bC0xLjAwNyAzLjI4aDE0LjgwNmwuOTgyLTMuMjhoNC4xNTRsMy4yNTUtMTAuNjI0em0tMTIuMzI4LTQxLjAwM2MtMi40MTYuNjY4LTYuOTEyIDIuNjk2LTYuOTEyIDIuNjk2bDMuOTk4LTEzLjE1MmgxMS45ODZsLTIuODkxIDkuNTgycy0zLjcwMy4yMTgtNi4xODEuODc0em0uMjMgMTguNzg4cy0zLjc2NS40NzMtNi4yNDMgMS4wMzJjLTIuNDQxLjc0LTcuMDE3IDMuMDcyLTcuMDE3IDMuMDcybDQuMTI5LTEzLjY4NmgxMi4wNDhsLTIuOTE2IDkuNTgyem0tNi43MTYgMjIuMzM2aC0xMi4wMjNsMy40ODUtMTEuNTVoMTEuOTg2bC0zLjQ0OCAxMS41NXptMjguOTU0LTMxLjkxOGgxNy4zM2wtMi40OSA4LjA2NWgtMTcuNTZsLTIuNjM3IDguODE3aDE1LjM2NWwtMTEuNjM0IDE2LjM4MmMtLjgxNSAxLjIwMy0xLjU0NSAxLjYyOS0yLjM1NyAxLjk2OC0uODE1LjQxNC0xLjg4NC44OTktMy4xMjIuODk5aC00LjI2M2wtMi45MjkgOS42NTdoMTEuMTQ5YzUuNzk1IDAgOS4yMTgtMi42MzcgMTEuNzQ2LTYuMDk3bDcuOTc4LTEwLjkxOSAxLjcxMyAxMS4wODdjLjM2NCAyLjA3NyAxLjg1NiAzLjI5MyAyLjg2NyAzLjc2NSAxLjExNi41NiAyLjI3IDEuNTIgMy44OTkgMS42NjMgMS43NDcuMDc1IDMuMDEuMTM0IDMuODQ5LjEzNGg1LjQ3OGwzLjI4OS0xMC44MDdoLTIuMTYxYy0xLjI0MSAwLTMuMzc2LS4yMDgtMy43NC0uNTk3LS4zNjQtLjQ3My0uMzY0LTEuMi0uNTYtMi4zMDdsLTEuNzM4LTExLjExMmgtNy4xMTdsMy4xMjItMy43MTVoMTcuNTI2bDIuNjk2LTguODE3aC0xNi4yMjZsMi41MjgtOC4wNjVoMTYuMTc3bDMtOS45NDZoLTQ4LjIyOGwtMi45NTEgOS45NDZ6bS0xNDYuMzcxIDM0LjE2NGw0LjA0NS0xMy40NTZoMTYuNjI0bDMuMDM4LTEwLjAwOGgtMTYuNjRsMi41NC04LjI4M2gxNi4yNjFsMy4wMTMtOS42OTFoLTQwLjY4NmwtMi45NTEgOS42OTFoOS4yNDNsLTIuNDY2IDguMjgzaC05LjI2OGwtMy4wNzIgMTAuMTc5aDkuMjRsLTUuMzkxIDE3LjgwM2MtLjcyOCAyLjM1Ny4zNDIgMy4yNTUgMS4wMiA0LjM1LjY5MyAxLjA2NiAxLjM5NiAxLjc3MiAyLjk3NSAyLjE3MyAxLjYyOS4zNjQgMi43NDUuNTgxIDQuMjYzLjU4MWgxOC43NDJsMy4zMzktMTEuMDg3LTguMzA4IDEuMTQxYy0xLjYwNCAwLTYuMDQ3LS4xOTMtNS41NjItMS42NzZ6bTEuOTA3LTY0LjQxN2wtNC4yMTMgNy42MTRjLS45MDIgMS42NjMtMS43MTMgMi42OTYtMi40NDQgMy4xNzEtLjY0NC40MDEtMS45MTguNTY5LTMuNzY1LjU2OWgtMi4xOThsLTIuOTM4IDkuNzQxaDcuM2MzLjUxIDAgNi4yMDYtMS4yODcgNy40OTMtMS45MzEgMS4zODQtLjc0IDEuNzQ3LS4zMTcgMi44MTctMS4zNDlsMi40NjYtMi4xMzZoMjIuNzk2bDMuMDI1LTEwLjE0MmgtMTYuNjg3bDIuOTEzLTUuNTM3aC0xNi41NjV6bTMzLjY1NCA2NC42MTJjLS4zODktLjU2LS4xMDktMS41NDUuNDg1LTMuNTk3bDYuMjMxLTIwLjYyM2gyMi4xNjVjMy4yMy0uMDQ3IDUuNTYyLS4wODQgNy4wNzktLjE5MyAxLjYyOS0uMTcxIDMuNDAxLS43NTIgNS4zMzItMS43OTcgMS45OTMtMS4wOTQgMy4wMTMtMi4yNDggMy44NzQtMy41NzIuOTYxLTEuMzIxIDIuNTAzLTQuMjEzIDMuODI3LTguNjcxbDcuODMyLTI2LjA5OC0yMy4wMDEuMTM0cy03LjA4MyAxLjA0NS0xMC4yMDEgMi4xOThjLTMuMTQ2IDEuMjg3LTcuNjQyIDQuODgxLTcuNjQyIDQuODgxbDIuMDc3LTcuMTU0aC0xNC4yMDlsLTE5Ljg5MiA2NS45NzJjLS43MDYgMi41NjItMS4xNzggNC40MjEtMS4yODcgNS41MzctLjAzNyAxLjIwMyAxLjUxNyAyLjM5NCAyLjUyNSAzLjI5MyAxLjE5MS44OTkgMi45NTEuNzUyIDQuNjM5Ljg5OSAxLjc3NS4xMzQgNC4zLjIxOCA3Ljc4NS4yMThoMTAuOTE5bDMuMzUyLTExLjMxNy05Ljc3NS45MjNjLTEuMDQ1IDAtMS44LS41Ni0yLjExNC0xLjAzMnptMTAuNzM2LTM4LjE0OWgyMy4yODFsLTEuNDggNC42MzljLS4yMDguMTA5LS43MDYtLjIzLTMuMDc1LjA1aC0yMC4xNTlsMS40MzMtNC42ODl6bTQuNjY0LTE1LjU1OGgyMy40NzdsLTEuNjg4IDUuNTg3cy0xMS4wNjUtLjEwOS0xMi44MzcuMjE4Yy03Ljc5OCAxLjM0OS0xMi4zNTIgNS41MTYtMTIuMzUyIDUuNTE2bDMuNDAxLTExLjMyem0xNy42NTggMzUuNzMxYy0uMTkzLjY5My0uNDk3IDEuMTE2LS45MjMgMS40MzMtLjQ3My4zMDUtMS4yMzcuNDE0LTIuMzc4LjQxNGgtMy4zMTdsLjE5Ni01LjY0OWgtMTMuNzk4bC0uNTYgMjcuNjE4Yy0uMDIyIDEuOTkzLjE3MSAzLjE0NiAxLjYyOSA0LjA3IDEuNDU4IDEuMTUzIDUuOTUxIDEuMyAxMS45OTggMS4zaDguNjQ2bDMuMTIyLTEwLjMzOC03LjUyNy40MTQtMi41MDMuMTQ2Yy0uMzQyLS4xNDYtLjY2OC0uMjgtMS4wMzItLjY0NC0uMzE3LS4zMTQtLjg1Mi0uMTIxLS43NjUtMi4xMTRsLjA1OS03LjA3OSA3Ljg5NC0uMzI2YzQuMjYzIDAgNi4wODUtMS4zODcgNy42MzktMi43MDggMS40ODMtMS4yNjUgMS45NjgtMi43MiAyLjUyOC00LjY4OWwxLjMyNC02LjI2OGgtMTAuODQ4bC0xLjM4NCA0LjQyMXoiIGZpbGw9IiNGRUZFRkUiLz48L3N2Zz4=");
}
.rccs__card--visa .rccs__card__background,
.rccs__card--visaelectron .rccs__card__background {
background: linear-gradient(25deg, #0f509e, #1399cd);
}
.rccs__card--visa .rccs__issuer,
.rccs__card--visaelectron .rccs__issuer {
background-size: 75%;
}
.rccs__card--visa .rccs__issuer {
background-image: url("data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz48c3ZnIHdpZHRoPSI1MTJweCIgaGVpZ2h0PSIxNjZweCIgdmlld0JveD0iMCAwIDUxMiAxNjYiIHZlcnNpb249IjEuMSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB4bWxuczp4bGluaz0iaHR0cDovL3d3dy53My5vcmcvMTk5OS94bGluayIgcHJlc2VydmVBc3BlY3RSYXRpbz0ieE1pZFlNaWQiPiAgICA8Zz4gICAgICAgIDxwYXRoIGQ9Ik0yNjQuNzk0MTg3LDExMi40Nzk0OTEgQzI2NC41MDIwNzIsODkuNDQ4NTYxNiAyODUuMzE5MDgsNzYuNTk1NTE5OCAzMDEuMDAxMDIxLDY4Ljk1NDQxNzIgQzMxNy4xMTM0NDcsNjEuMTEzNDQ2NiAzMjIuNTI1MjU0LDU2LjA4NjAwMDggMzIyLjQ2Mzc1Niw0OS4wNzUyNTA3IEMzMjIuMzQwNzYsMzguMzQzODgzMyAzMDkuNjEwNzE0LDMzLjYwODU1MiAyOTcuNjk1NTE0LDMzLjQyNDA1ODYgQzI3Ni45MDkyNTUsMzMuMTAxMTk1MSAyNjQuODI0OTM1LDM5LjAzNTczMzYgMjU1LjIxNTkwMyw0My41MjUwNzM2IEwyNDcuNzI4NTQ1LDguNDg2Njk3NSBDMjU3LjM2ODMyNiw0LjA0MzQ4MDg3IDI3NS4yMTgwNjUsMC4xNjkxMTg5NzIgMjkzLjcyODkwNSwtMS40MjEwODU0N2UtMTQgQzMzNy4xNzcxMDYsLTEuNDIxMDg1NDdlLTE0IDM2NS42MDQ0NjgsMjEuNDQ3MzYwNSAzNjUuNzU4MjEzLDU0LjcwMjMwMDIgQzM2NS45MjczMzIsOTYuOTA1MTcwOSAzMDcuMzgxNDE5LDk5LjI0MjA4NzYgMzA3Ljc4MTE1NCwxMTguMTA2NTQgQzMwNy45MTk1MjQsMTIzLjgyNTgzNiAzMTMuMzc3NDU1LDEyOS45Mjk0OTQgMzI1LjMzODc3OCwxMzEuNDgyMzEzIEMzMzEuMjU3OTQyLDEzMi4yNjY0MSAzNDcuNjAwOTg1LDEzMi44NjYwMTQgMzY2LjEyNzIsMTI0LjMzMzE5MyBMMzczLjM5OTMxNSwxNTguMjMzODYgQzM2My40MzY2NywxNjEuODYyMjMgMzUwLjYyOTc1MiwxNjUuMzM2ODU3IDMzNC42ODY0NDUsMTY1LjMzNjg1NyBDMjkzLjc5MDQwMywxNjUuMzM2ODU3IDI2NS4wMjQ4MDMsMTQzLjU5NzM4MiAyNjQuNzk0MTg3LDExMi40Nzk0OTEgTTQ0My4yNzYyLDE2Mi40MTU3MTEgQzQzNS4zNDI5ODIsMTYyLjQxNTcxMSA0MjguNjU1MDk2LDE1Ny43ODgwMDEgNDI1LjY3MjQ1MiwxNTAuNjg1MDA0IEwzNjMuNjA1Nzg5LDIuNDkwNjYxMjIgTDQwNy4wMjMyNDIsMi40OTA2NjEyMiBMNDE1LjY2MzY4NCwyNi4zNjcxODUyIEw0NjguNzIwOTE4LDI2LjM2NzE4NTIgTDQ3My43MzI5ODksMi40OTA2NjEyMiBMNTEyLDIuNDkwNjYxMjIgTDQ3OC42MDY2OSwxNjIuNDE1NzExIEw0NDMuMjc2MiwxNjIuNDE1NzExIE00NDkuMzQ5MTA4LDExOS4yMTM1MDEgTDQ2MS44NzkyODcsNTkuMTYwODkxMiBMNDI3LjU2MzUxLDU5LjE2MDg5MTIgTDQ0OS4zNDkxMDgsMTE5LjIxMzUwMSBNMjEyLjE1MjA2MywxNjIuNDE1NzExIEwxNzcuOTI4NTMzLDIuNDkwNjYxMjIgTDIxOS4zMDExODMsMi40OTA2NjEyMiBMMjUzLjUwOTMzOSwxNjIuNDE1NzExIEwyMTIuMTUyMDYzLDE2Mi40MTU3MTEgTTE1MC45NDYzNywxNjIuNDE1NzExIEwxMDcuODgyNTMsNTMuNTY0NTkwNyBMOTAuNDYzMjc1NSwxNDYuMTE4NzkyIEM4OC40MTg0NzM0LDE1Ni40NTA0MjMgODAuMzQ2ODg2MSwxNjIuNDE1NzExIDcxLjM4MzU4MDYsMTYyLjQxNTcxMSBMMC45ODM5NjQ5MjcsMTYyLjQxNTcxMSBMMCwxNTcuNzcyNjI2IEMxNC40NTE5ODQ5LDE1NC42MzYyMzggMzAuODcxODk5NiwxNDkuNTc4MDQzIDQwLjgxOTE3LDE0NC4xNjYyMzYgQzQ2LjkwNzQ1MywxNDAuODYwNzI5IDQ4LjY0NDc2NjEsMTM3Ljk3MDMzMiA1MC42NDM0NDQ4LDEzMC4xMTM5ODcgTDgzLjYzNzAxODgsMi40OTA2NjEyMiBMMTI3LjM2MTk2LDIuNDkwNjYxMjIgTDE5NC4zOTQ1NzEsMTYyLjQxNTcxMSBMMTUwLjk0NjM3LDE2Mi40MTU3MTEiIGZpbGw9IiNGRkZGRkYiIHRyYW5zZm9ybT0idHJhbnNsYXRlKDI1Ni4wMDAwMDAsIDgyLjY2ODQyOCkgc2NhbGUoMSwgLTEpIHRyYW5zbGF0ZSgtMjU2LjAwMDAwMCwgLTgyLjY2ODQyOCkgIj48L3BhdGg+ICAgIDwvZz48L3N2Zz4=");
}
.rccs__card--visaelectron .rccs__issuer {
background-image: url("data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz48c3ZnIHdpZHRoPSI1MTJweCIgaGVpZ2h0PSIyMjhweCIgdmlld0JveD0iMCAwIDUxMiAyMjgiIHZlcnNpb249IjEuMSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB4bWxuczp4bGluaz0iaHR0cDovL3d3dy53My5vcmcvMTk5OS94bGluayIgcHJlc2VydmVBc3BlY3RSYXRpbz0ieE1pZFlNaWQiPiAgICA8Zz4gICAgICAgIDxwYXRoIGQ9Ik0xOTQuMzkzMDEyLDIuOTE5MzQ0MTMgTDEyNy4zNjI1MTcsMTYyLjgzNTk4MSBMODMuNjI3OTMwNywxNjIuODM1OTgxIEw1MC42MzkwMTUyLDM1LjIwODY2MzIgQzQ4LjY0MTU2OTIsMjcuMzY1OTkwNyA0Ni45MDIzODU1LDI0LjQ3OTMzOCA0MC44MTUyNDI0LDIxLjE2NDQyNzYgQzMwLjg2NzI0MjIsMTUuNzYwNTM1MyAxNC40Mzk4MDE4LDEwLjcwNjQ0MTIgMCw3LjU2MTUyNjI4IEwwLjk3NDIwNDQyNCwyLjkxOTM0NDEzIEw3MS4zODE3MjM1LDIuOTE5MzQ0MTMgQzgwLjM0MjQ0MjcsMi45MTkzNDQxMyA4OC40MTA2ODYsOC44ODU1Mjg5NCA5MC40NjA0MzgzLDE5LjIxNjAxODggTDEwNy44OTE1MDUsMTExLjc2MjE3IEwxNTAuOTIzMjI2LDIuOTE5MzQ0MTMgTDE5NC4zOTMwMTIsMi45MTkzNDQxMyBaIE0yNTMuNTE1NDUyLDIuOTE5MzQ0MTMgTDIxOS4zMDcxNDYsMTYyLjgzNTk4MSBMMTc3LjkzMjg4LDE2Mi44MzU5ODEgTDIxMi4xNDExODYsMi45MTkzNDQxMyBMMjUzLjUxNTQ1MiwyLjkxOTM0NDEzIFogTTMwNy43Nzk5NDYsNDcuMjI5Mjk5NyBDMzA3LjkyMDUxOSw0MS40OTg0OTMxIDMxMy4zNzk5ODcsMzUuMzk1MDA0MyAzMjUuMzMxOTcxLDMzLjgzNTYyMzQgQzMzMS4yNTIzODgsMzMuMDcwNjQ0MSAzNDcuNjA3OTA3LDMyLjQ1OTMxNDUgMzY2LjEzNDEzNyw0MC45OTUwNDUyIEwzNzMuMzgxODI2LDcuMDY0NjE2NjQgQzM2My40MjcyODcsMy40NjUyOTA5IDM1MC42MTg3ODcsMCAzMzQuNjg0OTg3LDAgQzI5My43ODQ3NDcsMCAyNjUuMDEzMDI1LDIxLjcyMzQ1MSAyNjQuNzg0MTg2LDUyLjg1NTQ5MzcgQzI2NC41MTYxMTYsNzUuODgzMzMyNSAyODUuMzM0MDE0LDg4LjcxNzk4NTQgMzAwLjk4OTkzNyw5Ni4zODczOTM0IEMzMTcuMTE2NjE2LDEwNC4yMjAyNTggMzIyLjUyMzc3OCwxMDkuMjU0NzM4IDMyMi40NDg1ODgsMTE2LjI1Mzk3MSBDMzIyLjMzNDE2OCwxMjYuOTgzMjk3IDMwOS41NzE0MzYsMTMxLjczMzM2MSAyOTcuNzA0NDQ5LDEzMS45MDk4OTQgQzI3Ni44OTYzNTgsMTMyLjI0MDA3OCAyNjQuODMzMjIzLDEyNi4yODY5NjkgMjU1LjIxMjEzNywxMjEuODA0OTc1IEwyNDcuNzAyOTE3LDE1Ni44NTk5ODkgQzI1Ny4zODI4NDcsMTYxLjI5NjIxNSAyNzUuMjIyNTU3LDE2NS4xNTA1MzQgMjkzLjY5OTc1LDE2NS4zNDY2ODIgQzMzNy4xNzkzNDMsMTY1LjM0NjY4MiAzNjUuNjE3NjEzLDE0My44NzE2ODYgMzY1Ljc0NTEwOSwxMTAuNjI0NTA4IEMzNjUuOTE4Mzc0LDY4LjQyNjQxODggMzA3LjM5NzQ1Niw2Ni4wODU3MTI4IDMwNy43Nzk5NDYsNDcuMjI5Mjk5NyBaIE00NzMuNzQ3NzY1LDE2Mi44MzU5ODEgTDUxMiwxNjIuODM1OTgxIEw0NzguNTc5NTU4LDIuOTE5MzQ0MTMgTDQ0My4yODU4OTcsMi45MTkzNDQxMyBDNDM1LjMyNTUzNSwyLjkxOTM0NDEzIDQyOC42MzY4Nyw3LjUzODY0MjI4IDQyNS42Njg0ODksMTQuNjQyNDg4NiBMMzYzLjU4NzQ3NSwxNjIuODM1OTgxIEw0MDcuMDM0Mzc3LDE2Mi44MzU5ODEgTDQxNS42NTE4MzYsMTM4Ljk0ODM1OCBMNDY4LjczMjkwMSwxMzguOTQ4MzU4IEw0NzMuNzQ3NzY1LDE2Mi44MzU5ODEgWiBNNDI3LjU2NDU5MSwxMDYuMTc4NDc1IEw0NDkuMzUwMTU2LDQ2LjEyNDMyOTYgTDQ2MS44NjQ0MzMsMTA2LjE3ODQ3NSBMNDI3LjU2NDU5MSwxMDYuMTc4NDc1IFogTTMwNi40NjQ3NywyMDYuODU0MzI5IEwyODkuNjU0ODQsMjA2Ljg1NDMyOSBMMjg5LjY1NDg0LDIyMi41MDA0NDQgTDMwOC40NTg5NDcsMjIyLjUwMDQ0NCBMMzA4LjQ1ODk0NywyMjcuMjA0NzQgTDI4My45ODk0MTYsMjI3LjIwNDc0IEwyODMuOTg5NDE2LDE4My43OTcwNjggTDMwNy40OTQ1NSwxODMuNzk3MDY4IEwzMDcuNDk0NTUsMTg4LjUwMTM2NCBMMjg5LjY1NDg0LDE4OC41MDEzNjQgTDI4OS42NTQ4NCwyMDIuMjE1NDE2IEwzMDYuNDY0NzcsMjAyLjIxNTQxNiBMMzA2LjQ2NDc3LDIwNi44NTQzMjkgWiBNMzE3LjE0MzQyMywxODEuNDgxMjA4IEwzMjIuODA4ODQ3LDE4MS40ODEyMDggTDMyMi44MDg4NDcsMjI3LjIwMzQzMyBMMzE3LjE0MzQyMywyMjcuMjAzNDMzIEwzMTcuMTQzNDIzLDE4MS40ODEyMDggWiBNMzQ3Ljc4NzM4MywyMjMuNDY5MDkxIEMzNTEuODQ3NjU4LDIyMy40NjkwOTEgMzU0LjM1ODM1OSwyMjIuNzU5Njg3IDM1Ni40MjExODgsMjIxLjg2MDY3MyBMMzU3LjQ1MDk2OCwyMjUuOTE3Njc5IEMzNTUuNDUzNTIyLDIyNi44MTY2OTMgMzUxLjk3ODQyNCwyMjcuOTExODU2IDM0Ny4wMTU4NjYsMjI3LjkxMTg1NiBDMzM3LjQyNDIwMiwyMjcuOTExODU2IDMzMS42OTMzOTUsMjIxLjUzNzAyOCAzMzEuNjkzMzk1LDIxMi4xMzQ5NzUgQzMzMS42OTMzOTUsMjAyLjczMjkyMSAzMzcuMjI4MDUzLDE5NS4zOTA0MjggMzQ2LjMwOTczMSwxOTUuMzkwNDI4IEMzNTYuNTQ4Njg1LDE5NS4zOTA0MjggMzU5LjE5MDE1MiwyMDQuMjc5MjI2IDM1OS4xOTAxNTIsMjEwLjAxMDAzMiBDMzU5LjE5MDE1MiwyMTEuMTY3MzA5IDM1OS4xMjQ3NjksMjEyLjAwNDIwOSAzNTguOTk0MDAzLDIxMi42NDgyMyBMMzM3LjEwMDU1NywyMTIuNjQ4MjMgQzMzNy4yMjgwNTMsMjIwLjMxNDM2OSAzNDIuMDU5ODQ2LDIyMy40NjkwOTEgMzQ3Ljc4NzM4MywyMjMuNDY5MDkxIFogTTM1My43MTQzMzgsMjA4LjU5MTIyNCBDMzUzLjc3OTcyMSwyMDUuMDUwNzQzIDM1Mi4yMzM0MTcsMTk5LjQ0NzQzMyAzNDUuODU4NTg5LDE5OS40NDc0MzMgQzM0MC4wNjI0LDE5OS40NDc0MzMgMzM3LjYxMzgxMiwyMDQuNjY0OTg0IDMzNy4xNjI2NywyMDguNTkxMjI0IEwzNTMuNzE0MzM4LDIwOC41OTEyMjQgWiBNMzkwLjAyMjc0MSwyMjYuMTA4NTk3IEMzODguNTQxODE5LDIyNi44MTgwMDEgMzg1LjI1NjMzMSwyMjcuOTEzMTYzIDM4MS4wNzE4MjksMjI3LjkxMzE2MyBDMzcxLjY2OTc3NiwyMjcuOTEzMTYzIDM2NS41NTMyMSwyMjEuNTM4MzM2IDM2NS41NTMyMSwyMTIuMDA1NTE3IEMzNjUuNTUzMjEsMjAyLjQxMDU4NCAzNzIuMTE3NjQ4LDE5NS4zOTE3MzUgMzgyLjI5NDQ4OCwxOTUuMzkxNzM1IEMzODUuNjQyMDksMTk1LjM5MTczNSAzODguNjAzOTMzLDE5Ni4yMjUzNjcgMzkwLjE1MDIzOCwxOTcuMDY1NTM2IEwzODguODY1NDY1LDIwMS4zODA4MDQgQzM4Ny41MDg3NywyMDAuNjcxNCAzODUuMzgzODI4LDE5OS44OTY2MTMgMzgyLjI5NDQ4OCwxOTkuODk2NjEzIEMzNzUuMTQ0ODc0LDE5OS44OTY2MTMgMzcxLjI4NDAxNywyMDUuMjQxNjYxIDM3MS4yODQwMTcsMjExLjY4NTE0MSBDMzcxLjI4NDAxNywyMTguODk2ODY5IDM3NS45MTk2NjEsMjIzLjMzOTYzMyAzODIuMTAxNjA5LDIyMy4zMzk2MzMgQzM4NS4zMjE3MTQsMjIzLjMzOTYzMyAzODcuNDQ2NjU3LDIyMi41NjgxMTYgMzg5LjA1NTA3NSwyMjEuODU4NzEyIEwzOTAuMDIyNzQxLDIyNi4xMDg1OTcgWiBNNDA1LjkxODI5MiwxODguNTY0NDU4IEw0MDUuOTE4MjkyLDE5Ni4wMzQ0NDkgTDQxNC4wMjkwMzQsMTk2LjAzNDQ0OSBMNDE0LjAyOTAzNCwyMDAuMzQ2NDQ3IEw0MDUuOTE4MjkyLDIwMC4zNDY0NDcgTDQwNS45MTgyOTIsMjE3LjE1NjM3NyBDNDA1LjkxODI5MiwyMjEuMDIzNzczIDQwNy4wMTM0NTUsMjIzLjIxMDgyOSA0MTAuMTY4MTc3LDIyMy4yMTA4MjkgQzQxMS43MTEyMTIsMjIzLjIxMDgyOSA0MTIuNjE2NzY1LDIyMy4wODMzMzIgNDEzLjQ1MzY2NSwyMjIuODIxODAxIEw0MTMuNzExOTI3LDIyNy4xNDAzMzggQzQxMi42MTY3NjUsMjI3LjUyNjA5NyA0MTAuODc3NTgxLDIyNy45MTE4NTYgNDA4LjY4NzI1NSwyMjcuOTExODU2IEM0MDYuMDQ1Nzg4LDIyNy45MTE4NTYgNDAzLjkyMDg0NiwyMjcuMDA5NTcyIDQwMi41NzA2OSwyMjUuNTMxOTIgQzQwMS4wMjQzODYsMjIzLjc4OTQ2NyA0MDAuMzgwMzY1LDIyMS4wMjM3NzMgNDAwLjM4MDM2NSwyMTcuMzUyNTI2IEw0MDAuMzgwMzY1LDIwMC4zNDY0NDcgTDM5NS41NDg1NzIsMjAwLjM0NjQ0NyBMMzk1LjU0ODU3MiwxOTYuMDM0NDQ5IEw0MDAuMzgwMzY1LDE5Ni4wMzQ0NDkgTDQwMC4zODAzNjUsMTkwLjIzODI1OSBMNDA1LjkxODI5MiwxODguNTY0NDU4IFogTTQyMS45NDQ5MzUsMjA1Ljc1OTQ5MyBDNDIxLjk0NDkzNSwyMDIuMDg4MjQ2IDQyMS44NzYyODMsMTk4LjkzMzUyNCA0MjEuNjg2NjczLDE5Ni4wMzM3OTUgTDQyNi42NDU5NjIsMTk2LjAzMzc5NSBMNDI2LjkwNDIyNCwyMDIuMjE1NzQzIEw0MjcuMDkzODM1LDIwMi4yMTU3NDMgQzQyOC41MTI2NDIsMTk4LjAzMTI0MSA0MzEuOTg3NzQxLDE5NS4zODk3NzQgNDM1Ljc4NjQ4NCwxOTUuMzg5Nzc0IEM0MzYuMzY4MzkxLDE5NS4zODk3NzQgNDM2LjgxOTUzMywxOTUuNDU1MTU3IDQzNy4zMzYwNTgsMTk1LjUxNzI3IEw0MzcuMzM2MDU4LDIwMC44NjU1ODcgQzQzNi43NTQxNSwyMDAuNzM0ODIxIDQzNi4xNzg3ODEsMjAwLjczNDgyMSA0MzUuNDAwNzI1LDIwMC43MzQ4MjEgQzQzMS40MTIzNzIsMjAwLjczNDgyMSA0MjguNTc0NzU2LDIwMy42OTY2NjQgNDI3LjgwMzIzOCwyMDcuOTQ5ODE5IEM0MjcuNjc1NzQyLDIwOC43MjEzMzYgNDI3LjYxMDM1OSwyMDkuNjg1NzMzIDQyNy42MTAzNTksMjEwLjU5MTI4NiBMNDI3LjYxMDM1OSwyMjcuMjA1MDY3IEw0MjEuOTQ0OTM1LDIyNy4yMDUwNjcgTDQyMS45NDQ5MzUsMjA1Ljc1OTQ5MyBaIE00NzIuMTY2NDgxLDIxMS4zNjExNjkgQzQ3Mi4xNjY0ODEsMjIyLjg4ODE2NSA0NjQuMTE0NTgzLDIyNy45MTI4MzYgNDU2LjY0NDU5MywyMjcuOTEyODM2IEM0NDguMjcyMzIsMjI3LjkxMjgzNiA0NDEuNzA3ODgyLDIyMS43Mjc2MTkgNDQxLjcwNzg4MiwyMTEuODc3NjkzIEM0NDEuNzA3ODgyLDIwMS41MDc5NzQgNDQ4LjU5NTk2NSwxOTUuMzkxNDA4IDQ1Ny4xNjExMTgsMTk1LjM5MTQwOCBDNDY2LjExMjAyOSwxOTUuMzkxNDA4IDQ3Mi4xNjY0ODEsMjAxLjg5MzczMiA0NzIuMTY2NDgxLDIxMS4zNjExNjkgWiBNNDQ3LjUwMDgwMiwyMTEuNjg0ODE0IEM0NDcuNTAwODAyLDIxOC41MTA3ODMgNDUxLjM2NDkyOCwyMjMuNjYyOTUxIDQ1Ni45MDI4NTUsMjIzLjY2Mjk1MSBDNDYyLjMxMzI4NiwyMjMuNjYyOTUxIDQ2Ni4zNjcwMjMsMjE4LjU3Mjg5NyA0NjYuMzY3MDIzLDIxMS41NTQwNDggQzQ2Ni4zNjcwMjMsMjA2LjI3NDM4MyA0NjMuNzI4ODI1LDE5OS42MzgwMjQgNDU3LjAzMDM1MiwxOTkuNjM4MDI0IEM0NTAuNDAwNTMxLDE5OS42MzgwMjQgNDQ3LjUwMDgwMiwyMDUuODIzMjQyIDQ0Ny41MDA4MDIsMjExLjY4NDgxNCBaIE00ODAuODQ5OTc3LDIwNC40Njk0OSBDNDgwLjg0OTk3NywyMDEuMTg3MjcxIDQ4MC43ODEzMjUsMTk4LjYxMTE4NyA0ODAuNTkxNzE1LDE5Ni4wMzUxMDIgTDQ4NS42MTMxMTcsMTk2LjAzNTEwMiBMNDg1LjkzNjc2MywyMDEuMTg3MjcxIEw0ODYuMDY0MjU5LDIwMS4xODcyNzEgQzQ4Ny42MTA1NjMsMTk4LjI4NzU0MiA0OTEuMjE2NDI3LDE5NS4zOTEwODEgNDk2LjM2ODU5NiwxOTUuMzkxMDgxIEM1MDAuNjgwNTk1LDE5NS4zOTEwODEgNTA3LjM3OTA2NywxOTcuOTY3MTY2IDUwNy4zNzkwNjcsMjA4LjY1Mzk5MiBMNTA3LjM3OTA2NywyMjcuMjAzMTA2IEw1MDEuNzEzNjQ0LDIyNy4yMDMxMDYgTDUwMS43MTM2NDQsMjA5LjIzNTg5OSBDNTAxLjcxMzY0NCwyMDQuMjE0NDk3IDQ5OS44NDY5NjMsMjAwLjAyNjcyNSA0OTQuNTAxOTE2LDIwMC4wMjY3MjUgQzQ5MC44MzA2NjksMjAwLjAyNjcyNSA0ODcuOTMwOTM5LDIwMi42NjgxOTIgNDg2LjkwNDQyOSwyMDUuODIyOTE1IEM0ODYuNjQyODk3LDIwNi41MzIzMTkgNDg2LjUxNTQwMSwyMDcuNDk2NzE2IDQ4Ni41MTU0MDEsMjA4LjQ2NDM4MiBMNDg2LjUxNTQwMSwyMjcuMjAzMTA2IEw0ODAuODQ5OTc3LDIyNy4yMDMxMDYgTDQ4MC44NDk5NzcsMjA0LjQ2OTQ5IFoiIGZpbGw9IiNGRkZGRkYiPjwvcGF0aD4gICAgPC9nPjwvc3ZnPg==");
}
.rccs__number {
clear: both;
font-family: Consolas, Courier, monospace;
font-size: 20px;
left: 10%;
position: absolute;
top: 45%;
}
.rccs__number.rccs__number--large {
font-size: 17px;
}
.rccs__name {
bottom: 15%;
font-family: Consolas, Courier, monospace;
font-size: 17px;
left: 10%;
line-height: 1;
overflow: hidden;
position: absolute;
text-align: left;
text-overflow: ellipsis;
text-transform: uppercase;
width: 60%;
}
.rccs__expiry {
bottom: 15%;
font-size: 0;
line-height: 1;
position: absolute;
right: 10%;
}
.rccs__expiry > * {
vertical-align: middle;
}
.rccs__expiry__valid {
font-size: 10px;
margin-bottom: 5px;
}
.rccs__expiry__value {
font-family: Consolas, Courier, monospace;
font-size: 16px;
}
.rccs__number,
.rccs__name,
.rccs__expiry,
.rccs__cvc {
opacity: 0.5;
-webkit-transition: opacity 0.3s;
transition: opacity 0.3s;
}
.rccs__chip {
background-image: url("data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTAwIiBoZWlnaHQ9Ijc2IiB2aWV3Qm94PSIwIDAgMTAwIDc2IiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHByZXNlcnZlQXNwZWN0UmF0aW89InhNaWRZTWlkIj48ZGVmcz48bGluZWFyR3JhZGllbnQgeDE9IjEwMCUiIHkxPSIwJSIgeDI9IjAlIiB5Mj0iMTAwJSIgaWQ9ImEiPjxzdG9wIHN0b3AtY29sb3I9IiNGM0QwOEYiIG9mZnNldD0iMCUiLz48c3RvcCBzdG9wLWNvbG9yPSIjRkFENzY2IiBvZmZzZXQ9IjEwMCUiLz48L2xpbmVhckdyYWRpZW50PjwvZGVmcz48cGF0aCBkPSJNOTIuNzI3IDc1LjQ1NWgtODUuNDU1Yy00IDAtNy4yNzMtMy4yNzMtNy4yNzMtNy4yNzN2LTYwLjkwOWMwLTQgMy4yNzMtNy4yNzMgNy4yNzMtNy4yNzNoODUuNDU1YzQgMCA3LjI3MyAzLjI3MyA3LjI3MyA3LjI3M3Y2MC45MDljMCA0LTMuMjczIDcuMjczLTcuMjczIDcuMjczIiBmaWxsPSJ1cmwoI2EpIi8+PHBhdGggZD0iTTcyLjEyMyAyOC40ODVoMjcuODc4di0xLjgxOGgtMjkuNjQ4Yy0uOTY1IDAtMS44MzIuNjAxLTIuMTcyIDEuNTA0LTIuMjg3IDYuMDcyLTIuNDMzIDEyLjU5NC0uNDM4IDE5Ljg0Mi40NTUgMS42NTQuNDM1IDMuNC0uMSA1LjAzLTIuMDM2IDYuMTk1LTcuNzc5IDE5Ljk4OC0xOC41NTEgMTkuOTg4LTExLjAwOCAwLTE2LjA5Ni0xNS42OTktMTcuMzM0LTIxLjk1Mi0uMTU1LS43ODQtLjEyMi0xLjU5Mi4xMDctMi4zNTcgMS42OTUtNS42NDggMi4wOTQtMTAuNjQtLjAxNi0xOS41OS0uMjA1LS44Ny0uMTgyLTEuNzgzLjA0OC0yLjY0NiA0LjQ4LTE2Ljc1NSAxMi44ODItMjAuMTQ3IDEyLjk2NS0yMC4xNzkuMzU2LS4xMzIuNTkzLS40NzIuNTkzLS44NTJ2LTUuNDU1aC0xLjgxOHYzLjc3NmMwIC42NS0uMzMyIDEuMjUyLS44ODQgMS41OTYtMi44MDMgMS43NDItOC45MDQgNi45MzYtMTIuNTU3IDIwLjQ1Ni0uMTguNjY4LS43ODEgMS4xMzYtMS40NzMgMS4xMzNsLTI4LjcyMi0uMTM5djEuODE4bDI3LjQxNi4xMzNjMS40NjguMDA3IDIuNzM1IDEuMDQxIDMuMDM3IDIuNDc4IDEuNDE2IDYuNzQxIDEuMjE5IDExLjAzOS4wODIgMTUuNDU4LS4zMTYgMS4yMy0xLjQyIDIuMDk2LTIuNjkgMi4xMDlsLTI3Ljg0NC4yN3YxLjgxOWwyOC42MDUtLjI3OGMuNjkzLS4wMDcgMS4yOTYuNDczIDEuNDM1IDEuMTUyIDEuNDQyIDcuMDQxIDYuODg3IDIzLjA3IDE5LjA1IDIzLjA3IDYuMzY4IDAgMTIuMDYyLTQuMjUgMTYuNDY3LTEyLjI5IDIuNjQ0LTQuODI4IDQuMDY3LTkuNTkxIDQuNTQxLTExLjM0NmgyOS45MDF2LTEuODE4aC0yOC4wMTZjLTEuMTU4IDAtMi4xODMtLjc3Mi0yLjQ4OS0xLjg4OS0xLjY5Mi02LjE2NC0xLjc2MS0xMS43NTUtLjItMTYuOTU5LjM3MS0xLjIzNSAxLjUzOC0yLjA2MSAyLjgyNy0yLjA2MXptLTE3LjE1LTIxLjkxNGMuMDQ1LjAyMiA0LjUxOSAyLjMyMiA5LjI1MyAxMC4wNDEuMTcyLjI4LjQ3LjQzNC43NzYuNDM0LjE5OCAwIC4zOTktLjA2NC41NzEtLjIwMi4zNjUtLjI5Mi40MTYtLjgzNy4xNzItMS4yMzUtMy41Ny01LjgwNS03LjAyNC04LjcxLTguNzc1LTkuOTMxLS40My0uMjk5LS42OC0uNzkyLS42OC0xLjMxNXYtNC4zNjNoLTEuODE4djUuNzU4YzAgLjM0NS4xOTUuNjU5LjUwMi44MTN6IiBmaWxsPSIjMEMwMjAwIi8+PC9zdmc+");
background-repeat: no-repeat;
background-size: contain;
height: 26.36364px;
left: 10%;
position: absolute;
top: 10%;
width: 41.42857px;
}
.rccs__issuer {
background-position: top right;
background-repeat: no-repeat;
background-size: contain;
height: 23%;
position: absolute;
right: 10%;
top: 10%;
width: 40%;
}
.rccs__stripe {
background-color: #2a1d16;
height: 22%;
left: 0;
position: absolute;
top: 9%;
width: 100%;
}
.rccs__signature {
background: repeating-linear-gradient(
0.1deg,
#fff 20%,
#fff 40%,
#fea 40%,
#fea 44%,
#fff 44%
);
height: 18%;
left: 5%;
position: absolute;
top: 35%;
width: 75%;
}
.rccs__cvc {
color: #222;
font-family: Consolas, Courier, monospace;
font-size: 14px;
left: 67%;
line-height: 1;
position: absolute;
top: 42%;
}
.rccs__cvc__front {
font-family: Consolas, Courier, monospace;
font-size: 11.9px;
opacity: 0;
position: absolute;
right: 10%;
top: 38%;
visibility: hidden;
}
.rccs--filled {
opacity: 0.8 !important;
}
.rccs--focused {
font-weight: 700;
opacity: 1 !important;
}
`;
export default (props) => (
);
================================================
FILE: examples/credit-card/Styles.js
================================================
import styled, { css } from "styled-components";
const btn = (light, dark) => css`
white-space: nowrap;
display: inline-block;
border-radius: 5px;
padding: 5px 15px;
font-size: 16px;
color: white;
&:visited {
color: white;
}
background-image: linear-gradient(${light}, ${dark});
border: 1px solid ${dark};
&:hover {
background-image: linear-gradient(${light}, ${dark});
&[disabled] {
background-image: linear-gradient(${light}, ${dark});
}
}
&:visited {
color: black;
}
&[disabled] {
opacity: 0.6;
cursor: not-allowed;
}
`;
const btnDefault = css`
${btn("#ffffff", "#d5d5d5")} color: #555;
`;
const btnPrimary = btn("#4f93ce", "#285f8f");
const btnDanger = btn("#e27c79", "#c9302c");
export default styled.div`
font-family: sans-serif;
h1 {
text-align: center;
color: #222;
}
h2 {
text-align: center;
color: #222;
}
& > div {
text-align: center;
}
a {
display: block;
text-align: center;
color: #222;
margin-bottom: 10px;
}
p {
max-width: 500px;
margin: 10px auto;
& > a {
display: inline;
}
}
.loading {
font-size: 2em;
font-weight: bold;
text-align: center;
margin: 50px;
}
form,
div.form {
text-align: left;
max-width: 500px;
margin: 10px auto;
border: 1px solid #ccc;
padding: 20px;
box-shadow: 2px 2px 5px rgba(0, 0, 0, 0.3);
border-radius: 3px;
position: relative;
& > div {
display: flex;
flex-flow: row nowrap;
line-height: 2em;
position: relative;
padding: 8px 5px;
border: 1px solid transparent;
&.active {
background-color: paleturquoise;
border-color: turquoise;
}
& > label {
color: #333;
width: 110px;
min-width: 60px;
font-size: 1em;
line-height: 32px;
}
& > input,
& > .downshift > input,
& > select,
& > textarea {
flex: 1;
padding: 6px 9px;
font-size: 1em;
margin-left: 15px;
border: 1px solid #ccc;
border-radius: 3px;
&[disabled] {
background: #eee;
}
}
& > input[type="checkbox"] {
margin-top: 7px;
}
& > div {
margin-left: 16px;
& > label {
margin-left: 0;
display: block;
& > input {
margin-right: 3px;
}
}
&.downshift {
margin-left: 0;
padding-left: 15px;
flex: 1;
& > input {
width: 100%;
padding: 6px 5px;
font-size: 1em;
margin-left: 0;
border: 1px solid #ccc;
border-radius: 3px;
}
}
}
& > span {
line-height: 32px;
margin-left: 10px;
color: #800;
font-weight: bold;
}
& > button.remove {
${btnDanger};
}
}
& > .buttons {
display: flex;
flex-flow: row nowrap;
justify-content: center;
margin-top: 15px;
}
.error {
display: flex;
font-weight: bold;
color: #800;
flex-flow: row nowrap;
justify-content: center;
}
pre {
position: relative;
border: 1px solid #ccc;
background: rgba(0, 0, 0, 0.1);
box-shadow: inset 1px 1px 3px rgba(0, 0, 0, 0.2);
padding: 20px;
}
.submitting {
display: block;
position: absolute;
top: -5px;
left: -5px;
right: -5px;
padding: 0;
text-align: center;
background: rgba(0, 0, 0, 0.4);
color: white;
z-index: 10;
font-weight: bold;
font-size: 0.8em;
}
.saving {
font-size: 0.8em;
font-weight: bold;
color: darkblue;
margin: 8px 0 0 7px;
}
}
button {
margin: 0 10px;
&[type="submit"] {
${btnPrimary};
}
&[type="button"] {
${btnDefault};
}
}
.downshift-options {
border: 1px solid #ddd;
box-shadow: 1px 1px 2px rgba(0, 0, 0, 0.3);
& > div {
padding: 3px 5px;
}
}
`;
================================================
FILE: examples/credit-card/cardUtils.js
================================================
import Payment from "payment";
function clearNumber(value = "") {
return value.replace(/\D+/g, "");
}
export function formatCreditCardNumber(value) {
if (!value) {
return value;
}
const issuer = Payment.fns.cardType(value);
const clearValue = clearNumber(value);
let nextValue;
switch (issuer) {
case "amex":
nextValue = `${clearValue.slice(0, 4)} ${clearValue.slice(
4,
10,
)} ${clearValue.slice(10, 15)}`;
break;
case "dinersclub":
nextValue = `${clearValue.slice(0, 4)} ${clearValue.slice(
4,
10,
)} ${clearValue.slice(10, 14)}`;
break;
default:
nextValue = `${clearValue.slice(0, 4)} ${clearValue.slice(
4,
8,
)} ${clearValue.slice(8, 12)} ${clearValue.slice(12, 19)}`;
break;
}
return nextValue.trim();
}
export function formatCVC(value, prevValue, allValues = {}) {
const clearValue = clearNumber(value);
let maxLength = 4;
if (allValues.number) {
const issuer = Payment.fns.cardType(allValues.number);
maxLength = issuer === "amex" ? 4 : 3;
}
return clearValue.slice(0, maxLength);
}
export function formatExpirationDate(value) {
const clearValue = clearNumber(value);
if (clearValue.length >= 3) {
return `${clearValue.slice(0, 2)}/${clearValue.slice(2, 4)}`;
}
return clearValue;
}
================================================
FILE: examples/credit-card/index.js
================================================
import React from "react";
import { render } from "react-dom";
import Styles from "./Styles";
import { Form, Field } from "react-final-form";
import Card from "./Card";
import {
formatCreditCardNumber,
formatCVC,
formatExpirationDate,
} from "./cardUtils";
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
const onSubmit = async (values) => {
await sleep(300);
window.alert(JSON.stringify(values, 0, 2));
};
const App = () => (
🏁
{" "}
React Final Form
Credit Card Example
Read Docs
This example demonstrates how to use the amazing{" "}
React Credit Cards
{" "}
library with your form.
{
return (
Submit
Reset
Values
{JSON.stringify(values, 0, 2)}
);
}}
/>
);
render( , document.getElementById("root"));
================================================
FILE: examples/credit-card/package.json
================================================
{
"name": "react-final-form-credit-card-example",
"version": "1.0.0",
"description": "Demonstrates how to use 🏁 React Final Form with the React Credit Cards library.",
"keywords": [
"final-form",
"react-final-form",
"react-credit-cards",
"payment",
"credit cards"
],
"homepage": "https://codesandbox.io/s/new",
"main": "src/index.js",
"dependencies": {
"final-form": "4.20.4",
"react": "17.0.2",
"react-credit-cards": "0.7.0",
"react-dom": "17.0.2",
"react-final-form": "6.5.3",
"react-scripts": "3.0.1",
"styled-components": "4.2.0"
},
"devDependencies": {},
"scripts": {
"start": "react-scripts start",
"build": "react-scripts build",
"test": "react-scripts test --env=jsdom",
"eject": "react-scripts eject"
}
}
================================================
FILE: examples/credit-card/readme.md
================================================
# Credit Card Example
[](https://codesandbox.io/s/9y8vkrrx9o)
================================================
FILE: examples/credit-card/sandbox.config.json
================================================
{
"infiniteLoopProtection": false,
"hardReloadOnChange": false
}
================================================
FILE: examples/custom-validation-engine/OnBlurValidation.js
================================================
import React from "react";
import { getIn } from "final-form";
import { FormSpy } from "react-final-form";
class OnBlurValidation extends React.Component {
state = {
withError: {},
validating: false,
};
componentWillReceiveProps(nextProps) {
const field = this.props.active;
if (field && field !== nextProps.active) {
// blur occurred
const {
rules,
form: { mutators },
values,
} = nextProps;
const rule = rules[field];
if (rule) {
const value = getIn(values, field);
let isSync = false;
const setError = (error) => {
mutators.setFieldData(field, { error, validating: false });
isSync = true;
this.setState((state) => ({
withError: {
...state.withError,
[field]: !!error,
},
validating: false,
}));
};
rule(value, setError);
if (!isSync) {
mutators.setFieldData(field, { validating: true });
this.setState({
validating: true,
});
}
}
}
}
render() {
const { withError, validating } = this.state;
const hasErrors = Object.keys(withError).some((key) => withError[key]);
return this.props.render({ hasErrors, validating });
}
}
// Make a HOC
export default (props) => (
);
================================================
FILE: examples/custom-validation-engine/Styles.js
================================================
import styled, { css } from "styled-components";
const btn = (light, dark) => css`
white-space: nowrap;
display: inline-block;
border-radius: 5px;
padding: 5px 15px;
font-size: 16px;
color: white;
&:visited {
color: white;
}
background-image: linear-gradient(${light}, ${dark});
border: 1px solid ${dark};
&:hover {
background-image: linear-gradient(${light}, ${dark});
&[disabled] {
background-image: linear-gradient(${light}, ${dark});
}
}
&:visited {
color: black;
}
&[disabled] {
opacity: 0.6;
cursor: not-allowed;
}
`;
const btnDefault = css`
${btn("#ffffff", "#d5d5d5")} color: #555;
`;
const btnPrimary = btn("#4f93ce", "#285f8f");
const btnDanger = btn("#e27c79", "#c9302c");
export default styled.div`
font-family: sans-serif;
h1 {
text-align: center;
color: #222;
}
h2 {
text-align: center;
color: #222;
}
& > div {
text-align: center;
}
a {
display: block;
text-align: center;
color: #222;
margin-bottom: 10px;
}
p {
max-width: 500px;
margin: 10px auto;
& > a {
display: inline;
}
}
form,
div.form {
text-align: left;
max-width: 500px;
margin: 10px auto;
border: 1px solid #ccc;
padding: 20px;
box-shadow: 2px 2px 5px rgba(0, 0, 0, 0.3);
border-radius: 3px;
position: relative;
.loading {
text-align: center;
display: block;
position: absolute;
background: url("https://media.giphy.com/media/130AxGoOaR6t0I/giphy.gif")
center center;
background-size: fill;
font-size: 2em;
top: 0;
left: 0;
right: 0;
bottom: 0;
padding: 50px 0 0 0;
z-index: 2;
}
& > div {
display: flex;
flex-flow: row nowrap;
line-height: 2em;
margin: 5px;
position: relative;
& > label {
color: #333;
width: 110px;
min-width: 60px;
font-size: 1em;
line-height: 32px;
}
& > input,
& > select,
& > textarea {
flex: 1;
padding: 3px 5px;
font-size: 1em;
margin-left: 15px;
border: 1px solid #ccc;
border-radius: 3px;
}
& > input[type="checkbox"] {
margin-top: 7px;
}
& > div {
margin-left: 16px;
& > label {
display: block;
& > input {
margin-right: 3px;
}
}
}
& > span {
line-height: 32px;
margin-left: 10px;
color: #800;
font-weight: bold;
}
& > button.remove {
${btnDanger};
}
}
& > .buttons {
display: flex;
flex-flow: row nowrap;
justify-content: center;
margin-top: 15px;
}
.error {
display: flex;
font-weight: bold;
color: #800;
flex-flow: row nowrap;
justify-content: center;
}
pre {
position: relative;
border: 1px solid #ccc;
background: rgba(0, 0, 0, 0.1);
box-shadow: inset 1px 1px 3px rgba(0, 0, 0, 0.2);
padding: 20px;
}
.submitting {
display: block;
position: absolute;
top: -5px;
left: -5px;
right: -5px;
padding: 0;
text-align: center;
background: rgba(0, 0, 0, 0.4);
color: white;
z-index: 10;
font-weight: bold;
font-size: 0.8em;
}
.saving {
font-size: 0.8em;
font-weight: bold;
color: darkblue;
margin: 8px 0 0 7px;
}
}
button {
margin: 0 10px;
&[type="submit"] {
${btnPrimary};
}
&[type="button"] {
${btnDefault};
}
}
`;
================================================
FILE: examples/custom-validation-engine/index.js
================================================
import React from "react";
import { render } from "react-dom";
import Styles from "./Styles";
import { Form, Field } from "react-final-form";
import Icon from "react-fontawesome";
import OnBlurValidation from "./OnBlurValidation";
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
const onSubmit = async (values) => {
await sleep(300);
window.alert(JSON.stringify(values, 0, 2));
};
const isBeatle = (value) =>
~["john", "paul", "george", "ringo"].indexOf(value.toLowerCase());
const Error = ({ name }) => (
data.error ? {data.error} : null
}
/>
);
const AsyncIndicator = ({ name }) => (
data.validating ? : null
}
/>
);
const App = () => (
🏁
{" "}
React Final Form
Custom Validation Engine
Read Docs
This example includes a special OnBlurValidation component
that manages its own set of validation rules, completely apart from{" "}
🏁
Final Form's validation engine. This allows the rules to be run only on
blur, and maintains "validating" state for asynchronous validations. The
function then injects the hasErrors state into a render
function to render the rest of the form, thus allowing submission to be
halted if errors are present.
Any first name value of John, Paul,{" "}
George, or Ringo will fail asynchronous
validation.
(
{
if (!value) {
// return synchronously
setError("Required");
} else {
// return asynchronously
setTimeout(() => {
if (isBeatle(value)) {
setError("No Beatles");
} else {
setError(undefined);
}
}, 1000);
}
},
lastName: (value, setError) =>
setError(value ? undefined : "Required"),
}}
render={({ hasErrors, validating }) => (
{
event.preventDefault();
if (!hasErrors && !validating) handleSubmit();
}}
>
Submit
Reset
{JSON.stringify(values, 0, 2)}
)}
/>
)}
/>
);
render( , document.getElementById("root"));
================================================
FILE: examples/custom-validation-engine/package.json
================================================
{
"name": "react-final-form-custom-validation-engine",
"version": "1.0.0",
"description": "Demonstrates how extensible 🏁 React Final Form can be with the combination of FormSpy and setFieldData by creating a totally separate validation engine that runs when a field is blurred.",
"keywords": [],
"main": "index.js",
"dependencies": {
"final-form": "4.20.4",
"final-form-set-field-data": "1.0.2",
"react": "17.0.2",
"react-dom": "17.0.2",
"react-final-form": "6.5.3",
"react-fontawesome": "1.6.1",
"styled-components": "4.2.0"
}
}
================================================
FILE: examples/custom-validation-engine/readme.md
================================================
# Custom Validation Engine
[](https://codesandbox.io/s/kxxw4l0p9o)
================================================
FILE: examples/debounced-record-level-validation/ErrorWithDelay.js
================================================
import React from "react";
import { Field } from "react-final-form";
const DisplayError = ({ delay, active, dirty, error, touched, children }) => {
const [show, setShow] = React.useState(false);
React.useEffect(() => {
let timeout;
if (active && error && dirty) {
console.info("setting timeout");
timeout = setTimeout(() => setShow(true), delay);
}
return () => {
console.info("clearing timeout");
clearTimeout(timeout);
};
}, [delay, error, active, dirty]);
return error && ((touched && !active) || (touched && !show && active) || show)
? children(error)
: null;
};
const ErrorWithDelay = ({ name, children, delay }) => (
{({ meta: { active, dirty, error, touched } }) => (
)}
);
export default ErrorWithDelay;
================================================
FILE: examples/debounced-record-level-validation/README.md
================================================
# Synchronous Record-Level Validation (with debounced errors)
[](https://codesandbox.io/s/z2zqr008pm)
================================================
FILE: examples/debounced-record-level-validation/Styles.js
================================================
import styled, { css } from "styled-components";
const btn = (light, dark) => css`
white-space: nowrap;
display: inline-block;
border-radius: 5px;
padding: 5px 15px;
font-size: 16px;
color: white;
&:visited {
color: white;
}
background-image: linear-gradient(${light}, ${dark});
border: 1px solid ${dark};
&:hover {
background-image: linear-gradient(${light}, ${dark});
&[disabled] {
background-image: linear-gradient(${light}, ${dark});
}
}
&:visited {
color: black;
}
&[disabled] {
opacity: 0.6;
cursor: not-allowed;
}
`;
const btnDefault = css`
${btn("#ffffff", "#d5d5d5")} color: #555;
`;
const btnPrimary = btn("#4f93ce", "#285f8f");
export default styled.div`
font-family: sans-serif;
h1 {
text-align: center;
color: #222;
}
h2 {
text-align: center;
}
& > div {
text-align: center;
}
a {
display: block;
text-align: center;
color: #222;
}
p {
max-width: 500px;
margin: 10px auto;
& > a {
display: inline;
}
}
form {
max-width: 500px;
margin: 10px auto;
border: 1px solid #ccc;
padding: 20px;
box-shadow: 2px 2px 5px rgba(0, 0, 0, 0.3);
border-radius: 3px;
& > div {
display: flex;
flex-flow: row nowrap;
line-height: 2em;
margin: 5px;
& > label {
color: #333;
width: 110px;
font-size: 1em;
line-height: 32px;
}
& > span {
color: #800;
font-weight: bold;
line-height: 32px;
margin-left: 10px;
}
& > input,
& > select,
& > textarea {
flex: 1;
padding: 3px 5px;
font-size: 1em;
margin-left: 15px;
border: 1px solid #ccc;
border-radius: 3px;
}
& > input[type="checkbox"] {
margin-top: 7px;
}
& > div {
margin-left: 16px;
& > label {
display: block;
& > input {
margin-right: 3px;
}
}
}
}
& > .buttons {
display: flex;
flex-flow: row nowrap;
justify-content: center;
margin-top: 15px;
}
button {
margin: 0 10px;
&[type="submit"] {
${btnPrimary};
}
&[type="button"] {
${btnDefault};
}
}
pre {
border: 1px solid #ccc;
background: rgba(0, 0, 0, 0.1);
box-shadow: inset 1px 1px 3px rgba(0, 0, 0, 0.2);
padding: 20px;
}
}
`;
================================================
FILE: examples/debounced-record-level-validation/index.js
================================================
import React from "react";
import { render } from "react-dom";
import Styles from "./Styles";
import ErrorWithDelay from "./ErrorWithDelay";
import { Form, Field } from "react-final-form";
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
const onSubmit = async (values) => {
await sleep(300);
window.alert(JSON.stringify(values, 0, 2));
};
const App = () => (
🏁
{" "}
React Final Form Example
Synchronous Record-Level Validation (with debounced errors)
Read Docs
The important feature of this demo is that the errors disappear {" "}
instantly when they are fixed, appear instantly when blurred, but{" "}
appear on a delay when the field is active. Notice that the delay
mechanism is entirely separate from Final Form, encapsulated into a{" "}
ErrorWithDelay component.
Things to try:
Tab through all fields and watch errors appear on blur.
Remove first name value and notice how it doesn't complain for a
second.
Notice how you can type "44" as your age without the error for "4"
being an illegal value being displayed.
{
const errors = {};
if (!values.firstName) {
errors.firstName = "Required";
}
if (!values.lastName) {
errors.lastName = "Required";
}
if (!values.age) {
errors.age = "Required";
} else if (isNaN(values.age)) {
errors.age = "Must be a number";
} else if (values.age < 18) {
errors.age = "Must be >18";
}
return errors;
}}
render={({ handleSubmit, form, submitting, pristine, values }) => (
{({ input, meta }) => (
First Name
{(error) => {error} }
)}
{({ input, meta }) => (
Last Name
{(error) => {error} }
)}
{({ input, meta }) => (
Age
{(error) => {error} }
)}
Submit
Reset
{JSON.stringify(values, 0, 2)}
)}
/>
);
render( , document.getElementById("root"));
================================================
FILE: examples/debounced-record-level-validation/package.json
================================================
{
"name": "react-final-form-synchronous-errors-debounced",
"version": "1.0.0",
"description": "A DelayedRender component is provided to not show the error immediately.",
"keywords": [],
"main": "src/index.js",
"dependencies": {
"final-form": "4.20.4",
"react": "17.0.2",
"react-dom": "17.0.2",
"react-final-form": "6.5.3",
"react-scripts": "2.0.3",
"styled-components": "4.2.0"
},
"devDependencies": {},
"scripts": {
"start": "react-scripts start",
"build": "react-scripts build",
"test": "react-scripts test --env=jsdom",
"eject": "react-scripts eject"
},
"browserslist": [
">0.2%",
"not dead",
"not ie <= 11",
"not op_mini all"
]
}
================================================
FILE: examples/declarative-form-rules/Styles.js
================================================
import styled, { css } from "styled-components";
const btn = (light, dark) => css`
white-space: nowrap;
display: inline-block;
border-radius: 5px;
padding: 5px 15px;
font-size: 16px;
color: white;
&:visited {
color: white;
}
background-image: linear-gradient(${light}, ${dark});
border: 1px solid ${dark};
&:hover {
background-image: linear-gradient(${light}, ${dark});
&[disabled] {
background-image: linear-gradient(${light}, ${dark});
}
}
&:visited {
color: black;
}
&[disabled] {
opacity: 0.6;
cursor: not-allowed;
}
`;
const btnDefault = css`
${btn("#ffffff", "#d5d5d5")} color: #555;
`;
const btnPrimary = btn("#4f93ce", "#285f8f");
const btnDanger = btn("#e27c79", "#c9302c");
export default styled.div`
font-family: sans-serif;
h1 {
text-align: center;
color: #222;
}
h2 {
text-align: center;
color: #222;
}
& > div {
text-align: center;
}
a {
display: block;
text-align: center;
color: #222;
margin-bottom: 10px;
}
p {
max-width: 500px;
margin: 10px auto;
& > a {
display: inline;
}
}
.loading {
font-size: 2em;
font-weight: bold;
text-align: center;
margin: 50px;
}
form,
div.form {
text-align: left;
max-width: 500px;
margin: 10px auto;
border: 1px solid #ccc;
padding: 20px;
box-shadow: 2px 2px 5px rgba(0, 0, 0, 0.3);
border-radius: 3px;
position: relative;
& > div {
display: flex;
flex-flow: row nowrap;
line-height: 2em;
position: relative;
padding: 8px 5px;
border: 1px solid transparent;
&.active {
background-color: paleturquoise;
border-color: turquoise;
}
& > label {
color: #333;
width: 110px;
min-width: 60px;
font-size: 1em;
line-height: 32px;
}
& > input,
& > .downshift > input,
& > select,
& > textarea {
flex: 1;
padding: 3px 5px;
font-size: 1em;
margin-left: 15px;
border: 1px solid #ccc;
border-radius: 3px;
&[disabled] {
background: #eee;
}
}
& > input[type="checkbox"] {
margin-top: 7px;
}
& > div {
margin-left: 16px;
& > label {
margin-left: 0;
display: block;
& > input {
margin-right: 3px;
}
}
&.downshift {
margin-left: 0;
padding-left: 15px;
flex: 1;
& > input {
width: 100%;
padding: 6px 5px;
font-size: 1em;
margin-left: 0;
border: 1px solid #ccc;
border-radius: 3px;
}
}
}
& > span {
line-height: 32px;
margin-left: 10px;
color: #800;
font-weight: bold;
}
& > button.remove {
${btnDanger};
}
}
& > .buttons {
display: flex;
flex-flow: row nowrap;
justify-content: center;
margin-top: 15px;
}
.error {
display: flex;
font-weight: bold;
color: #800;
flex-flow: row nowrap;
justify-content: center;
}
pre {
position: relative;
border: 1px solid #ccc;
background: rgba(0, 0, 0, 0.1);
box-shadow: inset 1px 1px 3px rgba(0, 0, 0, 0.2);
padding: 20px;
}
.submitting {
display: block;
position: absolute;
top: -5px;
left: -5px;
right: -5px;
padding: 0;
text-align: center;
background: rgba(0, 0, 0, 0.4);
color: white;
z-index: 10;
font-weight: bold;
font-size: 0.8em;
}
.saving {
font-size: 0.8em;
font-weight: bold;
color: darkblue;
margin: 8px 0 0 7px;
}
}
button {
margin: 0 10px;
&[type="submit"] {
${btnPrimary};
}
&[type="button"] {
${btnDefault};
}
}
.downshift-options {
border: 1px solid #ddd;
box-shadow: 1px 1px 2px rgba(0, 0, 0, 0.3);
& > div {
padding: 3px 5px;
}
}
`;
================================================
FILE: examples/declarative-form-rules/index.js
================================================
import React from "react";
import { render } from "react-dom";
import Styles from "./Styles";
import { Form, Field } from "react-final-form";
import { OnChange } from "react-final-form-listeners";
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
const onSubmit = async (values) => {
await sleep(300);
window.alert(JSON.stringify(values, 0, 2));
};
const WhenFieldChanges = ({ field, becomes, set, to }) => (
{(
// No subscription. We only use Field to get to the change function
{ input: { onChange } },
) => (
{(value) => {
if (value === becomes) {
onChange(to);
}
}}
)}
);
const App = () => (
🏁
{" "}
React Final Form
Declarative Form Rules
Read Docs
This example demonstrates how to use{" "}
🏁
{" "}
React Final Form Listeners
{" "}
to listen to the change of one field to then update the value of other
fields.
When you uncheck "Is this a gift?", the other two fields are cleared and
disabled.
{
return (
Is this a gift?
Gift wrap?
Message
Submit
Reset
Data
{JSON.stringify(values, 0, 2)}
);
}}
/>
);
render( , document.getElementById("root"));
================================================
FILE: examples/declarative-form-rules/package.json
================================================
{
"name": "react-final-form-declarative-form-rules",
"version": "1.0.0",
"description": "Demonstrates how to use React Final Form Listeners to listen to the change of one field to update another.",
"keywords": [
"react",
"forms",
"final-form",
"react-final-form",
"listener"
],
"homepage": "https://codesandbox.io/s/new",
"main": "src/index.js",
"dependencies": {
"final-form": "4.20.4",
"react": "17.0.2",
"react-dom": "17.0.2",
"react-final-form": "6.5.3",
"react-final-form-listeners": "1.0.2",
"react-scripts": "3.0.1",
"styled-components": "4.2.0"
},
"devDependencies": {},
"scripts": {
"start": "react-scripts start",
"build": "react-scripts build",
"test": "react-scripts test --env=jsdom",
"eject": "react-scripts eject"
}
}
================================================
FILE: examples/declarative-form-rules/readme.md
================================================
# Declarative Form Rules
[](https://codesandbox.io/s/52q597j2p)
================================================
FILE: examples/downshift-typeahead/DownshiftInput.js
================================================
import React from "react";
import Downshift from "downshift";
import matchSorter from "match-sorter";
const itemToString = (item) => (item ? item : "");
const DownshiftInput = ({ input, meta, placeholder, items, ...rest }) => (
{
input.onChange(inputValue);
}}
itemToString={itemToString}
selectedItem={input.value}
>
{({
getInputProps,
getItemProps,
getLabelProps,
isOpen,
inputValue,
highlightedIndex,
selectedItem,
}) => {
const filteredItems = matchSorter(items, inputValue, {
keys: ["label"],
maxRanking: matchSorter.rankings.STARTS_WITH,
});
return (
{isOpen && !!filteredItems.length && (
{filteredItems.map(({ value, label }, index) => (
{label}
))}
)}
);
}}
);
export default DownshiftInput;
================================================
FILE: examples/downshift-typeahead/Styles.js
================================================
import styled, { css } from "styled-components";
const btn = (light, dark) => css`
white-space: nowrap;
display: inline-block;
border-radius: 5px;
padding: 5px 15px;
font-size: 16px;
color: white;
&:visited {
color: white;
}
background-image: linear-gradient(${light}, ${dark});
border: 1px solid ${dark};
&:hover {
background-image: linear-gradient(${light}, ${dark});
&[disabled] {
background-image: linear-gradient(${light}, ${dark});
}
}
&:visited {
color: black;
}
&[disabled] {
opacity: 0.6;
cursor: not-allowed;
}
`;
const btnDefault = css`
${btn("#ffffff", "#d5d5d5")} color: #555;
`;
const btnPrimary = btn("#4f93ce", "#285f8f");
const btnDanger = btn("#e27c79", "#c9302c");
export default styled.div`
font-family: sans-serif;
h1 {
text-align: center;
color: #222;
}
h2 {
text-align: center;
color: #222;
}
& > div {
text-align: center;
}
a {
display: block;
text-align: center;
color: #222;
margin-bottom: 10px;
}
p {
max-width: 500px;
margin: 10px auto;
& > a {
display: inline;
}
}
.loading {
font-size: 2em;
font-weight: bold;
text-align: center;
margin: 50px;
}
form,
div.form {
text-align: left;
max-width: 500px;
margin: 10px auto;
border: 1px solid #ccc;
padding: 20px;
box-shadow: 2px 2px 5px rgba(0, 0, 0, 0.3);
border-radius: 3px;
position: relative;
& > div {
display: flex;
flex-flow: row nowrap;
line-height: 2em;
margin: 5px;
position: relative;
& > label {
color: #333;
width: 110px;
min-width: 60px;
font-size: 1em;
line-height: 32px;
}
& > input,
& > .downshift > input,
& > select,
& > textarea {
flex: 1;
padding: 3px 5px;
font-size: 1em;
margin-left: 15px;
border: 1px solid #ccc;
border-radius: 3px;
}
& > input[type="checkbox"] {
margin-top: 7px;
}
& > div {
margin-left: 16px;
& > label {
margin-left: 0;
display: block;
& > input {
margin-right: 3px;
}
}
&.downshift {
margin-left: 0;
padding-left: 15px;
flex: 1;
& > input {
width: 100%;
padding: 6px 5px;
font-size: 1em;
margin-left: 0;
border: 1px solid #ccc;
border-radius: 3px;
}
}
}
& > span {
line-height: 32px;
margin-left: 10px;
color: #800;
font-weight: bold;
}
& > button.remove {
${btnDanger};
}
}
& > .buttons {
display: flex;
flex-flow: row nowrap;
justify-content: center;
margin-top: 15px;
}
.error {
display: flex;
font-weight: bold;
color: #800;
flex-flow: row nowrap;
justify-content: center;
}
pre {
position: relative;
border: 1px solid #ccc;
background: rgba(0, 0, 0, 0.1);
box-shadow: inset 1px 1px 3px rgba(0, 0, 0, 0.2);
padding: 20px;
}
.submitting {
display: block;
position: absolute;
top: -5px;
left: -5px;
right: -5px;
padding: 0;
text-align: center;
background: rgba(0, 0, 0, 0.4);
color: white;
z-index: 10;
font-weight: bold;
font-size: 0.8em;
}
.saving {
font-size: 0.8em;
font-weight: bold;
color: darkblue;
margin: 8px 0 0 7px;
}
}
button {
margin: 0 10px;
&[type="submit"] {
${btnPrimary};
}
&[type="button"] {
${btnDefault};
}
}
.downshift-options {
border: 1px solid #ddd;
box-shadow: 1px 1px 2px rgba(0, 0, 0, 0.3);
& > div {
padding: 3px 5px;
}
}
`;
================================================
FILE: examples/downshift-typeahead/fruit.js
================================================
export default [
{ value: "Apple", label: "🍎 Apple" },
{ value: "Banana", label: "🍌 Banana" },
{ value: "Cherry", label: "🍒 Cherry" },
{ value: "Grape", label: "🍇 Grape" },
{ value: "Kiwi", label: "🥝 Kiwi" },
{ value: "Orange", label: "🍊 Orange" },
{ value: "Peach", label: "🍑 Peach" },
{ value: "Pear", label: "🍐 Pear" },
{ value: "Pineapple", label: "🍍 Pineapple" },
{ value: "Strawberry", label: "🍓 Strawberry" },
{ value: "Watermelon", label: "🍉 Watermelon" },
];
================================================
FILE: examples/downshift-typeahead/index.js
================================================
import React from "react";
import { render } from "react-dom";
import Styles from "./Styles";
import { Form, Field } from "react-final-form";
import DownshiftInput from "./DownshiftInput";
import fruit from "./fruit";
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
const onSubmit = async (values) => {
await sleep(300);
window.alert(JSON.stringify(values, 0, 2));
};
const validate = (values) => {
const errors = {};
if (!values.firstName) {
errors.firstName = "Required";
}
if (!values.fruit) {
errors.fruit = "Required";
}
return errors;
};
const Error = ({ name }) => (
touched && error ? {error} : null
}
/>
);
const App = () => (
🏁
{" "}
React Final Form
🏎️
{" "}
Downshift Example
Read Docs
This example demonstrates using a{" "}
🏎️
{" "}
Downshift
{" "}
type-ahead component.
(
First Name
Favorite Fruit
Submit
Reset
{JSON.stringify(values, 0, 2)}
)}
/>
);
render( , document.getElementById("root"));
================================================
FILE: examples/downshift-typeahead/package.json
================================================
{
"name": "react-final-form-downshift-example",
"version": "1.0.0",
"description": "Demonstrates how to use a 🏎️ Downshift type-ahead component as an input.",
"keywords": [
"final-form",
"downshift",
"typeahead",
"react-final-form",
"form"
],
"homepage": "https://codesandbox.io/s/new",
"main": "src/index.js",
"dependencies": {
"downshift": "1.28.0",
"final-form": "4.20.4",
"match-sorter": "3.0.0",
"react": "17.0.2",
"react-dom": "17.0.2",
"react-final-form": "6.5.3",
"react-scripts": "3.0.1",
"styled-components": "4.2.0"
},
"devDependencies": {},
"scripts": {
"start": "react-scripts start",
"build": "react-scripts build",
"test": "react-scripts test --env=jsdom",
"eject": "react-scripts eject"
}
}
================================================
FILE: examples/downshift-typeahead/readme.md
================================================
# Downshift Type-Ahead
[](https://codesandbox.io/s/qzm43nn2mj)
================================================
FILE: examples/external-submit/Styles.js
================================================
import styled, { css } from "styled-components";
const btn = (light, dark) => css`
white-space: nowrap;
display: inline-block;
border-radius: 5px;
padding: 5px 15px;
font-size: 16px;
color: white;
&:visited {
color: white;
}
background-image: linear-gradient(${light}, ${dark});
border: 1px solid ${dark};
&:hover {
background-image: linear-gradient(${light}, ${dark});
&[disabled] {
background-image: linear-gradient(${light}, ${dark});
}
}
&:visited {
color: black;
}
&[disabled] {
opacity: 0.6;
cursor: not-allowed;
}
`;
const btnDefault = css`
${btn("#ffffff", "#d5d5d5")} color: #555;
`;
const btnPrimary = btn("#4f93ce", "#285f8f");
const btnDanger = btn("#e27c79", "#c9302c");
export default styled.div`
font-family: sans-serif;
h1 {
text-align: center;
color: #222;
}
h2 {
text-align: center;
color: #222;
}
& > div {
text-align: center;
}
a {
display: block;
text-align: center;
color: #222;
margin-bottom: 10px;
}
p {
max-width: 500px;
margin: 10px auto;
& > a {
display: inline;
}
}
form {
max-width: 500px;
margin: 10px auto;
border: 1px solid #ccc;
padding: 20px;
box-shadow: 2px 2px 5px rgba(0, 0, 0, 0.3);
border-radius: 3px;
position: relative;
.loading {
text-align: center;
display: block;
position: absolute;
background: url("https://media.giphy.com/media/130AxGoOaR6t0I/giphy.gif")
center center;
background-size: fill;
font-size: 2em;
top: 0;
left: 0;
right: 0;
bottom: 0;
padding: 50px 0 0 0;
z-index: 2;
}
& > div {
display: flex;
flex-flow: row nowrap;
line-height: 2em;
margin: 5px;
position: relative;
& > label {
color: #333;
width: 110px;
min-width: 60px;
font-size: 1em;
line-height: 32px;
}
& > input,
& > select,
& > textarea {
flex: 1;
padding: 3px 5px;
font-size: 1em;
margin-left: 15px;
border: 1px solid #ccc;
border-radius: 3px;
}
& > input[type="checkbox"] {
margin-top: 7px;
}
& > div {
margin-left: 16px;
& > label {
display: block;
& > input {
margin-right: 3px;
}
}
}
& > span {
line-height: 32px;
margin-left: 10px;
color: #800;
font-weight: bold;
}
& > button.remove {
${btnDanger};
}
}
& > .buttons {
display: flex;
flex-flow: row nowrap;
justify-content: center;
margin-top: 15px;
}
.error {
display: flex;
font-weight: bold;
color: #800;
flex-flow: row nowrap;
justify-content: center;
}
pre {
position: relative;
border: 1px solid #ccc;
background: rgba(0, 0, 0, 0.1);
box-shadow: inset 1px 1px 3px rgba(0, 0, 0, 0.2);
padding: 20px;
}
}
button {
margin: 0 10px;
&[type="submit"] {
${btnPrimary};
}
&[type="button"] {
${btnDefault};
}
}
`;
================================================
FILE: examples/external-submit/index.js
================================================
import React from "react";
import { render } from "react-dom";
import Styles from "./Styles";
import { Form, Field } from "react-final-form";
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
const onSubmit = async (values) => {
await sleep(300);
window.alert(JSON.stringify(values, 0, 2));
};
const App = () => {
let submit;
return (
🏁
{" "}
React Final Form Example
External Submit
Read Docs
// { cancelable: true } required for Firefox
// https://github.com/facebook/react/issues/12639#issuecomment-382519193
document
.getElementById("exampleForm")
.dispatchEvent(
new Event("submit", { cancelable: true, bubbles: true }),
)
}
>
External Submit via document.getElementById()
{
submit(event);
}}
style={{ marginTop: 10 }}
>
External Submit via closure
External Submit via form attribute
{
submit = handleSubmit;
return (
First Name
Last Name
Favorite Color
❤️
{" "}
Red
💚
{" "}
Green
💙
{" "}
Blue
Submit
Reset
{JSON.stringify(values, 0, 2)}
);
}}
/>
);
};
render( , document.getElementById("root"));
================================================
FILE: examples/external-submit/package.json
================================================
{
"name": "react-final-form-external-submit-button",
"version": "1.0.0",
"description": "Demonstrates how you can use document.getElementById() or a closure to trigger a submit from outside the form.",
"keywords": [
"final-form",
"react",
"submit",
"external submit",
"react-final-form"
],
"main": "index.js",
"dependencies": {
"final-form": "4.20.4",
"react": "17.0.2",
"react-dom": "17.0.2",
"react-final-form": "6.5.3",
"styled-components": "4.2.0"
}
}
================================================
FILE: examples/external-submit/readme.md
================================================
# External Submit
[](https://codesandbox.io/s/1y7noyrlmq)
================================================
FILE: examples/field-arrays/Styles.js
================================================
import styled, { css } from "styled-components";
const btn = (light, dark) => css`
white-space: nowrap;
display: inline-block;
border-radius: 5px;
padding: 5px 15px;
font-size: 16px;
color: white;
&:visited {
color: white;
}
background-image: linear-gradient(${light}, ${dark});
border: 1px solid ${dark};
&:hover {
background-image: linear-gradient(${light}, ${dark});
&[disabled] {
background-image: linear-gradient(${light}, ${dark});
}
}
&:visited {
color: black;
}
&[disabled] {
opacity: 0.6;
cursor: not-allowed;
}
`;
const btnDefault = css`
${btn("#ffffff", "#d5d5d5")} color: #555;
`;
const btnPrimary = btn("#4f93ce", "#285f8f");
const btnDanger = btn("#e27c79", "#c9302c");
export default styled.div`
font-family: sans-serif;
h1 {
text-align: center;
color: #222;
}
h2 {
text-align: center;
color: #222;
}
& > div {
text-align: center;
}
a {
display: block;
text-align: center;
color: #222;
margin-bottom: 10px;
}
p {
max-width: 500px;
margin: 10px auto;
& > a {
display: inline;
}
}
form {
max-width: 600px;
margin: 10px auto;
border: 1px solid #ccc;
padding: 20px;
box-shadow: 2px 2px 5px rgba(0, 0, 0, 0.3);
border-radius: 3px;
position: relative;
& > div {
display: flex;
flex-flow: row nowrap;
line-height: 2em;
margin: 5px;
position: relative;
& > label {
color: #333;
width: 110px;
min-width: 60px;
font-size: 1em;
line-height: 32px;
}
& > input,
& > select,
& > textarea {
flex: 1;
padding: 3px 5px;
font-size: 1em;
margin-left: 15px;
border: 1px solid #ccc;
border-radius: 3px;
}
& > input[type="checkbox"] {
margin-top: 7px;
}
& > div {
margin-left: 16px;
& > label {
display: block;
& > input {
margin-right: 3px;
}
}
}
& > span {
line-height: 32px;
margin-left: 10px;
color: #800;
font-weight: bold;
}
& > button.remove {
${btnDanger};
}
}
& > .buttons {
display: flex;
flex-flow: row nowrap;
justify-content: center;
margin-top: 15px;
}
button {
margin: 0 10px;
&[type="submit"] {
${btnPrimary};
}
&[type="button"] {
${btnDefault};
}
}
.error {
display: flex;
font-weight: bold;
color: #800;
flex-flow: row nowrap;
justify-content: center;
}
pre {
position: relative;
border: 1px solid #ccc;
background: rgba(0, 0, 0, 0.1);
box-shadow: inset 1px 1px 3px rgba(0, 0, 0, 0.2);
padding: 20px;
}
}
`;
================================================
FILE: examples/field-arrays/index.js
================================================
import React from "react";
import { render } from "react-dom";
import Styles from "./Styles";
import { Form, Field } from "react-final-form";
import arrayMutators from "final-form-arrays";
import { FieldArray } from "react-final-form-arrays";
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
const onSubmit = async (values) => {
await sleep(300);
window.alert(JSON.stringify(values, 0, 2));
};
const App = () => (
🏁
{" "}
React Final Form - Array Fields
Read Docs
{
return (
Company
push("customers", undefined)}
>
Add Customer
pop("customers")}>
Remove Customer
{({ fields }) =>
fields.map((name, index) => (
Cust. #{index + 1}
fields.remove(index)}
style={{ cursor: "pointer" }}
role="img"
aria-label="remove customer icon"
>
❌
))
}
Submit
Reset
{JSON.stringify(values, 0, 2)}
);
}}
/>
);
render( , document.getElementById("root"));
================================================
FILE: examples/field-arrays/package.json
================================================
{
"name": "react-final-form-field-arrays",
"version": "1.0.0",
"description": "This example demonstrates how to use helper packages final-form-arrays and react-final-form-arrays to manage array fields.",
"keywords": [],
"main": "index.js",
"dependencies": {
"final-form": "5.0.0-0",
"final-form-arrays": "1.1.2",
"react": "17.0.2",
"react-dom": "17.0.2",
"react-final-form": "6.5.3",
"react-final-form-arrays": "2.0.3",
"styled-components": "4.2.0"
}
}
================================================
FILE: examples/field-arrays/readme.md
================================================
# Field Arrays
[](https://codesandbox.io/s/kx8qv67nk5)
================================================
FILE: examples/field-level-validation/Styles.js
================================================
import styled, { css } from "styled-components";
const btn = (light, dark) => css`
white-space: nowrap;
display: inline-block;
border-radius: 5px;
padding: 5px 15px;
font-size: 16px;
color: white;
&:visited {
color: white;
}
background-image: linear-gradient(${light}, ${dark});
border: 1px solid ${dark};
&:hover {
background-image: linear-gradient(${light}, ${dark});
&[disabled] {
background-image: linear-gradient(${light}, ${dark});
}
}
&:visited {
color: black;
}
&[disabled] {
opacity: 0.6;
cursor: not-allowed;
}
`;
const btnDefault = css`
${btn("#ffffff", "#d5d5d5")} color: #555;
`;
const btnPrimary = btn("#4f93ce", "#285f8f");
export default styled.div`
font-family: sans-serif;
h1 {
text-align: center;
color: #222;
}
h2 {
text-align: center;
color: #222;
}
& > div {
text-align: center;
}
a {
display: block;
text-align: center;
color: #222;
}
form {
max-width: 500px;
margin: 10px auto;
border: 1px solid #ccc;
padding: 20px;
box-shadow: 2px 2px 5px rgba(0, 0, 0, 0.3);
border-radius: 3px;
& > div {
display: flex;
flex-flow: row nowrap;
line-height: 2em;
margin: 5px;
& > label {
color: #333;
width: 110px;
font-size: 1em;
line-height: 32px;
}
& > input,
& > select,
& > textarea {
flex: 1;
padding: 3px 5px;
font-size: 1em;
margin-left: 15px;
border: 1px solid #ccc;
border-radius: 3px;
}
& > input[type="checkbox"] {
margin-top: 7px;
}
& > div {
margin-left: 16px;
& > label {
display: block;
& > input {
margin-right: 3px;
}
}
}
& > span {
line-height: 32px;
margin-left: 10px;
color: #800;
font-weight: bold;
}
}
& > .buttons {
display: flex;
flex-flow: row nowrap;
justify-content: center;
margin-top: 15px;
}
button {
margin: 0 10px;
&[type="submit"] {
${btnPrimary};
}
&[type="button"] {
${btnDefault};
}
}
pre {
border: 1px solid #ccc;
background: rgba(0, 0, 0, 0.1);
box-shadow: inset 1px 1px 3px rgba(0, 0, 0, 0.2);
padding: 20px;
}
}
`;
================================================
FILE: examples/field-level-validation/index.js
================================================
import React from "react";
import { render } from "react-dom";
import Styles from "./Styles";
import { Form, Field } from "react-final-form";
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
const onSubmit = async (values) => {
await sleep(300);
window.alert(JSON.stringify(values, 0, 2));
};
const required = (value) => (value ? undefined : "Required");
const mustBeNumber = (value) => (isNaN(value) ? "Must be a number" : undefined);
const minValue = (min) => (value) =>
isNaN(value) || value >= min ? undefined : `Should be greater than ${min}`;
const composeValidators =
(...validators) =>
(value) =>
validators.reduce(
(error, validator) => error || validator(value),
undefined,
);
const App = () => (
React Final Form Example
Synchronous Field-Level Validation
Read Docs
(
{({ input, meta }) => (
First Name
{meta.error && meta.touched && {meta.error} }
)}
{({ input, meta }) => (
Last Name
{meta.error && meta.touched && {meta.error} }
)}
{({ input, meta }) => (
Age
{meta.error && meta.touched && {meta.error} }
)}
Submit
Reset
{JSON.stringify(values, 0, 2)}
)}
/>
);
render( , document.getElementById("root"));
================================================
FILE: examples/field-level-validation/package.json
================================================
{
"name": "React Final Form - Synchronous Field Level Validation",
"version": "1.0.0",
"description": "An example of a form with synchronous field-level validation created with React Final Form",
"keywords": [
"state management",
"react-final-form",
"form",
"final-form",
"react"
],
"main": "index.js",
"dependencies": {
"styled-components": "latest",
"react-final-form": "6.5.3",
"react": "latest",
"react-dom": "latest",
"final-form": "4.20.4"
}
}
================================================
FILE: examples/field-level-validation/readme.md
================================================
# Field Level Validation Example
[](https://codesandbox.io/s/github/final-form/react-final-form/tree/master/examples/field-level-validation?fontsize=14)
================================================
FILE: examples/field-warnings/Styles.js
================================================
import styled, { css } from "styled-components";
const btn = (light, dark) => css`
white-space: nowrap;
display: inline-block;
border-radius: 5px;
padding: 5px 15px;
font-size: 16px;
color: white;
&:visited {
color: white;
}
background-image: linear-gradient(${light}, ${dark});
border: 1px solid ${dark};
&:hover {
background-image: linear-gradient(${light}, ${dark});
&[disabled] {
background-image: linear-gradient(${light}, ${dark});
}
}
&:visited {
color: black;
}
&[disabled] {
opacity: 0.6;
cursor: not-allowed;
}
`;
const btnDefault = css`
${btn("#ffffff", "#d5d5d5")} color: #555;
`;
const btnPrimary = btn("#4f93ce", "#285f8f");
const btnDanger = btn("#e27c79", "#c9302c");
export default styled.div`
font-family: sans-serif;
h1 {
text-align: center;
color: #222;
}
h2 {
text-align: center;
color: #222;
}
& > div {
text-align: center;
}
a {
display: block;
text-align: center;
color: #222;
margin-bottom: 10px;
}
p {
max-width: 500px;
margin: 10px auto;
& > a {
display: inline;
}
}
form {
max-width: 500px;
margin: 10px auto;
border: 1px solid #ccc;
padding: 20px;
box-shadow: 2px 2px 5px rgba(0, 0, 0, 0.3);
border-radius: 3px;
position: relative;
.loading {
text-align: center;
display: block;
position: absolute;
background: url("https://media.giphy.com/media/130AxGoOaR6t0I/giphy.gif")
center center;
background-size: fill;
font-size: 2em;
top: 0;
left: 0;
right: 0;
bottom: 0;
padding: 50px 0 0 0;
z-index: 2;
}
& > div {
display: flex;
flex-flow: row nowrap;
line-height: 2em;
margin: 5px;
position: relative;
& > label {
color: #333;
width: 110px;
min-width: 60px;
font-size: 1em;
line-height: 32px;
}
& > input,
& > select,
& > textarea {
flex: 1;
padding: 3px 5px;
font-size: 1em;
margin-left: 15px;
border: 1px solid #ccc;
border-radius: 3px;
}
& > input[type="checkbox"] {
margin-top: 7px;
}
& > div {
margin-left: 16px;
& > label {
display: block;
& > input {
margin-right: 3px;
}
}
}
& > span {
line-height: 32px;
margin-left: 10px;
color: #800;
font-weight: bold;
}
& > button.remove {
${btnDanger};
}
}
& > .buttons {
display: flex;
flex-flow: row nowrap;
justify-content: center;
margin-top: 15px;
}
button {
margin: 0 10px;
&[type="submit"] {
${btnPrimary};
}
&[type="button"] {
${btnDefault};
}
}
.error {
display: flex;
font-weight: bold;
color: #800;
flex-flow: row nowrap;
justify-content: center;
}
pre {
position: relative;
border: 1px solid #ccc;
background: rgba(0, 0, 0, 0.1);
box-shadow: inset 1px 1px 3px rgba(0, 0, 0, 0.2);
padding: 20px;
}
}
`;
================================================
FILE: examples/field-warnings/index.js
================================================
import React from "react";
import { render } from "react-dom";
import Styles from "./Styles";
import { WarningEngine } from "./warning-engine";
import { Form, Field } from "react-final-form";
import setFieldData from "final-form-set-field-data";
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
const onSubmit = async (values) => {
await sleep(300);
window.alert(JSON.stringify(values, 0, 2));
};
const App = () => (
🏁
{" "}
React Final Form Example
⚠️
{" "}
Warnings{" "}
⚠️
Read Docs
Warnings, in this example, are defined as: suggestions to the user, like
validation errors, but that do not prevent submission. Note that the{" "}
<WarningEngine/> component must be at the bottom of the
form to guarantee that all the fields have registered.
{
return (
{({ input, meta }) => (
First Name
{meta.touched && meta.data.warning && (
{meta.data.warning}
)}
)}
{({ input, meta }) => (
Last Name
{meta.touched && meta.data.warning && (
{meta.data.warning}
)}
)}
Submit
Reset
{JSON.stringify(values, 0, 2)}
);
}}
/>
);
render( , document.getElementById("root"));
================================================
FILE: examples/field-warnings/package.json
================================================
{
"name": "react-final-form-warnings",
"version": "1.0.0",
"description": "Demonstrates how to use final-form-set-field-data to set warnings on fields.",
"keywords": [],
"homepage": "https://codesandbox.io/s/m5qwxpr6o8",
"main": "index.js",
"dependencies": {
"styled-components": "2.2.4",
"react-final-form": "6.5.3",
"react-dom": "17.0.2",
"react": "17.0.2",
"final-form-set-field-data": "1.0.0",
"final-form": "4.20.4"
}
}
================================================
FILE: examples/field-warnings/readme.md
================================================
# Field Warnings
[](https://codesandbox.io/s/react-final-form-warnings-0nimt)
================================================
FILE: examples/field-warnings/warning-engine.js
================================================
import React from "react";
import { FormSpy } from "react-final-form";
export const WarningEngine = ({ mutators: { setFieldData } }) => (
{
setFieldData("firstName", {
warning: values.firstName ? undefined : "Recommended",
});
setFieldData("lastName", {
warning: values.lastName ? undefined : "Recommended",
});
}}
/>
);
================================================
FILE: examples/fields-component/Styles.js
================================================
import styled, { css } from "styled-components";
const btn = (light, dark) => css`
white-space: nowrap;
display: inline-block;
border-radius: 5px;
padding: 5px 15px;
font-size: 16px;
color: white;
&:visited {
color: white;
}
background-image: linear-gradient(${light}, ${dark});
border: 1px solid ${dark};
&:hover {
background-image: linear-gradient(${light}, ${dark});
&[disabled] {
background-image: linear-gradient(${light}, ${dark});
}
}
&:visited {
color: black;
}
&[disabled] {
opacity: 0.6;
cursor: not-allowed;
}
`;
const btnDefault = css`
${btn("#ffffff", "#d5d5d5")} color: #555;
`;
const btnPrimary = btn("#4f93ce", "#285f8f");
export default styled.div`
font-family: sans-serif;
h1 {
text-align: center;
color: #222;
}
h2 {
text-align: center;
color: #222;
}
& > div {
text-align: center;
}
a {
display: block;
text-align: center;
color: #222;
}
form {
max-width: 500px;
margin: 10px auto;
border: 1px solid #ccc;
padding: 20px;
box-shadow: 2px 2px 5px rgba(0, 0, 0, 0.3);
border-radius: 3px;
& > div {
display: flex;
flex-flow: row nowrap;
line-height: 2em;
margin: 5px;
& > label {
color: #333;
width: 110px;
font-size: 1em;
line-height: 32px;
}
& > input,
& > select,
& > textarea {
flex: 1;
padding: 3px 5px;
font-size: 1em;
margin-left: 15px;
border: 1px solid #ccc;
border-radius: 3px;
}
& > input[type="checkbox"] {
margin-top: 7px;
}
& > div {
margin-left: 16px;
& > label {
display: block;
& > input {
margin-right: 3px;
}
}
}
}
& > .buttons {
display: flex;
flex-flow: row nowrap;
justify-content: center;
margin-top: 15px;
}
button {
margin: 0 10px;
&[type="submit"] {
${btnPrimary};
}
&[type="button"] {
${btnDefault};
}
}
pre {
border: 1px solid #ccc;
background: rgba(0, 0, 0, 0.1);
box-shadow: inset 1px 1px 3px rgba(0, 0, 0, 0.2);
padding: 20px;
}
}
`;
================================================
FILE: examples/fields-component/index.js
================================================
import React from "react";
import { render } from "react-dom";
import Styles from "./Styles";
import { Form, Field } from "react-final-form";
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
const onSubmit = async (values) => {
await sleep(300);
window.alert(JSON.stringify(values, 0, 2));
};
const Fields = ({
names,
subscription,
fieldsState = {},
children,
originalRender,
}) => {
if (!names.length) {
return (originalRender || children)(fieldsState);
}
const [name, ...rest] = names;
return (
{(fieldState) => (
)}
);
};
const App = () => (
🏁
{" "}
React Final Form
Fields Component
Read Docs
(
First Name
Last Name
Employed
Notes
Submit
Reset
{(fieldsState) => (
{JSON.stringify(fieldsState, undefined, 2)}
)}
)}
/>
);
render( , document.getElementById("root"));
================================================
FILE: examples/fields-component/package.json
================================================
{
"name": "react-final-form-fields-component",
"version": "1.0.0",
"description": "One common question from people migrating from Redux-Form is: Where's the Fields component?\n\nThe answer is that it's so easy to write one that one needn't ship with the library itself.",
"keywords": [],
"homepage": "https://codesandbox.io/s/ww40y2m595",
"main": "index.js",
"dependencies": {
"styled-components": "latest",
"react-fontawesome": "latest",
"react-final-form": "6.5.3",
"react-dom": "latest",
"react": "latest",
"final-form": "4.20.4"
}
}
================================================
FILE: examples/fields-component/readme.md
================================================
# Fields Component
[](https://codesandbox.io/s/pyrwplknom)
================================================
FILE: examples/focus-first-error/Styles.js
================================================
import styled, { css } from "styled-components";
const btn = (light, dark) => css`
white-space: nowrap;
display: inline-block;
border-radius: 5px;
padding: 5px 15px;
font-size: 16px;
color: white;
&:visited {
color: white;
}
background-image: linear-gradient(${light}, ${dark});
border: 1px solid ${dark};
&:hover {
background-image: linear-gradient(${light}, ${dark});
&[disabled] {
background-image: linear-gradient(${light}, ${dark});
}
}
&:visited {
color: black;
}
&[disabled] {
opacity: 0.6;
cursor: not-allowed;
}
`;
const btnDefault = css`
${btn("#ffffff", "#d5d5d5")} color: #555;
`;
const btnPrimary = btn("#4f93ce", "#285f8f");
const btnDanger = btn("#e27c79", "#c9302c");
export default styled.div`
font-family: sans-serif;
h1 {
text-align: center;
color: #222;
}
h2 {
text-align: center;
color: #222;
}
& > div {
text-align: center;
}
a {
display: block;
text-align: center;
color: #222;
margin-bottom: 10px;
}
p {
max-width: 500px;
margin: 10px auto;
& > a {
display: inline;
}
}
.loading {
font-size: 2em;
font-weight: bold;
text-align: center;
margin: 50px;
}
form,
div.form {
text-align: left;
max-width: 500px;
margin: 10px auto;
border: 1px solid #ccc;
padding: 20px;
box-shadow: 2px 2px 5px rgba(0, 0, 0, 0.3);
border-radius: 3px;
position: relative;
& > div {
display: flex;
flex-flow: row nowrap;
line-height: 2em;
position: relative;
padding: 8px 5px;
border: 1px solid transparent;
&.active {
background-color: paleturquoise;
border-color: turquoise;
}
& > label {
color: #333;
width: 110px;
min-width: 60px;
font-size: 1em;
line-height: 32px;
}
& > input,
& > .downshift > input,
& > select,
& > textarea {
flex: 1;
padding: 3px 5px;
font-size: 1em;
margin-left: 15px;
border: 1px solid #ccc;
border-radius: 3px;
}
& > input[type="checkbox"] {
margin-top: 7px;
}
& > div {
margin-left: 16px;
& > label {
margin-left: 0;
display: block;
& > input {
margin-right: 3px;
}
}
&.downshift {
margin-left: 0;
padding-left: 15px;
flex: 1;
& > input {
width: 100%;
padding: 6px 5px;
font-size: 1em;
margin-left: 0;
border: 1px solid #ccc;
border-radius: 3px;
}
}
}
& > span {
line-height: 32px;
margin-left: 10px;
color: #800;
font-weight: bold;
}
& > button.remove {
${btnDanger};
}
}
& > .buttons {
display: flex;
flex-flow: row nowrap;
justify-content: center;
margin-top: 15px;
}
.error {
display: flex;
font-weight: bold;
color: #800;
flex-flow: row nowrap;
justify-content: center;
}
pre {
position: relative;
border: 1px solid #ccc;
background: rgba(0, 0, 0, 0.1);
box-shadow: inset 1px 1px 3px rgba(0, 0, 0, 0.2);
padding: 20px;
}
.submitting {
display: block;
position: absolute;
top: -5px;
left: -5px;
right: -5px;
padding: 0;
text-align: center;
background: rgba(0, 0, 0, 0.4);
color: white;
z-index: 10;
font-weight: bold;
font-size: 0.8em;
}
.saving {
font-size: 0.8em;
font-weight: bold;
color: darkblue;
margin: 8px 0 0 7px;
}
}
button {
margin: 0 10px;
&[type="submit"] {
${btnPrimary};
}
&[type="button"] {
${btnDefault};
}
}
.downshift-options {
border: 1px solid #ddd;
box-shadow: 1px 1px 2px rgba(0, 0, 0, 0.3);
& > div {
padding: 3px 5px;
}
}
`;
================================================
FILE: examples/focus-first-error/index.js
================================================
import React from "react";
import { render } from "react-dom";
import Styles from "./Styles";
import { Form, Field } from "react-final-form";
import createDecorator from "final-form-focus";
import validate from "./validate";
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
const onSubmit = async (values) => {
await sleep(300);
window.alert(JSON.stringify(values, 0, 2));
};
const InputRow = ({ label, type, input, meta: { active, error, touched } }) => (
{label}
{error && touched && {error} }
);
const focusOnError = createDecorator();
const App = () => (
🏁
{" "}
React Final Form Example
Focus On First Error
Read Docs
Demonstrates how to use the{" "}
🏁
{" "}
Final Form Focus{" "}
🧐
{" "}
library as a pluggable{" "}
🏁
{" "}
Final Form decorator to provide "focus on first error" functionality.
Notice what when you click the Submit button, the focus is placed on the
first field with an error.
(
Submit
Reset
{JSON.stringify(values, 0, 2)}
)}
/>
);
render( , document.getElementById("root"));
================================================
FILE: examples/focus-first-error/package.json
================================================
{
"name": "react-final-form-focus-on-first-error",
"version": "1.0.0",
"description": "Demonstrates how to use the 🏁 Final Form Focus 🧐 decorator to focus on the first input with an error upon submission.",
"keywords": [],
"homepage": "https://codesandbox.io/s/new",
"main": "src/index.js",
"dependencies": {
"final-form": "4.20.4",
"final-form-focus": "1.1.2",
"react": "17.0.2",
"react-dom": "17.0.2",
"react-final-form": "6.5.3",
"react-scripts": "3.0.1",
"styled-components": "4.2.0"
},
"devDependencies": {},
"scripts": {
"start": "react-scripts start",
"build": "react-scripts build",
"test": "react-scripts test --env=jsdom",
"eject": "react-scripts eject"
}
}
================================================
FILE: examples/focus-first-error/readme.md
================================================
# Focus on First Error
[](https://codesandbox.io/s/6174kqr403)
================================================
FILE: examples/focus-first-error/validate.js
================================================
export default (values) => {
const errors = {};
if (!values.firstName) {
errors.firstName = "Required";
}
if (!values.lastName) {
errors.lastName = "Required";
}
if (!values.street) {
errors.street = "Required";
}
if (!values.age) {
errors.age = "Required";
} else if (isNaN(values.age)) {
errors.age = "Must be a number";
}
return errors;
};
================================================
FILE: examples/format-on-blur/Styles.js
================================================
import styled, { css } from "styled-components";
const btn = (light, dark) => css`
white-space: nowrap;
display: inline-block;
border-radius: 5px;
padding: 5px 15px;
font-size: 16px;
color: white;
&:visited {
color: white;
}
background-image: linear-gradient(${light}, ${dark});
border: 1px solid ${dark};
&:hover {
background-image: linear-gradient(${light}, ${dark});
&[disabled] {
background-image: linear-gradient(${light}, ${dark});
}
}
&:visited {
color: black;
}
&[disabled] {
opacity: 0.6;
cursor: not-allowed;
}
`;
const btnDefault = css`
${btn("#ffffff", "#d5d5d5")} color: #555;
`;
const btnPrimary = btn("#4f93ce", "#285f8f");
const btnDanger = btn("#e27c79", "#c9302c");
export default styled.div`
font-family: sans-serif;
h1 {
text-align: center;
color: #222;
}
h2 {
text-align: center;
color: #222;
}
& > div {
text-align: center;
}
a {
display: block;
text-align: center;
color: #222;
margin-bottom: 10px;
}
p {
max-width: 500px;
margin: 10px auto;
& > a {
display: inline;
}
}
.loading {
font-size: 2em;
font-weight: bold;
text-align: center;
margin: 50px;
}
form,
div.form {
text-align: left;
max-width: 500px;
margin: 10px auto;
border: 1px solid #ccc;
padding: 20px;
box-shadow: 2px 2px 5px rgba(0, 0, 0, 0.3);
border-radius: 3px;
position: relative;
& > div {
display: flex;
flex-flow: row nowrap;
line-height: 2em;
position: relative;
padding: 8px 5px;
border: 1px solid transparent;
&.active {
background-color: paleturquoise;
border-color: turquoise;
}
& > label {
color: #333;
width: 110px;
min-width: 60px;
font-size: 1em;
line-height: 32px;
}
& > input,
& > .downshift > input,
& > select,
& > textarea {
font-family: monospace, monospace;
flex: 1;
padding: 6px 9px;
font-size: 1em;
margin-left: 15px;
border: 1px solid #ccc;
border-radius: 3px;
&[disabled] {
background: #eee;
}
}
& > .jsonInput {
flex: 1;
font-size: 1em;
margin-left: 15px;
textarea {
width: 100%;
padding: 6px 9px;
height: 150px;
font-family: monospace, monospace;
}
}
& > input[type="checkbox"] {
margin-top: 7px;
}
& > div {
margin-left: 16px;
& > label {
margin-left: 0;
display: block;
& > input {
margin-right: 3px;
}
}
&.downshift {
margin-left: 0;
padding-left: 15px;
flex: 1;
& > input {
width: 100%;
padding: 6px 5px;
font-size: 1em;
margin-left: 0;
border: 1px solid #ccc;
border-radius: 3px;
}
}
}
& > span {
line-height: 32px;
margin-left: 10px;
color: #800;
font-weight: bold;
}
& > button.remove {
${btnDanger};
}
}
& > .buttons {
display: flex;
flex-flow: row nowrap;
justify-content: center;
margin-top: 15px;
}
.error {
display: flex;
font-weight: bold;
color: #800;
flex-flow: row nowrap;
justify-content: center;
}
pre {
position: relative;
border: 1px solid #ccc;
background: rgba(0, 0, 0, 0.1);
box-shadow: inset 1px 1px 3px rgba(0, 0, 0, 0.2);
padding: 20px;
}
.submitting {
display: block;
position: absolute;
top: -5px;
left: -5px;
right: -5px;
padding: 0;
text-align: center;
background: rgba(0, 0, 0, 0.4);
color: white;
z-index: 10;
font-weight: bold;
font-size: 0.8em;
}
.saving {
font-size: 0.8em;
font-weight: bold;
color: darkblue;
margin: 8px 0 0 7px;
}
}
button {
margin: 0 10px;
&[type="submit"] {
${btnPrimary};
}
&[type="button"] {
${btnDefault};
}
}
.downshift-options {
border: 1px solid #ddd;
box-shadow: 1px 1px 2px rgba(0, 0, 0, 0.3);
& > div {
padding: 3px 5px;
}
}
`;
================================================
FILE: examples/format-on-blur/index.js
================================================
import React from "react";
import { render } from "react-dom";
import Styles from "./Styles";
import { Form, Field } from "react-final-form";
import numeral from "numeral";
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
const onSubmit = async (values) => {
await sleep(300);
window.alert(JSON.stringify(values, 0, 2));
};
const formatPrice = (value) =>
value === undefined
? "" // make controlled
: numeral(value).format("$0,0.00");
const App = () => (
🏁
{" "}
React Final Form
Format On Blur Example
Read Docs
By default the format function given to Field is
called every time the component is rendered. But that can lead to a
difficult UX for some types of values. That's why there is a{" "}
formatOnBlur flag that will prevent the format{" "}
function from being called until the field is blurred.
(
Product Name
Price
Submit
Reset
{JSON.stringify(values, 0, 2)}
)}
/>
);
render( , document.getElementById("root"));
================================================
FILE: examples/format-on-blur/package.json
================================================
{
"name": "react-final-form-format-on-blur-example",
"version": "1.0.0",
"description": "Demonstrates how to use the formatOnBlur prop to postpone the formatting of a form field value until the field loses focus.",
"keywords": [
"react-final-form",
"final-form",
"format",
"form",
"currency"
],
"main": "src/index.js",
"dependencies": {
"final-form": "4.20.4",
"numeral": "2.0.6",
"react": "17.0.2",
"react-dom": "17.0.2",
"react-final-form": "6.5.3",
"react-scripts": "3.0.1",
"styled-components": "4.2.0"
},
"devDependencies": {},
"scripts": {
"start": "react-scripts start",
"build": "react-scripts build",
"test": "react-scripts test --env=jsdom",
"eject": "react-scripts eject"
}
}
================================================
FILE: examples/format-on-blur/readme.md
================================================
# Format on Blur
[](https://codesandbox.io/s/3rp260ly51)
================================================
FILE: examples/format-string-by-pattern/Styles.js
================================================
import styled, { css } from "styled-components";
const btn = (light, dark) => css`
white-space: nowrap;
display: inline-block;
border-radius: 5px;
padding: 5px 15px;
font-size: 16px;
color: white;
&:visited {
color: white;
}
background-image: linear-gradient(${light}, ${dark});
border: 1px solid ${dark};
&:hover {
background-image: linear-gradient(${light}, ${dark});
&[disabled] {
background-image: linear-gradient(${light}, ${dark});
}
}
&:visited {
color: black;
}
&[disabled] {
opacity: 0.6;
cursor: not-allowed;
}
`;
const btnDefault = css`
${btn("#ffffff", "#d5d5d5")} color: #555;
`;
const btnPrimary = btn("#4f93ce", "#285f8f");
const btnDanger = btn("#e27c79", "#c9302c");
export default styled.div`
font-family: sans-serif;
h1 {
text-align: center;
color: #222;
}
h2 {
text-align: center;
color: #222;
}
& > div {
text-align: center;
}
a {
display: block;
text-align: center;
color: #222;
margin-bottom: 10px;
}
p {
max-width: 500px;
margin: 10px auto;
& > a {
display: inline;
}
}
form {
max-width: 500px;
margin: 10px auto;
border: 1px solid #ccc;
padding: 20px;
box-shadow: 2px 2px 5px rgba(0, 0, 0, 0.3);
border-radius: 3px;
position: relative;
.loading {
text-align: center;
display: block;
position: absolute;
background: url("https://media.giphy.com/media/130AxGoOaR6t0I/giphy.gif")
center center;
background-size: fill;
font-size: 2em;
top: 0;
left: 0;
right: 0;
bottom: 0;
padding: 50px 0 0 0;
z-index: 2;
}
& > div {
display: flex;
flex-flow: row nowrap;
line-height: 2em;
margin: 5px;
position: relative;
& > label {
color: #333;
width: 110px;
min-width: 60px;
font-size: 1em;
line-height: 32px;
}
& > input,
& > select,
& > textarea {
flex: 1;
padding: 3px 5px;
font-size: 1em;
margin-left: 15px;
border: 1px solid #ccc;
border-radius: 3px;
}
& > input[type="checkbox"] {
margin-top: 7px;
}
& > div {
margin-left: 16px;
& > label {
display: block;
& > input {
margin-right: 3px;
}
}
}
& > span {
line-height: 32px;
margin-left: 10px;
color: #800;
font-weight: bold;
}
& > button.remove {
${btnDanger};
}
}
& > .buttons {
display: flex;
flex-flow: row nowrap;
justify-content: center;
margin-top: 15px;
}
.error {
display: flex;
font-weight: bold;
color: #800;
flex-flow: row nowrap;
justify-content: center;
}
pre {
position: relative;
border: 1px solid #ccc;
background: rgba(0, 0, 0, 0.1);
box-shadow: inset 1px 1px 3px rgba(0, 0, 0, 0.2);
padding: 20px;
}
}
button {
margin: 0 10px;
&[type="submit"] {
${btnPrimary};
}
&[type="button"] {
${btnDefault};
}
}
`;
================================================
FILE: examples/format-string-by-pattern/index.js
================================================
import React from "react";
import { render } from "react-dom";
import Styles from "./Styles";
import { Form, Field } from "react-final-form";
import formatString from "format-string-by-pattern";
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
const onSubmit = async (values) => {
await sleep(300);
window.alert(JSON.stringify(values, 0, 2));
};
const masks = [
{ name: "phone-1", parse: "999-999-9999" },
{ name: "phone-2", parse: "(999) 999-9999" },
{ name: "phone-3", parse: "+49 (AAAA) BBBBBB" },
{ name: "cep 🇧🇷", parse: "12345-678" },
{ name: "cpf 🇧🇷", parse: "XXX.XXX.XXX-XX" },
{ name: "cnpj 🇧🇷", parse: "XX.XXX.XXX/XXXX-XX" },
];
const App = () => (
🏁
{" "}
React Final Form
(
{masks.map((mask) => (
{mask.name}
))}
Submit
Reset
{JSON.stringify(values, 0, 2)}
)}
/>
);
render( , document.getElementById("root"));
================================================
FILE: examples/format-string-by-pattern/package.json
================================================
{
"name": "format-string-by-pattern-with-react-final-form",
"version": "1.0.0",
"description": "Demonstrates how to use the parse prop to format the inputs using format-string-by-pattern.",
"keywords": [],
"main": "index.js",
"dependencies": {
"final-form": "4.20.4",
"format-string-by-pattern": "1.1.1",
"react": "17.0.2",
"react-dom": "17.0.2",
"react-final-form": "6.5.3",
"styled-components": "latest"
}
}
================================================
FILE: examples/format-string-by-pattern/readme.md
================================================
# Format String by Pattern
[](https://codesandbox.io/s/no20p7z3l)
================================================
FILE: examples/hybrid-sync-async-record-level-validation/Spinner.js
================================================
import styled, { keyframes } from "styled-components";
const rotation = keyframes`
from {
-webkit-transform: rotate(0deg);
}
to {
-webkit-transform: rotate(359deg);
}
`;
export default styled.div`
height: 12px;
width: 12px;
margin-left: 5px;
position: absolute;
right: 0;
top: 0;
animation: ${rotation} 0.6s infinite linear;
border-left: 6px solid rgba(0, 174, 239, 0.15);
border-right: 6px solid rgba(0, 174, 239, 0.15);
border-bottom: 6px solid rgba(0, 174, 239, 0.15);
border-top: 6px solid rgba(0, 174, 239, 0.8);
border-radius: 100%;
`;
================================================
FILE: examples/hybrid-sync-async-record-level-validation/Styles.js
================================================
import styled, { css } from "styled-components";
const btn = (light, dark) => css`
white-space: nowrap;
display: inline-block;
border-radius: 5px;
padding: 5px 15px;
font-size: 16px;
color: white;
&:visited {
color: white;
}
background-image: linear-gradient(${light}, ${dark});
border: 1px solid ${dark};
&:hover {
background-image: linear-gradient(${light}, ${dark});
&[disabled] {
background-image: linear-gradient(${light}, ${dark});
}
}
&:visited {
color: black;
}
&[disabled] {
opacity: 0.6;
cursor: not-allowed;
}
`;
const btnDefault = css`
${btn("#ffffff", "#d5d5d5")} color: #555;
`;
const btnPrimary = btn("#4f93ce", "#285f8f");
export default styled.div`
font-family: sans-serif;
h1 {
text-align: center;
color: #222;
}
h2 {
text-align: center;
color: #222;
}
& > div {
text-align: center;
}
a {
display: block;
text-align: center;
color: #222;
margin-bottom: 10px;
}
form {
max-width: 500px;
margin: 10px auto;
border: 1px solid #ccc;
padding: 20px;
box-shadow: 2px 2px 5px rgba(0, 0, 0, 0.3);
border-radius: 3px;
position: relative;
& > div {
display: flex;
flex-flow: row nowrap;
line-height: 2em;
margin: 5px;
& > label {
color: #333;
width: 110px;
font-size: 1em;
line-height: 32px;
}
& > input,
& > select,
& > textarea {
flex: 1;
padding: 3px 5px;
font-size: 1em;
margin-left: 15px;
border: 1px solid #ccc;
border-radius: 3px;
}
& > input[type="checkbox"] {
margin-top: 7px;
}
& > div {
margin-left: 16px;
& > label {
display: block;
& > input {
margin-right: 3px;
}
}
}
& > span {
line-height: 32px;
margin-left: 10px;
color: #800;
font-weight: bold;
}
}
& > .buttons {
display: flex;
flex-flow: row nowrap;
justify-content: center;
margin-top: 15px;
}
button {
margin: 0 10px;
&[type="submit"] {
${btnPrimary};
}
&[type="button"] {
${btnDefault};
}
}
pre {
border: 1px solid #ccc;
background: rgba(0, 0, 0, 0.1);
box-shadow: inset 1px 1px 3px rgba(0, 0, 0, 0.2);
padding: 20px;
}
}
`;
================================================
FILE: examples/hybrid-sync-async-record-level-validation/index.js
================================================
import React from "react";
import { render } from "react-dom";
import Styles from "./Styles";
import Spinner from "./Spinner";
import { Form, Field } from "react-final-form";
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
const onSubmit = async (values) => {
await sleep(300);
window.alert(JSON.stringify(values, 0, 2));
};
const verifyUsername = async (values) => {
await sleep(400);
if (
~["john", "paul", "george", "ringo"].indexOf(
values.username && values.username.toLowerCase(),
)
) {
return { username: "Username taken!" };
}
};
const App = () => (
🏁
{" "}
React Final Form Example
Hybrid Synchronous/Asynchronous Record-Level Validation
Read Docs
Usernames John, Paul, George or Ringo will fail async validation.
{
const errors = {};
if (!values.username) {
errors.username = "Required";
}
if (!values.password) {
errors.password = "Required";
}
if (!values.confirm) {
errors.confirm = "Required";
} else if (values.confirm !== values.password) {
errors.confirm = "Does not match";
}
return Object.keys(errors).length ? errors : verifyUsername(values);
}}
render={({
handleSubmit,
form,
submitting,
pristine,
validating,
values,
}) => (
{validating && }
{({ input, meta }) => (
Username
{meta.error && meta.touched && {meta.error} }
)}
{({ input, meta }) => (
Password
{meta.error && meta.touched && {meta.error} }
)}
{({ input, meta }) => (
Confirm
{meta.error && meta.touched && {meta.error} }
)}
Submit
Reset
{JSON.stringify(values, 0, 2)}
)}
/>
);
render( , document.getElementById("root"));
================================================
FILE: examples/hybrid-sync-async-record-level-validation/package.json
================================================
{
"name": "react-final-form-hybrid-synchronous-asynchronous-record-level-validation-example",
"version": "1.0.0",
"description": "This example demonstrates how you can mix synchronous and asynchronous validation in the same record-level validation function.",
"keywords": [
"react-final-form",
"form",
"final-form",
"asynchronous",
"validation"
],
"main": "index.js",
"dependencies": {
"final-form": "4.20.4",
"react": "17.0.2",
"react-dom": "17.0.2",
"react-final-form": "6.5.3",
"styled-components": "4.2.0"
}
}
================================================
FILE: examples/hybrid-sync-async-record-level-validation/readme.md
================================================
# Hybrid Synchronous/Asynchronous Record-level Validation
[](https://codesandbox.io/s/kl9n295n5)
================================================
FILE: examples/independent-error-component-render-props/Styles.js
================================================
import styled, { css } from "styled-components";
const btn = (light, dark) => css`
white-space: nowrap;
display: inline-block;
border-radius: 5px;
padding: 5px 15px;
font-size: 16px;
color: white;
&:visited {
color: white;
}
background-image: linear-gradient(${light}, ${dark});
border: 1px solid ${dark};
&:hover {
background-image: linear-gradient(${light}, ${dark});
&[disabled] {
background-image: linear-gradient(${light}, ${dark});
}
}
&:visited {
color: black;
}
&[disabled] {
opacity: 0.6;
cursor: not-allowed;
}
`;
const btnDefault = css`
${btn("#ffffff", "#d5d5d5")} color: #555;
`;
const btnPrimary = btn("#4f93ce", "#285f8f");
const btnDanger = btn("#e27c79", "#c9302c");
export default styled.div`
font-family: sans-serif;
h1 {
text-align: center;
color: #222;
}
h2 {
text-align: center;
color: #222;
}
& > div {
text-align: center;
}
a {
display: block;
text-align: center;
color: #222;
margin-bottom: 10px;
}
p {
max-width: 500px;
margin: 10px auto;
& > a {
display: inline;
}
}
form {
max-width: 500px;
margin: 10px auto;
border: 1px solid #ccc;
padding: 20px;
box-shadow: 2px 2px 5px rgba(0, 0, 0, 0.3);
border-radius: 3px;
position: relative;
.loading {
text-align: center;
display: block;
position: absolute;
background: url("https://media.giphy.com/media/130AxGoOaR6t0I/giphy.gif")
center center;
background-size: fill;
font-size: 2em;
top: 0;
left: 0;
right: 0;
bottom: 0;
padding: 50px 0 0 0;
z-index: 2;
}
& > div {
display: flex;
flex-flow: row nowrap;
line-height: 2em;
margin: 5px;
position: relative;
& > label {
color: #333;
width: 110px;
min-width: 60px;
font-size: 1em;
line-height: 32px;
}
& > input,
& > select,
& > textarea {
flex: 1;
padding: 3px 5px;
font-size: 1em;
margin-left: 15px;
border: 1px solid #ccc;
border-radius: 3px;
}
& > input[type="checkbox"] {
margin-top: 7px;
}
& > div {
margin-left: 16px;
& > label {
display: block;
& > input {
margin-right: 3px;
}
}
}
& > span {
line-height: 32px;
margin-left: 10px;
color: #800;
font-weight: bold;
}
& > button.remove {
${btnDanger};
}
}
& > .buttons {
display: flex;
flex-flow: row nowrap;
justify-content: center;
margin-top: 15px;
}
button {
margin: 0 10px;
&[type="submit"] {
${btnPrimary};
}
&[type="button"] {
${btnDefault};
}
}
.error {
display: flex;
font-weight: bold;
color: #800;
flex-flow: row nowrap;
justify-content: center;
}
pre {
position: relative;
border: 1px solid #ccc;
background: rgba(0, 0, 0, 0.1);
box-shadow: inset 1px 1px 3px rgba(0, 0, 0, 0.2);
padding: 20px;
}
}
`;
================================================
FILE: examples/independent-error-component-render-props/index.js
================================================
import React from "react";
import { render } from "react-dom";
import Styles from "./Styles";
import { Form, Field } from "react-final-form";
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
const onSubmit = async (values) => {
await sleep(300);
window.alert(JSON.stringify(values, 0, 2));
};
const Error = ({ name }) => (
touched && error ? {error} : null
}
/>
);
const App = () => (
🏁
{" "}
React Final Form Example
Reusable Independent Error Component
Read Docs
{
const errors = {};
if (!values.firstName) {
errors.firstName = "Required";
}
if (!values.lastName) {
errors.lastName = "Required";
}
if (!values.age) {
errors.age = "Required";
} else if (isNaN(values.age)) {
errors.age = "Must be a number";
} else if (values.age < 18) {
errors.age = "No kids allowed";
}
return errors;
}}
render={({ handleSubmit, reset, submitting, pristine, values }) => (
First Name
Last Name
Age
Submit
Reset
{JSON.stringify(values, 0, 2)}
)}
/>
);
render( , document.getElementById("root"));
================================================
FILE: examples/independent-error-component-render-props/package.json
================================================
{
"name": "react-final-form-independent-error-component",
"version": "1.0.0",
"description": "This example demonstrates how to make a special Error component to display the error for any form field.",
"keywords": [],
"main": "index.js",
"dependencies": {
"final-form": "4.20.4",
"react": "17.0.2",
"react-dom": "17.0.2",
"react-final-form": "6.5.3",
"styled-components": "4.2.1"
}
}
================================================
FILE: examples/independent-error-component-render-props/readme.md
================================================
# Reusable Independent Error Component (using render props)
[](https://codesandbox.io/s/xoo3xq654p)
================================================
FILE: examples/independent-error-component-with-hooks/Styles.js
================================================
import styled, { css } from "styled-components";
const btn = (light, dark) => css`
white-space: nowrap;
display: inline-block;
border-radius: 5px;
padding: 5px 15px;
font-size: 16px;
color: white;
&:visited {
color: white;
}
background-image: linear-gradient(${light}, ${dark});
border: 1px solid ${dark};
&:hover {
background-image: linear-gradient(${light}, ${dark});
&[disabled] {
background-image: linear-gradient(${light}, ${dark});
}
}
&:visited {
color: black;
}
&[disabled] {
opacity: 0.6;
cursor: not-allowed;
}
`;
const btnDefault = css`
${btn("#ffffff", "#d5d5d5")} color: #555;
`;
const btnPrimary = btn("#4f93ce", "#285f8f");
const btnDanger = btn("#e27c79", "#c9302c");
export default styled.div`
font-family: sans-serif;
h1 {
text-align: center;
color: #222;
}
h2 {
text-align: center;
color: #222;
}
& > div {
text-align: center;
}
a {
display: block;
text-align: center;
color: #222;
margin-bottom: 10px;
}
p {
max-width: 500px;
margin: 10px auto;
& > a {
display: inline;
}
}
form {
max-width: 500px;
margin: 10px auto;
border: 1px solid #ccc;
padding: 20px;
box-shadow: 2px 2px 5px rgba(0, 0, 0, 0.3);
border-radius: 3px;
position: relative;
.loading {
text-align: center;
display: block;
position: absolute;
background: url("https://media.giphy.com/media/130AxGoOaR6t0I/giphy.gif")
center center;
background-size: fill;
font-size: 2em;
top: 0;
left: 0;
right: 0;
bottom: 0;
padding: 50px 0 0 0;
z-index: 2;
}
& > div {
display: flex;
flex-flow: row nowrap;
line-height: 2em;
margin: 5px;
position: relative;
& > label {
color: #333;
width: 110px;
min-width: 60px;
font-size: 1em;
line-height: 32px;
}
& > input,
& > select,
& > textarea {
flex: 1;
padding: 3px 5px;
font-size: 1em;
margin-left: 15px;
border: 1px solid #ccc;
border-radius: 3px;
}
& > input[type="checkbox"] {
margin-top: 7px;
}
& > div {
margin-left: 16px;
& > label {
display: block;
& > input {
margin-right: 3px;
}
}
}
& > span {
line-height: 32px;
margin-left: 10px;
color: #800;
font-weight: bold;
}
& > button.remove {
${btnDanger};
}
}
& > .buttons {
display: flex;
flex-flow: row nowrap;
justify-content: center;
margin-top: 15px;
}
button {
margin: 0 10px;
&[type="submit"] {
${btnPrimary};
}
&[type="button"] {
${btnDefault};
}
}
.error {
display: flex;
font-weight: bold;
color: #800;
flex-flow: row nowrap;
justify-content: center;
}
pre {
position: relative;
border: 1px solid #ccc;
background: rgba(0, 0, 0, 0.1);
box-shadow: inset 1px 1px 3px rgba(0, 0, 0, 0.2);
padding: 20px;
}
}
`;
================================================
FILE: examples/independent-error-component-with-hooks/index.js
================================================
import React from "react";
import { render } from "react-dom";
import Styles from "./Styles";
import { Form, Field, useField } from "react-final-form";
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
const onSubmit = async (values) => {
await sleep(300);
window.alert(JSON.stringify(values, 0, 2));
};
const Error = ({ name }) => {
const {
meta: { touched, error },
} = useField(name, { subscription: { touched: true, error: true } });
return touched && error ? {error} : null;
};
const App = () => (
🏁
{" "}
React Final Form Example
Reusable Independent Error Component
Read Docs
{
const errors = {};
if (!values.firstName) {
errors.firstName = "Required";
}
if (!values.lastName) {
errors.lastName = "Required";
}
if (!values.age) {
errors.age = "Required";
} else if (isNaN(values.age)) {
errors.age = "Must be a number";
} else if (values.age < 18) {
errors.age = "No kids allowed";
}
return errors;
}}
render={({ handleSubmit, reset, submitting, pristine, values }) => (
First Name
Last Name
Age
Submit
Reset
{JSON.stringify(values, 0, 2)}
)}
/>
);
render( , document.getElementById("root"));
================================================
FILE: examples/independent-error-component-with-hooks/package.json
================================================
{
"name": "react-final-form-independent-error-component-with-hooks",
"version": "1.0.0",
"description": "This example demonstrates how to make a special Error component to display the error for any form field.",
"keywords": [],
"main": "index.js",
"dependencies": {
"final-form": "4.20.4",
"react": "17.0.2",
"react-dom": "17.0.2",
"react-final-form": "6.5.3",
"styled-components": "4.2.1"
}
}
================================================
FILE: examples/independent-error-component-with-hooks/readme.md
================================================
# Reusable Independent Error Component (using hooks)
[](https://codesandbox.io/s/react-final-form-independent-error-component-with-hooks-y1grn)
================================================
FILE: examples/listening-for-external-changes/BooleanDecay.js
================================================
import React from "react";
import PropTypes from "prop-types";
/**
* 👋 Hey! Thanks for being curious about what this component does!
* It's pretty simple. Have you ever seen those boxes where, when you
* flip the switch, it activates a motor that causes a little hand to
* come out and flip the switch back off? THAT'S what this component does.
*
* Examples: https://giphy.com/search/useless-box
*
* The value prop will be flipped back to false after the delay.
*/
export default class BooleanDecay extends React.Component {
static propTypes = {
children: PropTypes.func.isRequired,
delay: PropTypes.number.isRequired,
value: PropTypes.bool.isRequired,
};
constructor(props) {
super(props);
this.state = {
value: props.value,
};
}
startTimer() {
this.stopTimer();
this.timeout = setTimeout(() => {
this.setState({ value: false });
}, this.props.delay);
}
stopTimer() {
if (this.timeout) {
clearTimeout(this.timeout);
}
}
componentDidMount() {
if (this.state.value) {
this.startTimer();
}
}
componentWillUnmount() {
this.stopTimer();
}
componentWillReceiveProps(nextProps) {
const { value } = nextProps;
if (value !== this.state.value) {
this.setState({ value });
if (value) {
this.startTimer();
} else {
this.stopTimer();
}
}
}
render() {
return this.props.children(this.state.value);
}
}
================================================
FILE: examples/listening-for-external-changes/ExternalModificationDetector.js
================================================
import React from "react";
import { Field } from "react-final-form";
/**
* Listens for changes to a field's value, and, if the value changes
* when the field is NOT active, it sets an externallyModified flag
* which is passed to the children render function.
*/
class ExternalModificationDetector extends React.Component {
constructor(props) {
super(props);
this.state = {
externallyModified: false,
value: props.input.value,
};
}
componentWillReceiveProps(nextProps) {
const {
input: { value },
meta: { active },
} = nextProps;
if (value !== this.state.value) {
this.setState({
value,
externallyModified: !active,
});
} else if (this.state.externallyModified) {
this.setState({
externallyModified: false,
});
}
}
render() {
const { children } = this.props;
const { externallyModified } = this.state;
return children(externallyModified);
}
}
// Wrap the ExternalModificationDetector in a Field
export default ({ name, children }) => (
(
{children}
)}
/>
);
================================================
FILE: examples/listening-for-external-changes/Styles.js
================================================
import styled, { css } from "styled-components";
const btn = (light, dark) => css`
white-space: nowrap;
display: inline-block;
border-radius: 5px;
padding: 5px 15px;
font-size: 16px;
color: white;
&:visited {
color: white;
}
background-image: linear-gradient(${light}, ${dark});
border: 1px solid ${dark};
&:hover {
background-image: linear-gradient(${light}, ${dark});
&[disabled] {
background-image: linear-gradient(${light}, ${dark});
}
}
&:visited {
color: black;
}
&[disabled] {
opacity: 0.6;
cursor: not-allowed;
}
`;
const btnDefault = css`
${btn("#ffffff", "#d5d5d5")} color: #555;
`;
const btnPrimary = btn("#4f93ce", "#285f8f");
const btnDanger = btn("#e27c79", "#c9302c");
export default styled.div`
font-family: sans-serif;
h1 {
text-align: center;
color: #222;
}
h2 {
text-align: center;
color: #222;
}
& > div {
text-align: center;
}
a {
display: block;
text-align: center;
color: #222;
margin-bottom: 10px;
}
p {
max-width: 500px;
margin: 10px auto;
& > a {
display: inline;
}
}
.loading {
font-size: 2em;
font-weight: bold;
text-align: center;
margin: 50px;
}
form,
div.form {
text-align: left;
max-width: 500px;
margin: 10px auto;
border: 1px solid #ccc;
padding: 20px;
box-shadow: 2px 2px 5px rgba(0, 0, 0, 0.3);
border-radius: 3px;
position: relative;
& > div {
display: flex;
flex-flow: row nowrap;
line-height: 2em;
margin: 5px;
position: relative;
& > label {
color: #333;
width: 110px;
min-width: 60px;
font-size: 1em;
line-height: 32px;
}
& > input,
& > .downshift > input,
& > select,
& > textarea {
flex: 1;
padding: 3px 5px;
font-size: 1em;
margin-left: 15px;
border: 1px solid #ccc;
border-radius: 3px;
}
& > input[type="checkbox"] {
margin-top: 7px;
}
& > div {
margin-left: 16px;
& > label {
margin-left: 0;
display: block;
& > input {
margin-right: 3px;
}
}
&.downshift {
margin-left: 0;
padding-left: 15px;
flex: 1;
& > input {
width: 100%;
padding: 6px 5px;
font-size: 1em;
margin-left: 0;
border: 1px solid #ccc;
border-radius: 3px;
}
}
}
& > span {
line-height: 32px;
margin-left: 10px;
color: #800;
font-weight: bold;
}
& > button.remove {
${btnDanger};
}
}
& > .buttons {
display: flex;
flex-flow: row nowrap;
justify-content: center;
margin-top: 15px;
}
.error {
display: flex;
font-weight: bold;
color: #800;
flex-flow: row nowrap;
justify-content: center;
}
pre {
position: relative;
border: 1px solid #ccc;
background: rgba(0, 0, 0, 0.1);
box-shadow: inset 1px 1px 3px rgba(0, 0, 0, 0.2);
padding: 20px;
}
.submitting {
display: block;
position: absolute;
top: -5px;
left: -5px;
right: -5px;
padding: 0;
text-align: center;
background: rgba(0, 0, 0, 0.4);
color: white;
z-index: 10;
font-weight: bold;
font-size: 0.8em;
}
.saving {
font-size: 0.8em;
font-weight: bold;
color: darkblue;
margin: 8px 0 0 7px;
}
}
button {
margin: 0 10px;
&[type="submit"] {
${btnPrimary};
}
&[type="button"] {
${btnDefault};
}
}
.downshift-options {
border: 1px solid #ddd;
box-shadow: 1px 1px 2px rgba(0, 0, 0, 0.3);
& > div {
padding: 3px 5px;
}
}
`;
================================================
FILE: examples/listening-for-external-changes/index.js
================================================
import React from "react";
import { render } from "react-dom";
import Styles from "./Styles";
import { Form, Field } from "react-final-form";
import createDecorator from "final-form-calculate";
import ExternalModificationDetector from "./ExternalModificationDetector";
import BooleanDecay from "./BooleanDecay";
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
const onSubmit = async (values) => {
await sleep(300);
window.alert(JSON.stringify(values, 0, 2));
};
const calculator = createDecorator({
field: /day\[\d\]/, // when a field matching this pattern changes...
updates: {
// ...update the total to the result of this function
total: (ignoredValue, allValues) =>
(allValues.day || []).reduce((sum, value) => sum + Number(value || 0), 0),
},
});
const App = () => (
🏁
{" "}
React Final Form Example
Listening for External Changes
Read Docs
By wrapping a stateful ExternalModificationDetector component
in a Field component, we can listen for changes to a field's
value, and by knowing whether or not the field is active, deduce when a
field's value changes due to external influences.
As you enter numbers for each day of the week, the total is calulated in
realtime.
(
Monday
Tuesday
Wednesday
Thursday
Friday
Total
{(externallyModified) => (
{(highlight) => (
)}
)}
Submit
Reset
{JSON.stringify(values, 0, 2)}
)}
/>
);
render( , document.getElementById("root"));
================================================
FILE: examples/listening-for-external-changes/package.json
================================================
{
"name": "react-final-form-listening-for-external-changes",
"version": "1.0.0",
"description": "Demonstrates how to use a stateful component wrapped in a Field to listen for external changes to that field.",
"keywords": [
"react",
"final-form",
"react-final-form",
"form",
"listener"
],
"homepage": "https://codesandbox.io/s/new",
"main": "src/index.js",
"dependencies": {
"final-form": "4.20.4",
"final-form-calculate": "1.3.1",
"react": "17.0.2",
"react-dom": "17.0.2",
"react-final-form": "6.5.3",
"react-scripts": "3.0.1",
"styled-components": "4.2.0"
},
"devDependencies": {},
"scripts": {
"start": "react-scripts start",
"build": "react-scripts build",
"test": "react-scripts test --env=jsdom",
"eject": "react-scripts eject"
}
}
================================================
FILE: examples/listening-for-external-changes/readme.md
================================================
# Listening for External Changes
[](https://codesandbox.io/s/3x989zl866)
================================================
FILE: examples/loading-initializing-values/Styles.js
================================================
import styled, { css } from "styled-components";
const btn = (light, dark) => css`
white-space: nowrap;
display: inline-block;
border-radius: 5px;
padding: 5px 15px;
font-size: 16px;
color: white;
&:visited {
color: white;
}
background-image: linear-gradient(${light}, ${dark});
border: 1px solid ${dark};
&:hover {
background-image: linear-gradient(${light}, ${dark});
&[disabled] {
background-image: linear-gradient(${light}, ${dark});
}
}
&:visited {
color: black;
}
&[disabled] {
opacity: 0.6;
cursor: not-allowed;
}
`;
const btnDefault = css`
${btn("#ffffff", "#d5d5d5")} color: #555;
`;
const btnPrimary = btn("#4f93ce", "#285f8f");
const btnDanger = btn("#e27c79", "#c9302c");
export default styled.div`
font-family: sans-serif;
h1 {
text-align: center;
color: #222;
}
h2 {
text-align: center;
color: #222;
}
& > div {
text-align: center;
}
a {
display: block;
text-align: center;
color: #222;
margin-bottom: 10px;
}
p {
max-width: 500px;
margin: 10px auto;
& > a {
display: inline;
}
}
form {
max-width: 500px;
margin: 10px auto;
border: 1px solid #ccc;
padding: 20px;
box-shadow: 2px 2px 5px rgba(0, 0, 0, 0.3);
border-radius: 3px;
position: relative;
.loading {
text-align: center;
display: block;
position: absolute;
background: url("https://media.giphy.com/media/130AxGoOaR6t0I/giphy.gif")
center center;
background-size: fill;
font-size: 2em;
top: 0;
left: 0;
right: 0;
bottom: 0;
padding: 50px 0 0 0;
z-index: 2;
}
& > div {
display: flex;
flex-flow: row nowrap;
line-height: 2em;
margin: 5px;
position: relative;
& > label {
color: #333;
width: 110px;
min-width: 60px;
font-size: 1em;
line-height: 32px;
}
& > input,
& > select,
& > textarea {
flex: 1;
padding: 3px 5px;
font-size: 1em;
margin-left: 15px;
border: 1px solid #ccc;
border-radius: 3px;
}
& > input[type="checkbox"] {
margin-top: 7px;
}
& > div {
margin-left: 16px;
& > label {
display: block;
& > input {
margin-right: 3px;
}
}
}
& > span {
line-height: 32px;
margin-left: 10px;
color: #800;
font-weight: bold;
}
& > button.remove {
${btnDanger};
}
}
& > .buttons {
display: flex;
flex-flow: row nowrap;
justify-content: center;
margin-top: 15px;
}
button {
margin: 0 10px;
&[type="submit"] {
${btnPrimary};
}
&[type="button"] {
${btnDefault};
}
}
.error {
display: flex;
font-weight: bold;
color: #800;
flex-flow: row nowrap;
justify-content: center;
}
pre {
position: relative;
border: 1px solid #ccc;
background: rgba(0, 0, 0, 0.1);
box-shadow: inset 1px 1px 3px rgba(0, 0, 0, 0.2);
padding: 20px;
}
}
`;
================================================
FILE: examples/loading-initializing-values/index.js
================================================
import React from "react";
import { render } from "react-dom";
import Styles from "./Styles";
import { Form, Field } from "react-final-form";
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
const load = async () => {
await sleep(2000);
return {
username: "erikras",
firstName: "Erik",
};
};
const onSubmit = async (values) => {
await sleep(300);
window.alert(JSON.stringify(values, 0, 2));
};
class App extends React.Component {
state = { data: {} };
async componentDidMount() {
this.setState({ loading: true });
const data = await load();
this.setState({ loading: false, data });
}
render() {
return (
🏁
{" "}
React Final Form - Loading and Initializing Values
Read Docs
{
return (
{this.state.loading &&
}
Username
First Name
Submit
Reset
{JSON.stringify(values, 0, 2)}
);
}}
/>
);
}
}
render( , document.getElementById("root"));
================================================
FILE: examples/loading-initializing-values/package.json
================================================
{
"name": "react-final-form-loading-and-initializing",
"version": "1.0.0",
"description": "This example demonstrates how you can initialize a form by passing in initialValues as a prop.",
"keywords": [],
"main": "index.js",
"dependencies": {
"final-form": "4.20.4",
"react": "17.0.2",
"react-dom": "17.0.2",
"react-final-form": "6.5.3",
"styled-components": "4.2.0"
}
}
================================================
FILE: examples/loading-initializing-values/readme.md
================================================
# Loading and Initializing Values
[](https://codesandbox.io/s/91w9ro3x9o)
================================================
FILE: examples/loading-saving-reinitializing/LoadSaveReinitializeForm.js
================================================
import React from "react";
import PropTypes from "prop-types";
import { Form } from "react-final-form";
export default class LoadSaveReinitializeForm extends React.Component {
static propTypes = {
load: PropTypes.func.isRequired,
loading: PropTypes.node.isRequired,
postLoadFormat: PropTypes.func,
preSaveFormat: PropTypes.func,
save: PropTypes.func.isRequired,
};
state = {
isLoading: false,
originalValues: undefined,
initialValues: undefined,
};
load = async () => {
const { load, postLoadFormat } = this.props;
this.setState({ isLoading: true });
const originalValues = await load();
const initialValues = postLoadFormat
? postLoadFormat(originalValues)
: originalValues;
this.setState({
isLoading: false,
originalValues,
initialValues,
});
};
save = async (values) => {
const { postLoadFormat, preSaveFormat, save } = this.props;
let valuesToSave = preSaveFormat
? preSaveFormat(values, this.state.originalValues)
: values;
const result = await save(valuesToSave);
this.setState({
originalValues: valuesToSave,
initialValues: postLoadFormat
? postLoadFormat(valuesToSave)
: valuesToSave,
});
return result;
};
componentDidMount() {
this.load();
}
render() {
const { load, loading, postLoadFormat, preSaveFormat, save, ...rest } =
this.props;
const { isLoading, initialValues } = this.state;
return isLoading || !initialValues ? (
loading
) : (
);
}
}
================================================
FILE: examples/loading-saving-reinitializing/Styles.js
================================================
import styled, { css } from "styled-components";
const btn = (light, dark) => css`
white-space: nowrap;
display: inline-block;
border-radius: 5px;
padding: 5px 15px;
font-size: 16px;
color: white;
&:visited {
color: white;
}
background-image: linear-gradient(${light}, ${dark});
border: 1px solid ${dark};
&:hover {
background-image: linear-gradient(${light}, ${dark});
&[disabled] {
background-image: linear-gradient(${light}, ${dark});
}
}
&:visited {
color: black;
}
&[disabled] {
opacity: 0.6;
cursor: not-allowed;
}
`;
const btnDefault = css`
${btn("#ffffff", "#d5d5d5")} color: #555;
`;
const btnPrimary = btn("#4f93ce", "#285f8f");
const btnDanger = btn("#e27c79", "#c9302c");
export default styled.div`
font-family: sans-serif;
h1 {
text-align: center;
color: #222;
}
h2 {
text-align: center;
color: #222;
}
& > div {
text-align: center;
}
a {
display: block;
text-align: center;
color: #222;
margin-bottom: 10px;
}
p {
max-width: 500px;
margin: 10px auto;
& > a {
display: inline;
}
}
.loading {
font-size: 2em;
font-weight: bold;
text-align: center;
margin: 50px;
}
form,
div.form {
text-align: left;
max-width: 500px;
margin: 10px auto;
border: 1px solid #ccc;
padding: 20px;
box-shadow: 2px 2px 5px rgba(0, 0, 0, 0.3);
border-radius: 3px;
position: relative;
& > div {
display: flex;
flex-flow: row nowrap;
line-height: 2em;
margin: 5px;
position: relative;
& > label {
color: #333;
width: 110px;
min-width: 60px;
font-size: 1em;
line-height: 32px;
}
& > input,
& > select,
& > textarea {
flex: 1;
padding: 3px 5px;
font-size: 1em;
margin-left: 15px;
border: 1px solid #ccc;
border-radius: 3px;
}
& > input[type="checkbox"] {
margin-top: 7px;
}
& > div {
margin-left: 16px;
& > label {
display: block;
& > input {
margin-right: 3px;
}
}
}
& > span {
line-height: 32px;
margin-left: 10px;
color: #800;
font-weight: bold;
}
& > button.remove {
${btnDanger};
}
}
& > .buttons {
display: flex;
flex-flow: row nowrap;
justify-content: center;
margin-top: 15px;
}
.error {
display: flex;
font-weight: bold;
color: #800;
flex-flow: row nowrap;
justify-content: center;
}
pre {
position: relative;
border: 1px solid #ccc;
background: rgba(0, 0, 0, 0.1);
box-shadow: inset 1px 1px 3px rgba(0, 0, 0, 0.2);
padding: 20px;
}
.submitting {
display: block;
position: absolute;
top: -5px;
left: -5px;
right: -5px;
padding: 0;
text-align: center;
background: rgba(0, 0, 0, 0.4);
color: white;
z-index: 10;
font-weight: bold;
font-size: 0.8em;
}
.saving {
font-size: 0.8em;
font-weight: bold;
color: darkblue;
margin: 8px 0 0 7px;
}
}
button {
margin: 0 10px;
&[type="submit"] {
${btnPrimary};
}
&[type="button"] {
${btnDefault};
}
}
`;
================================================
FILE: examples/loading-saving-reinitializing/index.js
================================================
import React from "react";
import { render } from "react-dom";
import Styles from "./Styles";
import { Field } from "react-final-form";
import LoadSaveReinitializeForm from "./LoadSaveReinitializeForm";
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
let record = {
primaryKey: 42,
phone: "2045551234",
name: "John Doe",
email: "john.doe@final-form.org",
otherExtraneousInfo: {
creditScore: 800,
},
};
const load = async () => {
console.info("Loading...");
await sleep(1500);
console.info("Loaded...");
return record;
};
const save = async (values) => {
console.info("Saving", values);
await sleep(1500);
record = values;
};
const postLoadFormat = (values) => {
const { name, email, phone } = values;
const [firstName, lastName] = name.split(/ /, 2);
return {
firstName,
lastName,
email,
phone: phone
? `${phone.slice(0, 3)}-${phone.slice(3, 6)}-${phone.slice(6, 10)}`
: "",
};
};
const preSaveFormat = (values, originalValues) => {
return {
...originalValues,
name: `${values.firstName || ""} ${values.lastName || ""}`,
email: values.email,
phone: values.phone.replace(/-/g, ""),
};
};
const Error = ({ name }) => (
touched && error ? {error} : null
}
/>
);
const loading = Loading...
;
const validate = (values) => {
const errors = {};
if (!values.firstName) {
errors.firstName = "Required";
}
if (!values.lastName) {
errors.lastName = "Required";
}
if (!values.email) {
errors.email = "Required";
}
if (!values.phone) {
errors.phone = "Required";
}
return errors;
};
const App = () => (
🏁
{" "}
React Final Form
Load, Save, and Reinitialize
Read Docs
LoadSaveReinitializeForm is a wrapper for a{" "}
🏁
{" "}
React Final Form component. On mount, it loads a record from the
database/API, and formats it to the shape of the form inputs. On submit,
it converts the data back to the format that the database/API wants
(including the original primary key), saves the data to the database/API,
and then re-initializes the form so that it is pristine again with the
canonical data.
{({ handleSubmit, form, submitting, pristine, values }) => (
First Name
Last Name
Email
Phone
Submit
Reset
Form Values
{JSON.stringify(values, 0, 2)}
Database Record
{JSON.stringify(record, 0, 2)}
)}
);
render( , document.getElementById("root"));
================================================
FILE: examples/loading-saving-reinitializing/package.json
================================================
{
"name": "react-final-form-example-load-save-and-reinitialize",
"version": "1.0.0",
"description": "Demonstrates how to make a wrapper component to handle, loading, normalization of data, saving, and reinitializing of the form.",
"keywords": [
"react-final-form",
"final-form",
"loading",
"initialization",
"asynchronous"
],
"main": "index.js",
"dependencies": {
"final-form": "4.20.4",
"react": "17.0.2",
"react-dom": "17.0.2",
"react-final-form": "6.5.3",
"react-fontawesome": "1.6.1",
"styled-components": "4.2.0"
}
}
================================================
FILE: examples/loading-saving-reinitializing/readme.md
================================================
# Loading, Normalizing, Saving, and Reinitializing
[](https://codesandbox.io/s/xr0mvl1904)
================================================
FILE: examples/material-ui/.prettierrc
================================================
{
"printWidth": 80,
"tabWidth": 2,
"useTabs": false,
"semi": true,
"singleQuote": true,
"trailingComma": "all",
"bracketSpacing": true,
"jsxBracketSameLine": false,
"fluid": false
}
================================================
FILE: examples/material-ui/index.js
================================================
import React from 'react';
import ReactDOM from 'react-dom';
import { Form } from 'react-final-form';
import {
TextField,
Checkboxes,
Radios,
Select,
DatePicker,
TimePicker,
} from 'mui-rff';
import {
Typography,
Paper,
Link,
Grid,
Button,
CssBaseline,
MenuItem,
} from '@material-ui/core';
// Picker
import DateFnsUtils from '@date-io/date-fns';
const onSubmit = async (values) => {
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
await sleep(300);
window.alert(JSON.stringify(values, 0, 2));
};
const validate = (values) => {
const errors = {};
if (!values.firstName) {
errors.firstName = 'Required';
}
if (!values.lastName) {
errors.lastName = 'Required';
}
if (!values.email) {
errors.email = 'Required';
}
return errors;
};
const formFields = [
{
size: 6,
field: (
),
},
{
size: 6,
field: (
),
},
{
size: 12,
field: (
),
},
{
size: 12,
field: (
),
},
{
size: 12,
field: (
),
},
{
size: 12,
field: (
),
},
{
size: 12,
field: ,
},
{
size: 12,
field: (
London
Paris
A city with a very long Name
),
},
{
size: 6,
field: (
),
},
{
size: 6,
field: (
),
},
];
function App() {
return (
🏁
{' '}
React Final Form
Material-UI Example
Read Docs
. This example demonstrates using{' '}
Material-UI
{' '}
form controls.
(
{formFields.map((item, idx) => (
{item.field}
))}
Reset
Submit
{JSON.stringify(values, 0, 2)}
)}
/>
);
}
ReactDOM.render( , document.querySelector('#root'));
================================================
FILE: examples/material-ui/package.json
================================================
{
"name": "react-final-form-material-ui-example",
"version": "1.0.0",
"description": "Demonstrates how to use Material-UI form controls.",
"keywords": [
"material-ui"
],
"homepage": "https://codesandbox.io/s/new",
"main": "src/index.js",
"dependencies": {
"@date-io/date-fns": "latest",
"@material-ui/core": "latest",
"@material-ui/lab": "latest",
"@material-ui/pickers": "latest",
"date-fns": "next",
"final-form": "4.20.4",
"mui-rff": "latest",
"react": "latest",
"react-dom": "latest",
"react-final-form": "6.5.3",
"react-scripts": "latest",
"tslib": "latest"
},
"devDependencies": {},
"scripts": {
"start": "react-scripts start",
"build": "react-scripts build",
"test": "react-scripts test --env=jsdom",
"eject": "react-scripts eject"
}
}
================================================
FILE: examples/material-ui/readme.md
================================================
# Material-UI 3.0
[](https://codesandbox.io/s/9ywq085k9w)
================================================
FILE: examples/parse-format/Styles.js
================================================
import styled, { css } from "styled-components";
const btn = (light, dark) => css`
white-space: nowrap;
display: inline-block;
border-radius: 5px;
padding: 5px 15px;
font-size: 16px;
color: white;
&:visited {
color: white;
}
background-image: linear-gradient(${light}, ${dark});
border: 1px solid ${dark};
&:hover {
background-image: linear-gradient(${light}, ${dark});
&[disabled] {
background-image: linear-gradient(${light}, ${dark});
}
}
&:visited {
color: black;
}
&[disabled] {
opacity: 0.6;
cursor: not-allowed;
}
`;
const btnDefault = css`
${btn("#ffffff", "#d5d5d5")} color: #555;
`;
const btnPrimary = btn("#4f93ce", "#285f8f");
const btnDanger = btn("#e27c79", "#c9302c");
export default styled.div`
font-family: sans-serif;
h1 {
text-align: center;
color: #222;
}
h2 {
text-align: center;
color: #222;
}
& > div {
text-align: center;
}
a {
display: block;
text-align: center;
color: #222;
margin-bottom: 10px;
}
p {
max-width: 500px;
margin: 10px auto;
& > a {
display: inline;
}
}
form {
max-width: 500px;
margin: 10px auto;
border: 1px solid #ccc;
padding: 20px;
box-shadow: 2px 2px 5px rgba(0, 0, 0, 0.3);
border-radius: 3px;
position: relative;
.loading {
text-align: center;
display: block;
position: absolute;
background: url("https://media.giphy.com/media/130AxGoOaR6t0I/giphy.gif")
center center;
background-size: fill;
font-size: 2em;
top: 0;
left: 0;
right: 0;
bottom: 0;
padding: 50px 0 0 0;
z-index: 2;
}
& > div {
display: flex;
flex-flow: row nowrap;
line-height: 2em;
margin: 5px;
position: relative;
& > label {
color: #333;
width: 110px;
min-width: 60px;
font-size: 1em;
line-height: 32px;
}
& > input,
& > select,
& > textarea {
flex: 1;
padding: 3px 5px;
font-size: 1em;
margin-left: 15px;
border: 1px solid #ccc;
border-radius: 3px;
}
& > input[type="checkbox"] {
margin-top: 7px;
}
& > div {
margin-left: 16px;
& > label {
display: block;
& > input {
margin-right: 3px;
}
}
}
& > span {
line-height: 32px;
margin-left: 10px;
color: #800;
font-weight: bold;
}
& > button.remove {
${btnDanger};
}
}
& > .buttons {
display: flex;
flex-flow: row nowrap;
justify-content: center;
margin-top: 15px;
}
.error {
display: flex;
font-weight: bold;
color: #800;
flex-flow: row nowrap;
justify-content: center;
}
pre {
position: relative;
border: 1px solid #ccc;
background: rgba(0, 0, 0, 0.1);
box-shadow: inset 1px 1px 3px rgba(0, 0, 0, 0.2);
padding: 20px;
}
}
button {
margin: 0 10px;
&[type="submit"] {
${btnPrimary};
}
&[type="button"] {
${btnDefault};
}
}
`;
================================================
FILE: examples/parse-format/index.js
================================================
import React from "react";
import { render } from "react-dom";
import Styles from "./Styles";
import { Form, Field } from "react-final-form";
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
const onSubmit = async (values) => {
await sleep(300);
window.alert(JSON.stringify(values, 0, 2));
};
const normalizePhone = (value) => {
if (!value) return value;
const onlyNums = value.replace(/[^\d]/g, "");
if (onlyNums.length <= 3) return onlyNums;
if (onlyNums.length <= 7)
return `(${onlyNums.slice(0, 3)}) ${onlyNums.slice(3, 7)}`;
return `(${onlyNums.slice(0, 3)}) ${onlyNums.slice(3, 6)}-${onlyNums.slice(
6,
10,
)}`;
};
const App = () => (
🏁
{" "}
React Final Form
Parse and Format
Read Docs
(
Username
value && value.toUpperCase()}
format={(value) => (value ? value.toLowerCase() : "")}
/>
Phone
Submit
Reset
{JSON.stringify(values, 0, 2)}
)}
/>
);
render( , document.getElementById("root"));
================================================
FILE: examples/parse-format/package.json
================================================
{
"name": "react-final-form-parse-and-format",
"version": "1.0.0",
"description": "Demonstrates how to use parse and format props to control how values are shown in the input and saved into the form state.",
"keywords": [
"parse",
"react",
"react-final-form",
"format",
"final-form"
],
"main": "index.js",
"dependencies": {
"final-form": "4.20.4",
"react": "17.0.2",
"react-dom": "17.0.2",
"react-final-form": "6.5.3",
"styled-components": "latest"
}
}
================================================
FILE: examples/parse-format/readme.md
================================================
# Parse and Format (and Normalize)
[](https://codesandbox.io/s/10rzowm323)
================================================
FILE: examples/prefixed-fields/Styles.js
================================================
import styled, { css } from "styled-components";
const btn = (light, dark) => css`
white-space: nowrap;
display: inline-block;
border-radius: 5px;
padding: 5px 15px;
font-size: 16px;
color: white;
&:visited {
color: white;
}
background-image: linear-gradient(${light}, ${dark});
border: 1px solid ${dark};
&:hover {
background-image: linear-gradient(${light}, ${dark});
&[disabled] {
background-image: linear-gradient(${light}, ${dark});
}
}
&:visited {
color: black;
}
&[disabled] {
opacity: 0.6;
cursor: not-allowed;
}
`;
const btnDefault = css`
${btn("#ffffff", "#d5d5d5")} color: #555;
`;
const btnPrimary = btn("#4f93ce", "#285f8f");
export default styled.div`
font-family: sans-serif;
h1,
h2,
h3 {
text-align: center;
color: #222;
}
& > div {
text-align: center;
}
a {
display: block;
text-align: center;
color: #222;
}
p a {
display: inline;
}
form {
max-width: 500px;
margin: 10px auto;
border: 1px solid #ccc;
padding: 20px;
box-shadow: 2px 2px 5px rgba(0, 0, 0, 0.3);
border-radius: 3px;
& > div {
display: flex;
flex-flow: row nowrap;
line-height: 2em;
margin: 5px;
& > label {
color: #333;
width: 110px;
font-size: 1em;
line-height: 32px;
}
& > input,
& > select,
& > textarea {
flex: 1;
padding: 3px 5px;
font-size: 1em;
margin-left: 15px;
border: 1px solid #ccc;
border-radius: 3px;
}
& > input[type="checkbox"] {
margin-top: 7px;
}
& > div {
margin-left: 16px;
& > label {
display: block;
& > input {
margin-right: 3px;
}
}
}
}
& > .buttons {
display: flex;
flex-flow: row nowrap;
justify-content: center;
margin-top: 15px;
}
button {
margin: 0 10px;
&[type="submit"] {
${btnPrimary};
}
&[type="button"] {
${btnDefault};
}
}
pre {
border: 1px solid #ccc;
background: rgba(0, 0, 0, 0.1);
box-shadow: inset 1px 1px 3px rgba(0, 0, 0, 0.2);
padding: 20px;
}
}
`;
================================================
FILE: examples/prefixed-fields/index.js
================================================
import React from "react";
import { render } from "react-dom";
import Styles from "./Styles";
import { Form, Field } from "react-final-form";
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
const onSubmit = async (values) => {
await sleep(300);
window.alert(JSON.stringify(values, 0, 2));
};
/************ IMPORTANT CODE STARTS HERE **************/
const FieldPrefixContext = React.createContext();
const FieldPrefix = ({ prefix, children }) => (
{children}
);
const PrefixedField = ({ name, ...props }) => (
{(prefix) => }
);
/************* IMPORTANT CODE ENDS HERE ***************/
const App = () => (
🏁
{" "}
React Final Form
Prefixed Fields Example
Read Docs
This example shows how to use React Context to create sections of your
form that have their fields' names prefixed, to structure the resulting
form data. This provides similar functionality to Redux Form's{" "}
FormSection
{" "}
component.
(
Shipping
Billing
Submit
Reset
{JSON.stringify(values, 0, 2)}
)}
/>
);
render( , document.getElementById("root"));
================================================
FILE: examples/prefixed-fields/package.json
================================================
{
"name": "react-final-form-prefixed-fields",
"version": "1.0.0",
"description": "This example shows how to use React Context to create sections of your form that have their fields' names prefixed, to structure the resulting form data. This provides similar functionality to Redux Form's FormSection component.",
"keywords": [],
"homepage": "https://codesandbox.io/s/ww40y2m595",
"main": "index.js",
"dependencies": {
"styled-components": "latest",
"react-fontawesome": "latest",
"react-final-form": "6.5.3",
"react-dom": "latest",
"react": "latest",
"final-form": "4.20.4"
}
}
================================================
FILE: examples/prefixed-fields/readme.md
================================================
# Prefixed Fields
[](https://codesandbox.io/s/react-final-form-prefixed-fields-seiy8)
================================================
FILE: examples/readme.md
================================================
# [See the Examples](https://final-form.org/docs/react-final-form/examples)
================================================
FILE: examples/record-level-validation/Styles.js
================================================
import styled, { css } from "styled-components";
const btn = (light, dark) => css`
white-space: nowrap;
display: inline-block;
border-radius: 5px;
padding: 5px 15px;
font-size: 16px;
color: white;
&:visited {
color: white;
}
background-image: linear-gradient(${light}, ${dark});
border: 1px solid ${dark};
&:hover {
background-image: linear-gradient(${light}, ${dark});
&[disabled] {
background-image: linear-gradient(${light}, ${dark});
}
}
&:visited {
color: black;
}
&[disabled] {
opacity: 0.6;
cursor: not-allowed;
}
`;
const btnDefault = css`
${btn("#ffffff", "#d5d5d5")} color: #555;
`;
const btnPrimary = btn("#4f93ce", "#285f8f");
export default styled.div`
font-family: sans-serif;
h1 {
text-align: center;
color: #222;
}
h2 {
text-align: center;
color: #222;
}
& > div {
text-align: center;
}
a {
display: block;
text-align: center;
color: #222;
}
form {
max-width: 500px;
margin: 10px auto;
border: 1px solid #ccc;
padding: 20px;
box-shadow: 2px 2px 5px rgba(0, 0, 0, 0.3);
border-radius: 3px;
& > div {
display: flex;
flex-flow: row nowrap;
line-height: 2em;
margin: 5px;
& > label {
color: #333;
width: 110px;
font-size: 1em;
line-height: 32px;
}
& > input,
& > select,
& > textarea {
flex: 1;
padding: 3px 5px;
font-size: 1em;
margin-left: 15px;
border: 1px solid #ccc;
border-radius: 3px;
}
& > input[type="checkbox"] {
margin-top: 7px;
}
& > div {
margin-left: 16px;
& > label {
display: block;
& > input {
margin-right: 3px;
}
}
}
& > span {
line-height: 32px;
margin-left: 10px;
color: #800;
font-weight: bold;
}
}
& > .buttons {
display: flex;
flex-flow: row nowrap;
justify-content: center;
margin-top: 15px;
}
button {
margin: 0 10px;
&[type="submit"] {
${btnPrimary};
}
&[type="button"] {
${btnDefault};
}
}
pre {
border: 1px solid #ccc;
background: rgba(0, 0, 0, 0.1);
box-shadow: inset 1px 1px 3px rgba(0, 0, 0, 0.2);
padding: 20px;
}
}
`;
================================================
FILE: examples/record-level-validation/index.js
================================================
import React from "react";
import { render } from "react-dom";
import Styles from "./Styles";
import { Form, Field } from "react-final-form";
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
const onSubmit = async (values) => {
await sleep(300);
window.alert(JSON.stringify(values, 0, 2));
};
const App = () => (
React Final Form Example
Password / Confirm Validation
Read Docs
{
const errors = {};
if (!values.username) {
errors.username = "Required";
}
if (!values.password) {
errors.password = "Required";
}
if (!values.confirm) {
errors.confirm = "Required";
} else if (values.confirm !== values.password) {
errors.confirm = "Must match";
}
return errors;
}}
render={({ handleSubmit, form, submitting, pristine, values }) => (
{({ input, meta }) => (
Username
{meta.error && meta.touched && {meta.error} }
)}
{({ input, meta }) => (
Password
{meta.error && meta.touched && {meta.error} }
)}
{({ input, meta }) => (
Confirm
{meta.error && meta.touched && {meta.error} }
)}
Submit
Reset
{JSON.stringify(values, 0, 2)}
)}
/>
);
render( , document.getElementById("root"));
================================================
FILE: examples/record-level-validation/package.json
================================================
{
"name": "React Final Form - Synchronous Record Level Validation",
"version": "1.0.0",
"description": "An example of a form with synchronous record-level validation created with React Final Form",
"keywords": [
"state management",
"react-final-form",
"form",
"final-form",
"react"
],
"main": "index.js",
"dependencies": {
"styled-components": "latest",
"react-final-form": "6.5.3",
"react": "latest",
"react-dom": "latest",
"final-form": "4.20.4"
}
}
================================================
FILE: examples/record-level-validation/readme.md
================================================
# Record Level Validation Example
[](https://codesandbox.io/s/github/final-form/react-final-form/tree/master/examples/record-level-validation?fontsize=14)
================================================
FILE: examples/redux/FormStateFromRedux.js
================================================
import React from "react";
import { connect } from "react-redux";
import { getFormState } from "./finalFormDuck";
const FormStateFromRedux = ({ state }) => (
{JSON.stringify(state, 0, 2)}
);
export default connect((state, ownProps) => ({
state: getFormState(state, ownProps.form),
}))(FormStateFromRedux);
================================================
FILE: examples/redux/FormStateToRedux.js
================================================
import React from "react";
import { connect } from "react-redux";
import { FormSpy } from "react-final-form";
import { updateFormState } from "./finalFormDuck";
const FormStateToRedux = ({ form, updateFormState }) => (
updateFormState(form, state)} />
);
export default connect(undefined, { updateFormState })(FormStateToRedux);
================================================
FILE: examples/redux/Styles.js
================================================
import styled, { css } from "styled-components";
const btn = (light, dark) => css`
white-space: nowrap;
display: inline-block;
border-radius: 5px;
padding: 5px 15px;
font-size: 16px;
color: white;
&:visited {
color: white;
}
background-image: linear-gradient(${light}, ${dark});
border: 1px solid ${dark};
&:hover {
background-image: linear-gradient(${light}, ${dark});
&[disabled] {
background-image: linear-gradient(${light}, ${dark});
}
}
&:visited {
color: black;
}
&[disabled] {
opacity: 0.6;
cursor: not-allowed;
}
`;
const btnDefault = css`
${btn("#ffffff", "#d5d5d5")} color: #555;
`;
const btnPrimary = btn("#4f93ce", "#285f8f");
const btnDanger = btn("#e27c79", "#c9302c");
export default styled.div`
font-family: sans-serif;
h1 {
text-align: center;
color: #222;
}
h2 {
text-align: center;
color: #222;
}
& > div {
text-align: center;
}
a {
display: block;
text-align: center;
color: #222;
margin-bottom: 10px;
}
p {
max-width: 500px;
margin: 10px auto;
& > a {
display: inline;
}
}
.loading {
font-size: 2em;
font-weight: bold;
text-align: center;
margin: 50px;
}
form,
div.form {
text-align: left;
max-width: 500px;
margin: 10px auto;
border: 1px solid #ccc;
padding: 20px;
box-shadow: 2px 2px 5px rgba(0, 0, 0, 0.3);
border-radius: 3px;
position: relative;
& > div {
display: flex;
flex-flow: row nowrap;
line-height: 2em;
margin: 5px;
position: relative;
& > label {
color: #333;
width: 110px;
min-width: 60px;
font-size: 1em;
line-height: 32px;
}
& > input,
& > select,
& > textarea {
flex: 1;
padding: 3px 5px;
font-size: 1em;
margin-left: 15px;
border: 1px solid #ccc;
border-radius: 3px;
}
& > input[type="checkbox"] {
margin-top: 7px;
}
& > div {
margin-left: 16px;
& > label {
display: block;
& > input {
margin-right: 3px;
}
}
}
& > span {
line-height: 32px;
margin-left: 10px;
color: #800;
font-weight: bold;
}
& > button.remove {
${btnDanger};
}
}
& > .buttons {
display: flex;
flex-flow: row nowrap;
justify-content: center;
margin-top: 15px;
}
.error {
display: flex;
font-weight: bold;
color: #800;
flex-flow: row nowrap;
justify-content: center;
}
pre {
position: relative;
border: 1px solid #ccc;
background: rgba(0, 0, 0, 0.1);
box-shadow: inset 1px 1px 3px rgba(0, 0, 0, 0.2);
padding: 20px;
}
.submitting {
display: block;
position: absolute;
top: -5px;
left: -5px;
right: -5px;
padding: 0;
text-align: center;
background: rgba(0, 0, 0, 0.4);
color: white;
z-index: 10;
font-weight: bold;
font-size: 0.8em;
}
.saving {
font-size: 0.8em;
font-weight: bold;
color: darkblue;
margin: 8px 0 0 7px;
}
}
button {
margin: 0 10px;
&[type="submit"] {
${btnPrimary};
}
&[type="button"] {
${btnDefault};
}
}
`;
================================================
FILE: examples/redux/finalFormDuck.js
================================================
// QUACK! This is a duck. https://github.com/erikras/ducks-modular-redux
// Actions
const UPDATE_FORM_STATE =
"final-form-redux-example/finalForm/UPDATE_FORM_STATE";
// Reducer
export default function reducer(state = {}, action = {}) {
switch (action.type) {
case UPDATE_FORM_STATE:
return {
...state,
[action.form]: action.payload,
};
default:
return state;
}
}
// Action Creators
export const updateFormState = (form, state) => ({
type: UPDATE_FORM_STATE,
form,
payload: state,
});
// Selectors
export const getFormState = (state, form) =>
(state && state.finalForm && state.finalForm[form]) || {};
================================================
FILE: examples/redux/index.js
================================================
import React from "react";
import { render } from "react-dom";
import { Provider } from "react-redux";
import store from "./store";
import Styles from "./Styles";
import { Form, Field } from "react-final-form";
import FormStateToRedux from "./FormStateToRedux";
import FormStateFromRedux from "./FormStateFromRedux";
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
const onSubmit = async (values) => {
await sleep(300);
window.alert(JSON.stringify(values, 0, 2));
};
const App = () => (
🏁
{" "}
React Final Form
Redux Example
Read Docs
The only reason to keep your{" "}
🏁
{" "}
Final Form form data in Redux is if you need to be able to read it from
outside your form. This example demonstrates how to use a{" "}
FormSpy to keep a copy of your form data in the Redux
store. Note that the canonical authoritative version of the data still
lives in{" "}
🏁
{" "}
Final Form. If you need to mutate your data via dispatching
Redux actions, you should probably use{" "}
Redux Form .
{({ handleSubmit, form, submitting, pristine }) => (
First Name
Last Name
Email
Favorite Color
❤️
{" "}
Red
💚
{" "}
Green
💙
{" "}
Blue
Employed?
Toppings
🐷
{" "}
Ham
🍄
{" "}
Mushrooms
🧀
{" "}
Cheese
🐓
{" "}
Chicken
🍍
{" "}
Pinapple
Best Stooge?
{" "}
Larry
{" "}
Moe
{" "}
Curly
Sauces
{" "}
Ketchup
{" "}
Mustard
{" "}
Salsa
{" "}
Guacamole{" "}
🥑
Notes
Submit
Reset
Form State from Redux
)}
);
render( , document.getElementById("root"));
================================================
FILE: examples/redux/package.json
================================================
{
"name": "🏁 React Final Form - Redux Example",
"version": "1.0.0",
"description": "Demonstrates how to store a copy of your form data in the Redux store for easy access outside your form.",
"keywords": [
"redux",
"form",
"react-final-form",
"final-form",
"forms"
],
"homepage": "https://codesandbox.io/s/new",
"main": "src/index.js",
"dependencies": {
"final-form": "4.20.4",
"react": "17.0.2",
"react-dom": "17.0.2",
"react-final-form": "6.5.3",
"react-redux": "7.0.3",
"react-scripts": "3.0.1",
"redux": "4.0.1",
"styled-components": "4.2.0"
},
"devDependencies": {},
"scripts": {
"start": "react-scripts start",
"build": "react-scripts build",
"test": "react-scripts test --env=jsdom",
"eject": "react-scripts eject"
}
}
================================================
FILE: examples/redux/readme.md
================================================
# Redux Example
[](https://codesandbox.io/s/4xq2qpzw79)
================================================
FILE: examples/redux/store.js
================================================
import { createStore, combineReducers } from "redux";
import finalFormReducer from "./finalFormDuck";
const reducer = combineReducers({
finalForm: finalFormReducer,
});
const store = (
typeof window !== "undefined" && window.devToolsExtension
? window.devToolsExtension()(createStore)
: createStore
)(reducer);
export default store;
================================================
FILE: examples/reusable-field-groups/Styles.js
================================================
import styled, { css } from "styled-components";
const btn = (light, dark) => css`
white-space: nowrap;
display: inline-block;
border-radius: 5px;
padding: 5px 15px;
font-size: 16px;
color: white;
&:visited {
color: white;
}
background-image: linear-gradient(${light}, ${dark});
border: 1px solid ${dark};
&:hover {
background-image: linear-gradient(${light}, ${dark});
&[disabled] {
background-image: linear-gradient(${light}, ${dark});
}
}
&:visited {
color: black;
}
&[disabled] {
opacity: 0.6;
cursor: not-allowed;
}
`;
const btnDefault = css`
${btn("#ffffff", "#d5d5d5")} color: #555;
`;
const btnPrimary = btn("#4f93ce", "#285f8f");
const btnDanger = btn("#e27c79", "#c9302c");
export default styled.div`
font-family: sans-serif;
h1 {
text-align: center;
color: #222;
}
h2 {
text-align: center;
color: #222;
}
& > div {
text-align: center;
}
a {
display: block;
text-align: center;
color: #222;
margin-bottom: 10px;
}
p {
max-width: 500px;
margin: 10px auto;
& > a {
display: inline;
}
}
form {
max-width: 500px;
margin: 10px auto;
border: 1px solid #ccc;
padding: 20px;
box-shadow: 2px 2px 5px rgba(0, 0, 0, 0.3);
border-radius: 3px;
position: relative;
.loading {
text-align: center;
display: block;
position: absolute;
background: url("https://media.giphy.com/media/130AxGoOaR6t0I/giphy.gif")
center center;
background-size: fill;
font-size: 2em;
top: 0;
left: 0;
right: 0;
bottom: 0;
padding: 50px 0 0 0;
z-index: 2;
}
& > div {
display: flex;
flex-flow: row nowrap;
line-height: 2em;
margin: 5px;
position: relative;
& > label {
color: #333;
width: 180px;
min-width: 60px;
font-size: 1em;
line-height: 32px;
}
& > input,
& > select,
& > textarea {
flex: 1;
padding: 3px 5px;
font-size: 1em;
margin-left: 15px;
border: 1px solid #ccc;
border-radius: 3px;
}
& > input[type="checkbox"] {
margin-top: 7px;
}
& > div {
margin-left: 16px;
& > label {
display: block;
& > input {
margin-right: 3px;
}
}
}
& > span {
line-height: 32px;
margin-left: 10px;
color: #800;
font-weight: bold;
}
& > button.remove {
${btnDanger};
}
}
& > .buttons {
display: flex;
flex-flow: row nowrap;
justify-content: center;
margin-top: 15px;
}
button {
margin: 0 10px;
&[type="submit"] {
${btnPrimary};
}
&[type="button"] {
${btnDefault};
}
}
.error {
display: flex;
font-weight: bold;
color: #800;
flex-flow: row nowrap;
justify-content: center;
}
pre {
position: relative;
border: 1px solid #ccc;
background: rgba(0, 0, 0, 0.1);
box-shadow: inset 1px 1px 3px rgba(0, 0, 0, 0.2);
padding: 20px;
}
}
`;
================================================
FILE: examples/reusable-field-groups/index.js
================================================
import React from "react";
import { render } from "react-dom";
import Styles from "./Styles";
import { Form, Field } from "react-final-form";
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
const onSubmit = async (values) => {
await sleep(300);
window.alert(JSON.stringify(values, 0, 2));
};
const Address = ({ name, label }) => (
{label} Street
{label} City
{label} Postal Code
);
const App = () => (
🏁
{" "}
React Final Form Example
Reusable Field Groups
Read Docs
(
Name
Submit
Reset
{JSON.stringify(values, 0, 2)}
)}
/>
);
render( , document.getElementById("root"));
================================================
FILE: examples/reusable-field-groups/package.json
================================================
{
"name": "react-final-form-reusable-field-groups",
"version": "1.0.0",
"description": "Demonstrates how fields can be grouped into a reusable component.",
"keywords": [],
"main": "index.js",
"dependencies": {
"final-form": "4.20.4",
"react": "17.0.2",
"react-dom": "17.0.2",
"react-final-form": "6.5.3",
"styled-components": "4.2.0"
}
}
================================================
FILE: examples/reusable-field-groups/readme.md
================================================
# Reusable Field Groups
[](https://codesandbox.io/s/8z5jm6x80)
================================================
FILE: examples/simple/Styles.js
================================================
import styled, { css } from "styled-components";
const btn = (light, dark) => css`
white-space: nowrap;
display: inline-block;
border-radius: 5px;
padding: 5px 15px;
font-size: 16px;
color: white;
&:visited {
color: white;
}
background-image: linear-gradient(${light}, ${dark});
border: 1px solid ${dark};
&:hover {
background-image: linear-gradient(${light}, ${dark});
&[disabled] {
background-image: linear-gradient(${light}, ${dark});
}
}
&:visited {
color: black;
}
&[disabled] {
opacity: 0.6;
cursor: not-allowed;
}
`;
const btnDefault = css`
${btn("#ffffff", "#d5d5d5")} color: #555;
`;
const btnPrimary = btn("#4f93ce", "#285f8f");
export default styled.div`
font-family: sans-serif;
h1 {
text-align: center;
color: #222;
}
& > div {
text-align: center;
}
a {
display: block;
text-align: center;
color: #222;
}
form {
max-width: 500px;
margin: 10px auto;
border: 1px solid #ccc;
padding: 20px;
box-shadow: 2px 2px 5px rgba(0, 0, 0, 0.3);
border-radius: 3px;
& > div {
display: flex;
flex-flow: row nowrap;
line-height: 2em;
margin: 5px;
& > label {
color: #333;
width: 110px;
font-size: 1em;
line-height: 32px;
}
& > input,
& > select,
& > textarea {
flex: 1;
padding: 3px 5px;
font-size: 1em;
margin-left: 15px;
border: 1px solid #ccc;
border-radius: 3px;
}
& > input[type="checkbox"] {
margin-top: 7px;
}
& > div {
margin-left: 16px;
& > label {
display: block;
& > input {
margin-right: 3px;
}
}
}
}
& > .buttons {
display: flex;
flex-flow: row nowrap;
justify-content: center;
margin-top: 15px;
}
button {
margin: 0 10px;
&[type="submit"] {
${btnPrimary};
}
&[type="button"] {
${btnDefault};
}
}
pre {
border: 1px solid #ccc;
background: rgba(0, 0, 0, 0.1);
box-shadow: inset 1px 1px 3px rgba(0, 0, 0, 0.2);
padding: 20px;
}
}
`;
================================================
FILE: examples/simple/index.js
================================================
/* eslint-disable jsx-a11y/accessible-emoji */
import React from "react";
import { render } from "react-dom";
import Styles from "./Styles";
import { Form, Field } from "react-final-form";
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
const onSubmit = async (values) => {
await sleep(300);
window.alert(JSON.stringify(values, 0, 2));
};
const App = () => (
React Final Form - Simple Example
Read Docs
(
First Name
Last Name
Employed
Favorite Color
❤️ Red
💚 Green
💙 Blue
Toppings
🐓 Chicken
🐷 Ham
🍄 Mushrooms
🧀 Cheese
🐟 Tuna
🍍 Pineapple
Sauces
{" "}
Ketchup
{" "}
Mustard
{" "}
Mayonnaise
{" "}
Guacamole 🥑
Best Stooge
{" "}
Larry
{" "}
Moe
{" "}
Curly
Notes
Submit
Reset
{JSON.stringify(values, 0, 2)}
)}
/>
);
render( , document.getElementById("root"));
================================================
FILE: examples/simple/package.json
================================================
{
"name": "React Final Form - Simple Example",
"version": "1.0.0",
"description": "An example of a simple form created with React Final Form",
"keywords": [
"state management",
"react-final-form",
"form",
"final-form",
"react"
],
"main": "index.js",
"dependencies": {
"styled-components": "latest",
"react-final-form": "6.5.3",
"react": "latest",
"react-dom": "latest",
"final-form": "4.20.4",
"prop-types": "latest"
}
}
================================================
FILE: examples/simple/readme.md
================================================
# Simple Example
[](https://codesandbox.io/s/github/final-form/react-final-form/tree/master/examples/simple?fontsize=14)
================================================
FILE: examples/strongly-typed-values-typescript/Styles.tsx
================================================
import styled, { css } from "styled-components";
const btn = (light: string, dark: string) => css`
white-space: nowrap;
display: inline-block;
border-radius: 5px;
padding: 5px 15px;
font-size: 16px;
color: white;
&:visited {
color: white;
}
background-image: linear-gradient(${light}, ${dark});
border: 1px solid ${dark};
&:hover {
background-image: linear-gradient(${light}, ${dark});
&[disabled] {
background-image: linear-gradient(${light}, ${dark});
}
}
&:visited {
color: black;
}
&[disabled] {
opacity: 0.6;
cursor: not-allowed;
}
`;
const btnDefault = css`
${btn("#ffffff", "#d5d5d5")} color: #555;
`;
const btnPrimary = btn("#4f93ce", "#285f8f");
export default styled.div`
font-family: sans-serif;
h1 {
text-align: center;
color: #222;
}
h2 {
text-align: center;
color: #222;
}
& > div {
text-align: center;
}
p {
text-align: center;
}
a {
display: block;
text-align: center;
color: #222;
}
form {
max-width: 500px;
margin: 10px auto;
border: 1px solid #ccc;
padding: 20px;
box-shadow: 2px 2px 5px rgba(0, 0, 0, 0.3);
border-radius: 3px;
& > div {
display: flex;
flex-flow: row nowrap;
line-height: 2em;
margin: 5px;
& > label {
color: #333;
width: 110px;
font-size: 1em;
line-height: 32px;
}
& > input,
& > select,
& > textarea {
flex: 1;
padding: 3px 5px;
font-size: 1em;
margin-left: 15px;
border: 1px solid #ccc;
border-radius: 3px;
}
& > input[type="checkbox"] {
margin-top: 7px;
}
& > div {
margin-left: 16px;
& > label {
display: block;
& > input {
margin-right: 3px;
}
}
}
}
& > .buttons {
display: flex;
flex-flow: row nowrap;
justify-content: center;
margin-top: 15px;
}
button {
margin: 0 10px;
&[type="submit"] {
${btnPrimary};
}
&[type="button"] {
${btnDefault};
}
}
pre {
border: 1px solid #ccc;
background: rgba(0, 0, 0, 0.1);
box-shadow: inset 1px 1px 3px rgba(0, 0, 0, 0.2);
padding: 20px;
}
}
`;
================================================
FILE: examples/strongly-typed-values-typescript/components/CheckboxInput.tsx
================================================
import React from "react";
import { FieldRenderProps } from "react-final-form";
type Props = FieldRenderProps;
const CheckboxInput: React.FC = ({
input: { value, ...input },
}: Props) => ;
export default CheckboxInput;
================================================
FILE: examples/strongly-typed-values-typescript/components/MultiCheckboxInput.tsx
================================================
import React from "react";
import { FieldRenderProps } from "react-final-form";
type Props = FieldRenderProps;
const MultiCheckboxInput: React.FC = ({
input: { value, ...input },
}: Props) => ;
export default MultiCheckboxInput;
================================================
FILE: examples/strongly-typed-values-typescript/components/MultiSelectInput.tsx
================================================
import React from "react";
import { FieldRenderProps } from "react-final-form";
type Props = FieldRenderProps;
const MultiSelectInput: React.FC = ({ input, meta, ...rest }: Props) => (
);
export default MultiSelectInput;
================================================
FILE: examples/strongly-typed-values-typescript/components/NumberInput.tsx
================================================
import React from "react";
import { FieldRenderProps } from "react-final-form";
type Props = FieldRenderProps;
const NumberInput: React.FC = ({ input, meta, ...rest }: Props) => (
);
export default NumberInput;
================================================
FILE: examples/strongly-typed-values-typescript/components/RadioInput.tsx
================================================
import React from "react";
import { FieldRenderProps } from "react-final-form";
function RadioInput({
input,
meta,
...rest
}: FieldRenderProps) {
return ;
}
export default RadioInput;
================================================
FILE: examples/strongly-typed-values-typescript/components/SelectInput.tsx
================================================
import React from "react";
import { FieldRenderProps } from "react-final-form";
type Props = FieldRenderProps;
const SelectInput: React.FC = ({ input, meta, ...rest }: Props) => (
);
export default SelectInput;
================================================
FILE: examples/strongly-typed-values-typescript/components/TextAreaInput.tsx
================================================
import React from "react";
import { FieldRenderProps } from "react-final-form";
type Props = FieldRenderProps;
const TextAreaInput: React.FC = ({ input, meta, ...rest }: Props) => (
);
export default TextAreaInput;
================================================
FILE: examples/strongly-typed-values-typescript/components/TextInput.tsx
================================================
import React from "react";
import { FieldRenderProps } from "react-final-form";
type Props = FieldRenderProps;
const TextInput: React.FC = ({ input, meta, ...rest }: Props) => (
);
export default TextInput;
================================================
FILE: examples/strongly-typed-values-typescript/index.tsx
================================================
/* eslint-disable jsx-a11y/accessible-emoji */
import React from "react";
import { render } from "react-dom";
import Styles from "./Styles";
import { Form, Field } from "react-final-form";
import CheckboxInput from "./components/CheckboxInput";
import RadioInput from "./components/RadioInput";
import TextInput from "./components/TextInput";
import NumberInput from "./components/NumberInput";
import TextAreaInput from "./components/TextAreaInput";
import SelectInput from "./components/SelectInput";
import MultiSelectInput from "./components/MultiSelectInput";
import MultiCheckboxInput from "./components/MultiCheckboxInput";
const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));
type Stooge = "larry" | "moe" | "curly";
interface Values {
firstName?: string;
lastName?: string;
employed: boolean;
favoriteColor?: string;
toppings?: string[];
sauces?: string[];
stooge: Stooge;
notes?: string;
}
const onSubmit = async (values: Values) => {
await sleep(300);
window.alert(JSON.stringify(values, undefined, 2));
};
const App: React.FC = () => (
🏁
{" "}
React Final Form
Strongly Typed Values with TypeScript
Strongly Typed form values and field values. Each input requires a
specific type, which is provided by a JSX generic specification on the
Field component.
Read Docs
(
First Name
name="firstName"
component={TextInput}
placeholder="First Name"
/>
Last Name
name="lastName"
component={TextInput}
placeholder="Last Name"
/>
Age
name="age"
component={NumberInput}
placeholder="Age"
/>
Employed
name="employed"
type="checkbox"
component={CheckboxInput}
/>
Favorite Color
name="favoriteColor" component={SelectInput}>
❤️ Red
💚 Green
💙 Blue
Toppings
name="toppings" component={MultiSelectInput}>
🐓 Chicken
🐷 Ham
🍄 Mushrooms
🧀 Cheese
🐟 Tuna
🍍 Pineapple
Sauces
name="sauces"
component={MultiCheckboxInput}
type="checkbox"
value="ketchup"
/>{" "}
Ketchup
name="sauces"
component="input"
type="checkbox"
value="mustard"
/>{" "}
Mustard
name="sauces"
component="input"
type="checkbox"
value="mayonnaise"
/>{" "}
Mayonnaise
name="sauces"
component="input"
type="checkbox"
value="guacamole"
/>{" "}
Guacamole 🥑
Best Stooge
name="stooge"
component={RadioInput}
type="radio"
value="larry"
/>{" "}
Larry
name="stooge"
component={RadioInput}
type="radio"
value="moe"
/>{" "}
Moe
name="stooge"
component={RadioInput}
type="radio"
value="curly"
/>{" "}
Curly
Notes
Submit
Reset
{JSON.stringify(values, undefined, 2)}
)}
/>
);
const rootElement = document.getElementById("root");
render( , rootElement);
================================================
FILE: examples/strongly-typed-values-typescript/readme.md
================================================
# Strongly Typed Form and Field Values with TypeScript
[](https://codesandbox.io/s/strongly-typed-form-values-with-react-final-form-26jkd)
================================================
FILE: examples/styling-with-smooth-ui/index.js
================================================
import React from "react";
import { render } from "react-dom";
import { Form, Field } from "react-final-form";
import {
Box,
Button,
Checkbox,
Col,
ControlFeedback,
FormCheck,
FormCheckLabel,
FormGroup,
Input,
Label,
Radio,
RadioGroup,
Row,
Select,
Textarea,
Typography,
} from "smooth-ui";
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
const onSubmit = async (values) => {
await sleep(300);
window.alert(JSON.stringify(values, 0, 2));
};
// ****************************************
//⬇️ THIS IS WHERE ALL THE MAGIC HAPPENS ⬇️
// ****************************************
const adapt /* ⬅️ this is a HOC */ =
(Component) =>
({ input, meta: { valid }, ...rest }) => (
);
const AdaptedInput = adapt(Input);
const AdaptedCheckbox = adapt(Checkbox);
const AdaptedRadio = adapt(Radio);
const AdaptedSelect = adapt(Select);
const AdaptedTextarea = adapt(Textarea);
const Error = ({ name }) => (
{({ meta: { touched, error } }) =>
touched && error ? (
{error}
) : null
}
);
// ****************************************
//⬆️ THIS IS WHERE ALL THE MAGIC HAPPENS ⬆️
// ****************************************
const required = (value) => (value ? undefined : "Required");
const App = () => (
🏁
{" "}
React Final Form
🍭
{" "}
Smooth-UI Example
This example demonstrates how to use{" "}
🍭
{" "}
Smooth-UI
{" "}
to make your forms look fabulous. All you really need is the
higher-order component that adapts the{" "}
🍭
{" "}
Smooth-UI form components to be compatible with{" "}
🏁
{" "}
React Final Form.
(
First Name
Last Name
Employed
Favorite Color
Toppings
Sauces
Ketchup
Mayonnaise
Guacamole{" "}
🥑
Best Stooge
Larry
Moe
Curly
Notes
Submit
Reset
{JSON.stringify(values, 0, 2)}
)}
/>
);
render( , document.getElementById("root"));
================================================
FILE: examples/styling-with-smooth-ui/package.json
================================================
{
"name": "react-final-form-smooth-ui-example",
"version": "1.0.0",
"description": "Demonstrates how to use the 🍭 Smooth UI styling library to make your forms gorgeous in 🏁 React Final Form.",
"keywords": [
"react-final-form",
"final-form",
"smooth-ui",
"styling",
"forms"
],
"main": "src/index.js",
"dependencies": {
"final-form": "4.20.4",
"react": "17.0.2",
"react-dom": "17.0.2",
"react-final-form": "6.5.3",
"react-scripts": "3.0.1",
"smooth-ui": "4.3.2",
"styled-components": "3.4.10"
},
"devDependencies": {},
"scripts": {
"start": "react-scripts start",
"build": "react-scripts build",
"test": "react-scripts test --env=jsdom",
"eject": "react-scripts eject"
}
}
================================================
FILE: examples/styling-with-smooth-ui/readme.md
================================================
# Styling with Smooth-UI
[](https://codesandbox.io/s/40o45po3l4)
================================================
FILE: examples/submission-errors/Styles.js
================================================
import styled, { css } from "styled-components";
const btn = (light, dark) => css`
white-space: nowrap;
display: inline-block;
border-radius: 5px;
padding: 5px 15px;
font-size: 16px;
color: white;
&:visited {
color: white;
}
background-image: linear-gradient(${light}, ${dark});
border: 1px solid ${dark};
&:hover {
background-image: linear-gradient(${light}, ${dark});
&[disabled] {
background-image: linear-gradient(${light}, ${dark});
}
}
&:visited {
color: black;
}
&[disabled] {
opacity: 0.6;
cursor: not-allowed;
}
`;
const btnDefault = css`
${btn("#ffffff", "#d5d5d5")} color: #555;
`;
const btnPrimary = btn("#4f93ce", "#285f8f");
export default styled.div`
font-family: sans-serif;
h1 {
text-align: center;
color: #222;
}
h2 {
text-align: center;
color: #222;
}
& > div {
text-align: center;
}
a {
display: block;
text-align: center;
color: #222;
margin-bottom: 10px;
}
form {
max-width: 500px;
margin: 10px auto;
border: 1px solid #ccc;
padding: 20px;
box-shadow: 2px 2px 5px rgba(0, 0, 0, 0.3);
border-radius: 3px;
position: relative;
& > div {
display: flex;
flex-flow: row nowrap;
line-height: 2em;
margin: 5px;
& > label {
color: #333;
width: 110px;
font-size: 1em;
line-height: 32px;
}
& > input,
& > select,
& > textarea {
flex: 1;
padding: 3px 5px;
font-size: 1em;
margin-left: 15px;
border: 1px solid #ccc;
border-radius: 3px;
}
& > input[type="checkbox"] {
margin-top: 7px;
}
& > div {
margin-left: 16px;
& > label {
display: block;
& > input {
margin-right: 3px;
}
}
}
& > span {
line-height: 32px;
margin-left: 10px;
color: #800;
font-weight: bold;
}
}
& > .buttons {
display: flex;
flex-flow: row nowrap;
justify-content: center;
margin-top: 15px;
}
button {
margin: 0 10px;
&[type="submit"] {
${btnPrimary};
}
&[type="button"] {
${btnDefault};
}
}
.error {
display: flex;
font-weight: bold;
color: #800;
flex-flow: row nowrap;
justify-content: center;
}
pre {
border: 1px solid #ccc;
background: rgba(0, 0, 0, 0.1);
box-shadow: inset 1px 1px 3px rgba(0, 0, 0, 0.2);
padding: 20px;
}
}
`;
================================================
FILE: examples/submission-errors/index.js
================================================
import React from "react";
import { render } from "react-dom";
import Styles from "./Styles";
import { Form, Field } from "react-final-form";
import { FORM_ERROR } from "final-form";
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
const onSubmit = async (values) => {
await sleep(300);
if (values.username !== "erikras") {
return { username: "Unknown username" };
}
if (values.password !== "finalformrocks") {
return { [FORM_ERROR]: "Login Failed" };
}
window.alert("LOGIN SUCCESS!");
};
const App = () => (
React Final Form Example
Submission Errors
Read Docs
Only successful credentials are erikras and{" "}
finalformrocks.
{
const errors = {};
if (!values.username) {
errors.username = "Required";
}
if (!values.password) {
errors.password = "Required";
}
return errors;
}}
render={({
submitError,
handleSubmit,
form,
submitting,
pristine,
values,
}) => (
{({ input, meta }) => (
Username
{(meta.error || meta.submitError) && meta.touched && (
{meta.error || meta.submitError}
)}
)}
{({ input, meta }) => (
Password
{meta.error && meta.touched && {meta.error} }
)}
{submitError && {submitError}
}
Log In
Reset
{JSON.stringify(values, 0, 2)}
)}
/>
);
render( , document.getElementById("root"));
================================================
FILE: examples/submission-errors/package.json
================================================
{
"name": "React Final Form - Submission Errors",
"version": "1.0.0",
"description": "An example of a form with submission errors created with React Final Form",
"keywords": [
"state management",
"react-final-form",
"form",
"final-form",
"react"
],
"main": "index.js",
"dependencies": {
"styled-components": "latest",
"react-final-form": "6.5.3",
"react": "latest",
"react-dom": "latest",
"final-form": "4.20.4"
}
}
================================================
FILE: examples/submission-errors/readme.md
================================================
# Submission Errors Example
[](https://codesandbox.io/s/github/final-form/react-final-form/tree/master/examples/submission-errors?fontsize=14)
================================================
FILE: examples/subscriptions/RenderCount.js
================================================
import React from "react";
import styled from "styled-components";
export default function RenderCount() {
const renders = React.useRef(0);
return {++renders.current} ;
}
const size = 30;
const Circle = styled.i`
position: absolute;
top: 0;
right: 0;
font-style: normal;
text-align: center;
height: ${size}px;
width: ${size}px;
line-height: ${size}px;
border-radius: ${size / 2}px;
border: 1px solid #ddd;
background: #eee;
`;
================================================
FILE: examples/subscriptions/Styles.js
================================================
import styled, { css } from "styled-components";
const btn = (light, dark) => css`
white-space: nowrap;
display: inline-block;
border-radius: 5px;
padding: 5px 15px;
font-size: 16px;
color: white;
&:visited {
color: white;
}
background-image: linear-gradient(${light}, ${dark});
border: 1px solid ${dark};
&:hover {
background-image: linear-gradient(${light}, ${dark});
&[disabled] {
background-image: linear-gradient(${light}, ${dark});
}
}
&:visited {
color: black;
}
&[disabled] {
opacity: 0.6;
cursor: not-allowed;
}
`;
const btnDefault = css`
${btn("#ffffff", "#d5d5d5")} color: #555;
`;
const btnPrimary = btn("#4f93ce", "#285f8f");
export default styled.div`
font-family: sans-serif;
h1,
h2 {
text-align: center;
color: #222;
}
& > div {
text-align: center;
}
a {
display: block;
text-align: center;
color: #222;
}
form {
position: relative;
max-width: 500px;
margin: 10px auto;
border: 1px solid #ccc;
padding: 20px;
box-shadow: 2px 2px 5px rgba(0, 0, 0, 0.3);
border-radius: 3px;
& > div {
position: relative;
display: flex;
flex-flow: row nowrap;
line-height: 2em;
margin: 5px;
& > label {
color: #333;
width: 110px;
font-size: 1em;
line-height: 32px;
}
& > input,
& > select,
& > textarea {
flex: 1;
padding: 3px 5px;
font-size: 1em;
margin-left: 15px;
border: 1px solid #ccc;
border-radius: 3px;
}
& > input[type="checkbox"] {
margin-top: 7px;
}
& > span {
line-height: 32px;
margin-left: 10px;
color: #800;
font-weight: bold;
}
& > div {
margin-left: 16px;
& > label {
display: block;
& > input {
margin-right: 3px;
}
}
}
}
& > .buttons {
display: flex;
flex-flow: row nowrap;
justify-content: center;
margin-top: 15px;
}
button {
margin: 0 10px;
&[type="submit"] {
${btnPrimary};
}
&[type="button"] {
${btnDefault};
}
}
pre {
position: relative;
border: 1px solid #ccc;
background: rgba(0, 0, 0, 0.1);
box-shadow: inset 1px 1px 3px rgba(0, 0, 0, 0.2);
padding: 20px;
}
}
`;
================================================
FILE: examples/subscriptions/index.js
================================================
import React from "react";
import { render } from "react-dom";
import Styles from "./Styles";
import { Form, Field, FormSpy } from "react-final-form";
import RenderCount from "./RenderCount";
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
const onSubmit = async (values) => {
await sleep(300);
window.alert(JSON.stringify(values, 0, 2));
};
const required = (value) => (value ? undefined : "Required");
const App = () => (
React Final Form Example
Performance Optimization Through Subscriptions
Read Docs
In this example, the numbers in the circles are the number of times that
component has been rendered.
The top form, with no specified subscription, rerenders the whole form and
every input on every change.
The bottom form subscribes only to the changes it needs to update. By not
rerendering the whole form on every change, the fields, too, become
independent. Notice that we must now use a FormSpy component
to show the values in realtime.
);
const MyForm = ({ subscription }) => (
(
{({ input, meta }) => (
First Name
{meta.touched && meta.error && {meta.error} }
)}
{({ input, meta }) => (
Last Name
{meta.touched && meta.error && {meta.error} }
)}
Submit
Reset
{values ? (
{JSON.stringify(values, 0, 2)}
) : (
{({ values }) => (
{JSON.stringify(values, 0, 2)}
)}
)}
)}
/>
);
render( , document.getElementById("root"));
================================================
FILE: examples/subscriptions/package.json
================================================
{
"name": "React Final Form - Subscriptions Example",
"version": "1.0.0",
"description": "An example of controlling rerenders using subscriptions",
"keywords": [
"state management",
"react-final-form",
"form",
"final-form",
"react"
],
"main": "index.js",
"dependencies": {
"styled-components": "latest",
"react-final-form": "6.5.3",
"react": "latest",
"react-dom": "latest",
"final-form": "4.20.4"
}
}
================================================
FILE: examples/subscriptions/readme.md
================================================
# Subscriptions Example
[](https://codesandbox.io/s/github/final-form/react-final-form/tree/master/examples/subscriptions?fontsize=14)
================================================
FILE: examples/third-party-components/Styles.js
================================================
import styled, { css } from "styled-components";
const btn = (light, dark) => css`
white-space: nowrap;
display: inline-block;
border-radius: 5px;
padding: 5px 15px;
font-size: 16px;
color: white;
&:visited {
color: white;
}
background-image: linear-gradient(${light}, ${dark});
border: 1px solid ${dark};
&:hover {
background-image: linear-gradient(${light}, ${dark});
&[disabled] {
background-image: linear-gradient(${light}, ${dark});
}
}
&:visited {
color: black;
}
&[disabled] {
opacity: 0.6;
cursor: not-allowed;
}
`;
const btnDefault = css`
${btn("#ffffff", "#d5d5d5")} color: #555;
`;
const btnPrimary = btn("#4f93ce", "#285f8f");
export default styled.div`
font-family: sans-serif;
h1 {
text-align: center;
color: #222;
}
h2 {
text-align: center;
color: #222;
}
& > div {
text-align: center;
}
& > a {
display: block;
text-align: center;
color: #222;
margin-bottom: 10px;
}
form {
max-width: 500px;
margin: 10px auto;
border: 1px solid #ccc;
padding: 20px;
box-shadow: 2px 2px 5px rgba(0, 0, 0, 0.3);
border-radius: 3px;
position: relative;
& > .buttons {
display: flex;
flex-flow: row nowrap;
justify-content: center;
margin-top: 15px;
}
button {
margin: 0 10px;
&[type="submit"] {
${btnPrimary};
}
&[type="button"] {
${btnDefault};
}
}
.error {
display: flex;
font-weight: bold;
color: #800;
flex-flow: row nowrap;
justify-content: center;
}
.Select {
width: 100%;
}
pre {
border: 1px solid #ccc;
background: rgba(0, 0, 0, 0.1);
box-shadow: inset 1px 1px 3px rgba(0, 0, 0, 0.2);
padding: 20px;
}
}
`;
================================================
FILE: examples/third-party-components/index.js
================================================
import React from "react";
import { render } from "react-dom";
import Styles from "./Styles";
import { Form, Field } from "react-final-form";
import MuiThemeProvider from "material-ui/styles/MuiThemeProvider";
import getMuiTheme from "material-ui/styles/getMuiTheme";
import TextField from "material-ui/TextField";
import Toggle from "material-ui/Toggle";
import Select from "react-select";
import states from "./states";
const TextFieldAdapter = ({ input, meta, ...rest }) => (
input.onChange(value)}
errorText={meta.touched ? meta.error : ""}
/>
);
const ToggleAdapter = ({ input: { onChange, value }, label, ...rest }) => (
onChange(isInputChecked)}
{...rest}
/>
);
const ReactSelectAdapter = ({ input, ...rest }) => (
);
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
const onSubmit = async (values) => {
await sleep(300);
window.alert(JSON.stringify(values, 0, 2));
};
const required = (value) => (value ? undefined : "Required");
const App = () => (
🏁
{" "}
React Final Form Example
Third Party Components
Read Docs
(
Log In
Reset
{JSON.stringify(values, 0, 2)}
)}
/>
);
render( , document.getElementById("root"));
================================================
FILE: examples/third-party-components/package.json
================================================
{
"name": "react-final-form-third-party-components-example",
"version": "1.0.0",
"description": "This example demonstrates how easy it is to use third party input components. All the third party component really needs is value and onChange, but more complex components can accept things like errors.",
"keywords": [
"react",
"inputs",
"react-final-form",
"material-ui",
"final-form"
],
"main": "index.js",
"dependencies": {
"final-form": "4.20.4",
"material-ui": "0.20.2",
"react": "17.0.2",
"react-addons-shallow-compare": "15.6.2",
"react-dom": "17.0.2",
"react-final-form": "6.5.3",
"react-select": "2.4.3",
"styled-components": "4.2.0"
}
}
================================================
FILE: examples/third-party-components/readme.md
================================================
# Third Party Components
[](https://codesandbox.io/s/40mr0v2r87)
================================================
FILE: examples/third-party-components/states.js
================================================
const states = {
AL: "Alabama",
AK: "Alaska",
AS: "American Samoa",
AZ: "Arizona",
AR: "Arkansas",
CA: "California",
CO: "Colorado",
CT: "Connecticut",
DE: "Delaware",
DC: "District Of Columbia",
FM: "Federated States Of Micronesia",
FL: "Florida",
GA: "Georgia",
GU: "Guam",
HI: "Hawaii",
ID: "Idaho",
IL: "Illinois",
IN: "Indiana",
IA: "Iowa",
KS: "Kansas",
KY: "Kentucky",
LA: "Louisiana",
ME: "Maine",
MH: "Marshall Islands",
MD: "Maryland",
MA: "Massachusetts",
MI: "Michigan",
MN: "Minnesota",
MS: "Mississippi",
MO: "Missouri",
MT: "Montana",
NE: "Nebraska",
NV: "Nevada",
NH: "New Hampshire",
NJ: "New Jersey",
NM: "New Mexico",
NY: "New York",
NC: "North Carolina",
ND: "North Dakota",
MP: "Northern Mariana Islands",
OH: "Ohio",
OK: "Oklahoma",
OR: "Oregon",
PW: "Palau",
PA: "Pennsylvania",
PR: "Puerto Rico",
RI: "Rhode Island",
SC: "South Carolina",
SD: "South Dakota",
TN: "Tennessee",
TX: "Texas",
UT: "Utah",
VT: "Vermont",
VI: "Virgin Islands",
VA: "Virginia",
WA: "Washington",
WV: "West Virginia",
WI: "Wisconsin",
WY: "Wyoming",
};
export default Object.keys(states).map((value) => ({
value,
label: states[value],
}));
================================================
FILE: examples/wizard/Styles.js
================================================
import styled, { css } from "styled-components";
const btn = (light, dark) => css`
white-space: nowrap;
display: inline-block;
border-radius: 5px;
padding: 5px 15px;
font-size: 16px;
color: white;
&:visited {
color: white;
}
background-image: linear-gradient(${light}, ${dark});
border: 1px solid ${dark};
&:hover {
background-image: linear-gradient(${light}, ${dark});
&[disabled] {
background-image: linear-gradient(${light}, ${dark});
}
}
&:visited {
color: black;
}
&[disabled] {
opacity: 0.6;
cursor: not-allowed;
}
`;
const btnDefault = css`
${btn("#ffffff", "#d5d5d5")} color: #555;
`;
const btnPrimary = btn("#4f93ce", "#285f8f");
const btnDanger = btn("#e27c79", "#c9302c");
export default styled.div`
font-family: sans-serif;
h1 {
text-align: center;
color: #222;
}
h2 {
text-align: center;
color: #222;
}
& > div {
text-align: center;
}
a {
display: block;
text-align: center;
color: #222;
margin-bottom: 10px;
}
p {
max-width: 500px;
margin: 10px auto;
& > a {
display: inline;
}
}
form {
max-width: 500px;
margin: 10px auto;
border: 1px solid #ccc;
padding: 20px;
box-shadow: 2px 2px 5px rgba(0, 0, 0, 0.3);
border-radius: 3px;
position: relative;
.loading {
text-align: center;
display: block;
position: absolute;
background: url("https://media.giphy.com/media/130AxGoOaR6t0I/giphy.gif")
center center;
background-size: fill;
font-size: 2em;
top: 0;
left: 0;
right: 0;
bottom: 0;
padding: 50px 0 0 0;
z-index: 2;
}
& > div {
display: flex;
flex-flow: row nowrap;
line-height: 2em;
margin: 5px;
position: relative;
& > label {
color: #333;
width: 110px;
min-width: 60px;
font-size: 1em;
line-height: 32px;
}
& > input,
& > select,
& > textarea {
flex: 1;
padding: 3px 5px;
font-size: 1em;
margin-left: 15px;
border: 1px solid #ccc;
border-radius: 3px;
}
& > input[type="checkbox"] {
margin-top: 7px;
}
& > div {
margin-left: 16px;
& > label {
display: block;
& > input {
margin-right: 3px;
}
}
}
& > span {
line-height: 32px;
margin-left: 10px;
color: #800;
font-weight: bold;
}
& > button.remove {
${btnDanger};
}
}
& > .buttons {
display: flex;
flex-flow: row nowrap;
justify-content: center;
margin-top: 15px;
}
.error {
display: flex;
font-weight: bold;
color: #800;
flex-flow: row nowrap;
justify-content: center;
}
pre {
position: relative;
border: 1px solid #ccc;
background: rgba(0, 0, 0, 0.1);
box-shadow: inset 1px 1px 3px rgba(0, 0, 0, 0.2);
padding: 20px;
}
}
button {
margin: 0 10px;
&[type="submit"] {
${btnPrimary};
}
&[type="button"] {
${btnDefault};
}
}
`;
================================================
FILE: examples/wizard/Wizard.js
================================================
import React from "react";
import PropTypes from "prop-types";
import { Form } from "react-final-form";
export default class Wizard extends React.Component {
static propTypes = {
onSubmit: PropTypes.func.isRequired,
};
static Page = ({ children }) => children;
constructor(props) {
super(props);
this.state = {
page: 0,
values: props.initialValues || {},
};
}
next = (values) =>
this.setState((state) => ({
page: Math.min(state.page + 1, this.props.children.length - 1),
values,
}));
previous = () =>
this.setState((state) => ({
page: Math.max(state.page - 1, 0),
}));
/**
* NOTE: Both validate and handleSubmit switching are implemented
* here because 🏁 Redux Final Form does not accept changes to those
* functions once the form has been defined.
*/
validate = (values) => {
const activePage = React.Children.toArray(this.props.children)[
this.state.page
];
return activePage.props.validate ? activePage.props.validate(values) : {};
};
handleSubmit = (values) => {
const { children, onSubmit } = this.props;
const { page } = this.state;
const isLastPage = page === React.Children.count(children) - 1;
if (isLastPage) {
return onSubmit(values);
} else {
this.next(values);
}
};
render() {
const { children } = this.props;
const { page, values } = this.state;
const activePage = React.Children.toArray(children)[page];
const isLastPage = page === React.Children.count(children) - 1;
return (
{({ handleSubmit, submitting, values }) => (
{activePage}
{page > 0 && (
« Previous
)}
{!isLastPage && Next » }
{isLastPage && (
Submit
)}
{JSON.stringify(values, 0, 2)}
)}
);
}
}
================================================
FILE: examples/wizard/index.js
================================================
/* eslint-disable jsx-a11y/accessible-emoji */
import React from "react";
import { render } from "react-dom";
import Styles from "./Styles";
import { Field } from "react-final-form";
import Wizard from "./Wizard";
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
const onSubmit = async (values) => {
await sleep(300);
window.alert(JSON.stringify(values, 0, 2));
};
const Error = ({ name }) => (
touched && error ? {error} : null
}
/>
);
const required = (value) => (value ? undefined : "Required");
const App = () => (
React Final Form Example
Wizard Form
Read Docs
Notice the mixture of field-level and record-level (or page-level {" "}
in this case) validation.
First Name
Last Name
{
const errors = {};
if (!values.email) {
errors.email = "Required";
}
if (!values.favoriteColor) {
errors.favoriteColor = "Required";
}
return errors;
}}
>
Email
Favorite Color
❤️ Red
💚 Green
💙 Blue
{
const errors = {};
if (!values.toppings) {
errors.toppings = "Required";
} else if (values.toppings.length < 2) {
errors.toppings = "Choose more";
}
return errors;
}}
>
Employed?
Toppings
🐷 Ham
🍄 Mushrooms
🧀 Cheese
🐓 Chicken
🍍 Pinapple
{
const errors = {};
if (!values.notes) {
errors.notes = "Required";
}
return errors;
}}
>
Best Stooge?
{" "}
Larry
{" "}
Moe
{" "}
Curly
Notes
);
render( , document.getElementById("root"));
================================================
FILE: examples/wizard/package.json
================================================
{
"name": "Wizard-Example",
"version": "1.0.0",
"description": "An example of a wizard form created with React Final Form",
"keywords": [
"state management",
"react-final-form",
"form",
"final-form",
"react"
],
"main": "index.js",
"dependencies": {
"prop-types": "latest",
"styled-components": "latest",
"react-final-form": "6.5.3",
"react": "latest",
"react-dom": "latest",
"react-scripts": "3.0.1",
"final-form": "4.20.4",
"prop-types": "latest"
},
"scripts": {
"start": "react-scripts start",
"build": "react-scripts build"
},
"browserslist": [
">0.2%",
"not dead",
"not ie <= 11",
"not op_mini all"
]
}
================================================
FILE: examples/wizard/readme.md
================================================
# Wizard Form Example
[](https://codesandbox.io/s/github/final-form/react-final-form/tree/master/examples/wizard?fontsize=14)
================================================
FILE: package-scripts.js
================================================
const npsUtils = require("nps-utils");
const series = npsUtils.series;
const concurrent = npsUtils.concurrent;
const rimraf = npsUtils.rimraf;
const crossEnv = npsUtils.crossEnv;
module.exports = {
scripts: {
test: {
default: crossEnv("NODE_ENV=test jest --coverage"),
update: crossEnv("NODE_ENV=test jest --coverage --updateSnapshot"),
watch: crossEnv("NODE_ENV=test jest --watch"),
codeCov: crossEnv(
"cat ./coverage/lcov.info | ./node_modules/codecov.io/bin/codecov.io.js",
),
size: {
description: "check the size of the bundle",
script: "size-limit",
},
},
build: {
description: "delete the dist directory and run all builds",
default: series(
rimraf("dist"),
concurrent.nps(
"build.es",
"build.cjs",
"build.umd.main",
"build.umd.min",
),
"tsc --project tsconfig.build.json",
'echo \'{"name":"react-final-form","main":"react-final-form.cjs.js","module":"react-final-form.es.js","types":"index.d.ts"}\' > dist/package.json',
),
es: {
description: "run the build with rollup (uses rollup.config.js)",
script: "rollup --config --environment FORMAT:es",
},
cjs: {
description: "run rollup build with CommonJS format",
script: "rollup --config --environment FORMAT:cjs",
},
umd: {
min: {
description: "run the rollup build with sourcemaps",
script: "rollup --config --sourcemap --environment MINIFY,FORMAT:umd",
},
main: {
description: "builds the cjs and umd files",
script: "rollup --config --sourcemap --environment FORMAT:umd",
},
},
andTest: series.nps("build", "test.size"),
},
docs: {
description: "Generates table of contents in README",
script: "doctoc README.md",
},
prettier: {
description: "Runs prettier on everything",
script: 'prettier --write "**/*.([jt]s*)"',
},
lint: {
description: "lint the entire project",
script: "eslint .",
},
typescript: {
default: {
description: "typescript type checking",
script: "tsc --noEmit",
},
definitions: {
description: "typescript definition tests",
script: "cd typescript && tsc --noEmit --skipLibCheck",
},
},
validate: {
description:
"This runs several scripts to make sure things look good before committing or on clean install",
default: series(
"nps build.andTest",
concurrent.nps("lint", "typescript", "typescript.definitions", "test"),
),
},
},
options: {
silent: false,
},
};
================================================
FILE: package.json
================================================
{
"name": "react-final-form",
"version": "7.0.0",
"description": "🏁 High performance subscription-based form state management for React",
"main": "dist/react-final-form.cjs.js",
"jsnext:main": "dist/react-final-form.es.js",
"module": "dist/react-final-form.es.js",
"typings": "dist/index.d.ts",
"files": [
"dist"
],
"scripts": {
"start": "nps",
"test": "nps test",
"precommit": "lint-staged && npm start validate",
"build": "yarn start build",
"prepublish": "yarn start validate",
"setup": "yarn install"
},
"author": "Erik Rasmussen (http://github.com/erikras)",
"license": "MIT",
"repository": {
"type": "git",
"url": "https://github.com/final-form/react-final-form.git"
},
"bugs": {
"url": "https://github.com/final-form/react-final-form/issues"
},
"homepage": "https://github.com/final-form/react-final-form#readme",
"devDependencies": {
"@babel/core": "^7.27.1",
"@babel/plugin-syntax-dynamic-import": "^7.8.3",
"@babel/plugin-syntax-import-meta": "^7.10.4",
"@babel/plugin-transform-react-jsx-source": "^7.27.1",
"@babel/plugin-transform-runtime": "^7.27.1",
"@babel/preset-env": "^7.27.2",
"@babel/preset-react": "^7.27.1",
"@babel/preset-typescript": "^7.27.1",
"@rollup/plugin-babel": "^6.0.4",
"@rollup/plugin-commonjs": "^25.0.8",
"@rollup/plugin-json": "^6.1.0",
"@rollup/plugin-node-resolve": "^15.2.4",
"@rollup/plugin-replace": "^5.0.7",
"@rollup/plugin-terser": "^0.4.4",
"@testing-library/dom": "^10.4.0",
"@testing-library/jest-dom": "^6.6.3",
"@testing-library/react": "^16.3.0",
"@types/node": "^20.17.50",
"@types/react": "^19.1.5",
"@types/react-dom": "^19.1.5",
"@typescript-eslint/eslint-plugin": "^8.32.1",
"@typescript-eslint/parser": "^8.32.1",
"babel-core": "^7.0.0-bridge.0",
"babel-eslint": "^10.1.0",
"babel-jest": "^29.7.0",
"@size-limit/preset-small-lib": "^11.1.6",
"size-limit": "^11.1.6",
"doctoc": "^2.2.1",
"dtslint": "^4.2.1",
"eslint": "^9.27.0",
"eslint-config-react-app": "^7.0.1",
"eslint-plugin-babel": "^5.3.1",
"eslint-plugin-import": "^2.31.0",
"eslint-plugin-jsx-a11y": "^6.10.2",
"eslint-plugin-react": "^7.37.5",
"eslint-plugin-react-hooks": "^5.2.0",
"fast-deep-equal": "^3.1.3",
"final-form": "5.0.0",
"husky": "^9.1.7",
"jest": "^29.7.0",
"jest-environment-jsdom": "^29.7.0",
"jest-mock-console": "^2.0.0",
"lint-staged": "^15.2.11",
"nps": "^5.10.0",
"nps-utils": "^1.7.0",
"opencollective": "^1.0.3",
"prettier": "^3.5.3",
"prettier-eslint-cli": "^8.0.1",
"react": "^19.1.0",
"react-dom": "^19.1.0",
"rollup": "^3.29.5",
"tar": "^7.4.3",
"ts-essentials": "^10.0.4",
"tslint": "^6.1.3",
"typescript": "^5.8.3"
},
"peerDependencies": {
"final-form": "^5.0.0",
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
},
"lint-staged": {
"*.{js*,ts*,json,md,css}": [
"prettier --write",
"git add"
]
},
"jest": {
"testEnvironment": "jsdom",
"testPathIgnorePatterns": [
"/node_modules/",
"/typescript/"
]
},
"size-limit": [
{
"path": "dist/react-final-form.umd.min.js",
"limit": "4kB"
},
{
"path": "dist/react-final-form.es.js",
"limit": "4kB"
},
{
"path": "dist/react-final-form.cjs.js",
"limit": "4kB"
}
],
"collective": {
"type": "opencollective",
"url": "https://opencollective.com/final-form"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/final-form"
},
"dependencies": {
"@babel/runtime": "^7.15.4"
}
}
================================================
FILE: rollup.config.mjs
================================================
import { nodeResolve } from "@rollup/plugin-node-resolve";
import babel from "@rollup/plugin-babel";
import commonjs from "@rollup/plugin-commonjs";
import json from "@rollup/plugin-json";
import terser from "@rollup/plugin-terser";
import replace from "@rollup/plugin-replace";
// import pkg from "./package.json" assert { type: "json" };
// Hardcode version to avoid import issues
const pkg = {
version: "6.5.9",
dependencies: {
"@babel/runtime": "^7.15.4",
},
peerDependencies: {
"final-form": "^5.0.0-0",
react: "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
},
};
const makeExternalPredicate = (externalArr) => {
if (externalArr.length === 0) {
return () => false;
}
const pattern = new RegExp(`^(${externalArr.join("|")})($|/)`);
return (id) => pattern.test(id);
};
const minify = process.env.MINIFY;
const format = process.env.FORMAT;
const es = format === "es";
const umd = format === "umd";
const cjs = format === "cjs";
let output;
if (es) {
output = { file: `dist/react-final-form.es.js`, format: "es" };
} else if (umd) {
if (minify) {
output = {
file: `dist/react-final-form.umd.min.js`,
format: "umd",
};
} else {
output = { file: `dist/react-final-form.umd.js`, format: "umd" };
}
} else if (cjs) {
output = { file: `dist/react-final-form.cjs.js`, format: "cjs" };
} else if (format) {
throw new Error(`invalid format specified: "${format}".`);
} else {
throw new Error("no format specified. --environment FORMAT:xxx");
}
export default {
input: "src/index.ts",
output: Object.assign(
{
name: "react-final-form",
exports: "named",
globals: {
react: "React",
"final-form": "FinalForm",
},
},
output,
),
external: makeExternalPredicate(
umd
? Object.keys(pkg.peerDependencies || {})
: [
...Object.keys(pkg.dependencies || {}),
...Object.keys(pkg.peerDependencies || {}),
],
),
plugins: [
nodeResolve({
extensions: [".js", ".jsx", ".ts", ".tsx"],
}),
json(),
commonjs(),
babel({
exclude: "node_modules/**",
extensions: [".js", ".jsx", ".ts", ".tsx"],
plugins: [["@babel/plugin-transform-runtime", { useESModules: !cjs }]],
babelHelpers: "runtime",
}),
umd
? replace({
"process.env.NODE_ENV": JSON.stringify(
minify ? "production" : "development",
),
preventAssignment: true,
})
: null,
minify ? terser() : null,
].filter(Boolean),
};
================================================
FILE: src/Field.test.js
================================================
import React from "react";
import { render, fireEvent, cleanup, act } from "@testing-library/react";
import "@testing-library/jest-dom";
import { ErrorBoundary, Toggle, wrapWith } from "./testUtils";
import Form from "./ReactFinalForm";
import Field from "./Field";
const onSubmitMock = (_values) => {};
const timeout = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
async function sleep(ms) {
await act(async () => {
await timeout(ms);
});
}
describe("Field", () => {
afterEach(cleanup);
it("should warn if not used inside a form", () => {
jest.spyOn(console, "error").mockImplementation(() => {});
const errorSpy = jest.fn();
render(
,
);
expect(errorSpy).toHaveBeenCalled();
expect(errorSpy).toHaveBeenCalledTimes(1);
expect(errorSpy.mock.calls[0][0].message).toBe(
"useField must be used inside of a component",
);
console.error.mockRestore();
});
it("should resubscribe if name changes", () => {
const { getByTestId, getByText } = render(
{(isCat) => (
{({ handleSubmit }) => (
)}
)}
,
);
expect(getByTestId("name").value).toBe("Odie");
fireEvent.click(getByText("Toggle"));
expect(getByTestId("name").value).toBe("Garfield");
fireEvent.click(getByText("Toggle"));
expect(getByTestId("name").value).toBe("Odie");
});
it("should render via children render function", () => {
const { getByTestId } = render(
{() => (
{({ input }) => }
)}
,
);
expect(getByTestId("name")).toBeDefined();
});
it("should render via render prop function", () => {
const { getByTestId } = render(
{() => (
}
/>
)}
,
);
expect(getByTestId("name")).toBeDefined();
});
it("should include children when rendering via render prop function", () => {
const { getByTestId } = render(
{() => (
(
{children}
)}
>
Red
Green
Blue
)}
,
);
expect(getByTestId("color")).toBeDefined();
expect(getByTestId("red")).toBeDefined();
expect(getByTestId("green")).toBeDefined();
expect(getByTestId("blue")).toBeDefined();
});
it("should unsubscribe on unmount", () => {
// This is mainly here for code coverage. 🧐
const { getByText } = render(
{(hidden) => (
{({ handleSubmit }) => (
{!hidden && (
)}
)}
)}
,
);
fireEvent.click(getByText("Toggle"));
});
it("should focus, change, and blur", () => {
const spy = jest.fn();
const { getByTestId } = render(
{() => (
{wrapWith(spy, ({ input }) => (
))}
)}
,
);
expect(spy).toHaveBeenCalledTimes(2);
expect(spy.mock.calls[0][0].meta.active).toBe(false);
expect(spy.mock.calls[0][0].input.value).toBe("");
fireEvent.focus(getByTestId("name"));
expect(spy).toHaveBeenCalledTimes(3);
expect(spy.mock.calls[2][0].meta.active).toBe(true);
expect(spy.mock.calls[2][0].input.value).toBe("");
fireEvent.change(getByTestId("name"), { target: { value: "erikras" } });
expect(spy).toHaveBeenCalledTimes(4);
expect(spy.mock.calls[3][0].meta.active).toBe(true);
expect(spy.mock.calls[3][0].input.value).toBe("erikras");
fireEvent.blur(getByTestId("name"));
expect(spy).toHaveBeenCalledTimes(5);
expect(spy.mock.calls[4][0].meta.active).toBe(false);
expect(spy.mock.calls[4][0].input.value).toBe("erikras");
});
it("should convert '' to undefined on change", () => {
const spy = jest.fn();
const { getByTestId } = render(
{wrapWith(spy, () => (
))}
,
);
expect(spy).toHaveBeenCalled();
expect(spy).toHaveBeenCalledTimes(2);
expect(spy.mock.calls[0][0].values).toEqual({});
fireEvent.change(getByTestId("name"), { target: { value: "erikras" } });
expect(spy).toHaveBeenCalledTimes(3);
expect(spy.mock.calls[2][0].values).toEqual({ name: "erikras" });
fireEvent.change(getByTestId("name"), { target: { value: "" } });
expect(spy).toHaveBeenCalledTimes(4);
expect(spy.mock.calls[3][0].values).toEqual({});
});
it("should accept an identity parse prop to preserve empty strings", () => {
const spy = jest.fn();
const { getByTestId } = render(
{wrapWith(spy, () => (
v}>
{({ input: { value, ...props } }) => (
)}
))}
,
);
expect(spy).toHaveBeenCalled();
expect(spy).toHaveBeenCalledTimes(2);
expect(spy.mock.calls[0][0].values).toEqual({});
fireEvent.change(getByTestId("name"), { target: { value: "erikras" } });
expect(spy).toHaveBeenCalledTimes(3);
expect(spy.mock.calls[2][0].values).toEqual({ name: "erikras" });
fireEvent.change(getByTestId("name"), { target: { value: "" } });
expect(spy).toHaveBeenCalledTimes(4);
expect(spy.mock.calls[3][0].values).toEqual({ name: "" });
});
it("should accept a format function prop", () => {
const spy = jest.fn();
const { getByTestId } = render(
{wrapWith(spy, () => (
(value ? value.toUpperCase() : "")}
data-testid="name"
/>
))}
,
);
expect(spy).toHaveBeenCalled();
expect(spy).toHaveBeenCalledTimes(2);
expect(spy.mock.calls[0][0].values).toEqual({});
fireEvent.change(getByTestId("name"), { target: { value: "erikras" } });
expect(spy).toHaveBeenCalledTimes(3);
expect(spy.mock.calls[2][0].values).toEqual({ name: "erikras" });
expect(getByTestId("name").value).toBe("ERIKRAS");
});
it("should only format on blur if formatOnBlur is true", () => {
const format = jest.fn((value) => (value ? value.toUpperCase() : ""));
const { getByTestId } = render(
{() => (
)}
,
);
fireEvent.focus(getByTestId("name"));
expect(getByTestId("name").value).toBe("");
fireEvent.change(getByTestId("name"), { target: { value: "erikras" } });
expect(getByTestId("name").value).toBe("erikras");
expect(format).not.toHaveBeenCalled();
fireEvent.blur(getByTestId("name"));
expect(format).toHaveBeenCalled();
expect(format).toHaveBeenCalledTimes(1);
expect(getByTestId("name").value).toBe("ERIKRAS");
});
it("should `formatOnBlur` most updated value", () => {
const format = jest.fn((value) => (value ? value.trim() : ""));
const { getByTestId } = render(
{() => (
{({ input }) => (
{
input.onChange(
e.target.value && e.target.value.toUpperCase(),
);
input.onBlur(e);
}}
/>
)}
)}
,
);
const inputText = " erikras";
fireEvent.focus(getByTestId("name"));
expect(getByTestId("name").value).toBe("");
fireEvent.change(getByTestId("name"), { target: { value: inputText } });
expect(getByTestId("name").value).toBe(inputText);
fireEvent.blur(getByTestId("name"));
expect(format.mock.calls[0][0]).toBe(inputText.toUpperCase());
expect(getByTestId("name").value).toBe(inputText.trim().toUpperCase());
});
it("should not format value at all when formatOnBlur and render prop", () => {
const format = jest.fn((value) => (value ? value.toUpperCase() : ""));
render(
{() => (
{({ input }) => {
expect(input.value).toBeUndefined();
expect(format).not.toHaveBeenCalled();
return ;
}}
)}
,
);
});
it("should accept an identity format prop to preserve undefined values", () => {
const spy = jest.fn();
const { getByTestId } = render(
{() => (
v}>
{wrapWith(spy, ({ input: { value, ...props } }) => (
))}
)}
,
);
expect(spy).toHaveBeenCalled();
expect(spy).toHaveBeenCalledTimes(2);
expect(spy.mock.calls[0][0].input.value).toBeUndefined();
fireEvent.change(getByTestId("name"), { target: { value: "erikras" } });
expect(spy).toHaveBeenCalledTimes(3);
expect(spy.mock.calls[2][0].input.value).toBe("erikras");
fireEvent.change(getByTestId("name"), { target: { value: "" } });
expect(spy).toHaveBeenCalledTimes(4);
expect(spy.mock.calls[3][0].input.value).toBeUndefined();
});
it("should provide a value of [] when empty on a select multiple", () => {
const { getByTestId } = render(
{() => (
)}
,
);
// This test is mostly for code coverage. Is there a way to assure that the value prop
// passed to the is []?
expect(getByTestId("name").value).toBe("");
});
it("should pass multiple through to custom components", () => {
const CustomSelect = jest.fn(({ input }) => (
));
render(
{() => (
)}
,
);
expect(CustomSelect).toHaveBeenCalled();
expect(CustomSelect).toHaveBeenCalledTimes(2);
expect(CustomSelect.mock.calls[0][0].input.multiple).toBe(true);
});
it("should pass ref through to the input", () => {
const ref = React.createRef();
render(
{() => (
)}
,
);
expect(ref.current).not.toBe(null);
expect(ref.current instanceof HTMLInputElement).toBe(true);
});
it("should not pass an undefined type through to the input", () => {
const MyInput = jest.fn(({ input }) => );
render(
{() => (
)}
,
);
expect(MyInput).toHaveBeenCalled();
expect(MyInput).toHaveBeenCalledTimes(2);
expect(MyInput.mock.calls[0][0].input).not.toHaveProperty("type");
});
it("should optionally allow null values", () => {
const spy = jest.fn();
const { getByTestId } = render(
{() => (
{wrapWith(spy, ({ input: { value, ...props } }) => (
))}
)}
,
);
expect(spy).toHaveBeenCalled();
expect(spy).toHaveBeenCalledTimes(2);
expect(spy.mock.calls[1][0].input.value).toBe(null); // Check second render pass for correct state
fireEvent.change(getByTestId("name"), { target: { value: "erikras" } });
expect(spy).toHaveBeenCalledTimes(3);
expect(spy.mock.calls[2][0].input.value).toBe("erikras");
act(() => {
spy.mock.calls[2][0].input.onChange(null);
});
expect(spy).toHaveBeenCalledTimes(4);
expect(spy.mock.calls[3][0].input.value).toBe(null);
});
it("should not allow null values when allowNull not true", () => {
const spy = jest.fn();
const { getByTestId } = render(
{() => (
{wrapWith(spy, ({ input: { value, ...props } }) => (
))}
)}
,
);
expect(spy).toHaveBeenCalled();
expect(spy).toHaveBeenCalledTimes(2);
expect(spy.mock.calls[0][0].input.value).toBe("");
fireEvent.change(getByTestId("name"), { target: { value: "erikras" } });
expect(spy).toHaveBeenCalledTimes(3);
expect(spy.mock.calls[2][0].input.value).toBe("erikras");
act(() => {
spy.mock.calls[2][0].input.onChange(null);
});
expect(spy).toHaveBeenCalledTimes(4);
expect(spy.mock.calls[3][0].input.value).toBe("");
});
it("should not let validate prop bleed through", () => {
const spy = jest.fn();
render(
{() => (
{wrapWith(spy, ({ input }) => (
))}
)}
,
);
expect(spy).toHaveBeenCalled();
expect(spy).toHaveBeenCalledTimes(2);
expect(spy.mock.calls[0][0].validate).toBeUndefined();
});
it("should not let subscription prop bleed through", () => {
const spy = jest.fn();
render(
{() => (
{wrapWith(spy, ({ input }) => (
))}
)}
,
);
expect(spy).toHaveBeenCalled();
expect(spy).toHaveBeenCalledTimes(2);
expect(spy.mock.calls[0][0].subscription).toBeUndefined();
});
it("should allow changing field-level validation function", () => {
const simpleValidate = (value) => (value ? undefined : "Required");
const complexValidate = (value) => {
if (value) {
if (value !== value.toUpperCase()) {
return "SHOULD BE UPPERCASE!";
}
} else {
return "Required";
}
};
const { getByTestId, getByText } = render(
{(useComplexValidation) => (
{({ handleSubmit }) => (
{({ input, meta }) => (
)}
)}
)}
,
);
expect(getByTestId("error")).toHaveTextContent("Required");
fireEvent.change(getByTestId("name"), { target: { value: "erikras" } });
expect(getByTestId("error")).toHaveTextContent("");
fireEvent.click(getByText("Toggle"));
expect(getByTestId("error")).toHaveTextContent("SHOULD BE UPPERCASE!");
fireEvent.change(getByTestId("name"), { target: { value: "ERIKRAS" } });
expect(getByTestId("error")).toHaveTextContent("");
});
/**
* Allow me to explain this. If we allow field level validation functions
* to be swapped, it means that we'd have to run _ALL_ the validation
* every time a new field was removed regardless of whether or
* not it was using field-level validation. To avoid this overhead, we must
* accept some inconsistency when swapping of field-level validation functions.
* In this test, swapping from no validation to "required" validation
* does work because the field-level validation function is called on mount,
* but the error does not clear when we switch back to no validation function
* because in order for Final Form to determine if the 'Required' error came
* from the newly unmounted Field, it would need to run validation on the entire
* form.
*/
it("should ignore changes field-level validation function", () => {
const createValidator = (isRequired) =>
isRequired ? (value) => (value ? undefined : "Required") : undefined;
const Error = ({ name }) => (
{({ meta: { error } }) => {error}
}
);
const { getByTestId, getByText } = render(
{(isRequired) => (
{({ handleSubmit }) => (
{({ input, meta }) => (
)}
)}
)}
,
);
expect(getByTestId("error")).toBeEmptyDOMElement();
expect(getByTestId("error2")).toBeEmptyDOMElement();
fireEvent.click(getByText("Toggle"));
expect(getByTestId("error")).toHaveTextContent("Required");
expect(getByTestId("error2")).toHaveTextContent("Required");
fireEvent.click(getByText("Toggle"));
// ERROR IS NOT CLEARED (see comment above)
expect(getByTestId("error")).toHaveTextContent("Required");
expect(getByTestId("error2")).toHaveTextContent("Required");
});
it("should not rerender if validateFields is !== every time", () => {
// https://github.com/final-form/react-final-form/issues/502
const required = (value) => (value ? undefined : "Required");
const spy = jest.fn();
const { getByTestId } = render(
{({ handleSubmit }) => (
{wrapWith(spy, ({ input, meta }) => (
))}
)}
,
);
// first render registered validation, second contains error
expect(spy).toHaveBeenCalledTimes(2);
expect(getByTestId("error")).toHaveTextContent("Required");
});
it("should pass along type prop", () => {
const { getByTestId } = render(
{({ handleSubmit }) => (
)}
,
);
expect(getByTestId("checkbox").type).toBe("checkbox");
expect(getByTestId("password").type).toBe("password");
expect(getByTestId("radio").type).toBe("radio");
});
it("should render checkboxes with checked prop", () => {
const { getByTestId } = render(
{({ handleSubmit }) => (
)}
,
);
expect(getByTestId("employed").type).toBe("checkbox");
expect(getByTestId("employed").checked).toBe(false);
fireEvent.change(getByTestId("employed"), { target: { checked: true } });
expect(getByTestId("employed").checked).toBe(true);
});
it('should render "array" checkboxes with checked prop when value is included in array', () => {
const { getByTestId } = render(
{({ handleSubmit }) => (
)}
,
);
expect(getByTestId("red").checked).toBe(true);
expect(getByTestId("green").checked).toBe(false);
expect(getByTestId("blue").checked).toBe(true);
});
it('should render "array" custom checkboxes with checked prop when value is included in array', () => {
const red = jest.fn();
const green = jest.fn();
const blue = jest.fn();
render(
{({ handleSubmit }) => (
{wrapWith(red, ({ input }) => (
))}
{wrapWith(green, ({ input }) => (
))}
{wrapWith(blue, ({ input }) => (
))}
)}
,
);
expect(red).toHaveBeenCalled();
expect(red).toHaveBeenCalledTimes(2);
// After fix #1050, initialValues work on first render
expect(red.mock.calls[0][0].input.checked).toBe(true); // Correctly true for "red" from initialValues
expect(red.mock.calls[1][0].input.checked).toBe(true);
expect(green).toHaveBeenCalled();
expect(green).toHaveBeenCalledTimes(2);
expect(green.mock.calls[0][0].input.checked).toBe(false);
expect(green.mock.calls[1][0].input.checked).toBe(false); // Correctly false for "green"
expect(blue).toHaveBeenCalled();
expect(blue).toHaveBeenCalledTimes(2);
// After fix #1050, initialValues work on first render
expect(blue.mock.calls[0][0].input.checked).toBe(true); // Correctly true for "blue" from initialValues
expect(blue.mock.calls[1][0].input.checked).toBe(true);
});
it("should render radio buttons with checked prop", () => {
const { getByTestId } = render(
{({ handleSubmit }) => (
)}
,
);
expect(getByTestId("red").type).toBe("radio");
expect(getByTestId("red").checked).toBe(false);
expect(getByTestId("green").type).toBe("radio");
expect(getByTestId("green").checked).toBe(true);
expect(getByTestId("blue").type).toBe("radio");
expect(getByTestId("blue").checked).toBe(false);
});
it("should render custom radio component with checked prop", () => {
const red = jest.fn();
const green = jest.fn();
const blue = jest.fn();
render(
{({ handleSubmit }) => (
{wrapWith(red, ({ input }) => (
))}
{wrapWith(green, ({ input }) => (
))}
{wrapWith(blue, ({ input }) => (
))}
)}
,
);
expect(red).toHaveBeenCalled();
expect(red).toHaveBeenCalledTimes(2);
expect(red.mock.calls[0][0].input.checked).toBe(false);
expect(red.mock.calls[1][0].input.checked).toBe(false); // Correctly false for "red" radio
expect(green).toHaveBeenCalled();
expect(green).toHaveBeenCalledTimes(2);
// After fix #1050, initialValues work on first render
expect(green.mock.calls[0][0].input.checked).toBe(true); // Correctly true for "green" from initialValues
expect(green.mock.calls[1][0].input.checked).toBe(true);
expect(blue).toHaveBeenCalled();
expect(blue).toHaveBeenCalledTimes(2);
expect(blue.mock.calls[0][0].input.checked).toBe(false);
expect(blue.mock.calls[1][0].input.checked).toBe(false); // Correctly false for "blue" radio
});
it("should use isEqual to calculate dirty/pristine", () => {
const isEqual = (a, b) => (a && a.toUpperCase()) === (b && b.toUpperCase());
const { getByTestId } = render(
{() => (
{({ input, meta }) => (
)}
)}
,
);
expect(getByTestId("input").value).toBe("bob");
expect(getByTestId("dirty")).toHaveTextContent("Pristine");
fireEvent.change(getByTestId("input"), { target: { value: "bobby" } });
expect(getByTestId("dirty")).toHaveTextContent("Dirty");
fireEvent.change(getByTestId("input"), { target: { value: "BOB" } });
expect(getByTestId("dirty")).toHaveTextContent("Pristine");
});
it("should be able to use inline isEqual to calculate dirty/pristine without falling into infinite rerender loop", () => {
const { getByTestId } = render(
{() => (
(a && a.toUpperCase()) === (b && b.toUpperCase())
}
>
{({ input, meta }) => (
)}
)}
,
);
expect(getByTestId("input").value).toBe("bob");
expect(getByTestId("dirty")).toHaveTextContent("Pristine");
fireEvent.change(getByTestId("input"), { target: { value: "bobby" } });
expect(getByTestId("dirty")).toHaveTextContent("Dirty");
fireEvent.change(getByTestId("input"), { target: { value: "BOB" } });
expect(getByTestId("dirty")).toHaveTextContent("Pristine");
});
it("should only call each field-level validation once upon initial mount", () => {
const fooValidate = jest.fn();
const barValidate = jest.fn();
const bazValidate = jest.fn();
render(
{() => (
)}
,
);
expect(fooValidate).toHaveBeenCalledTimes(1);
expect(barValidate).toHaveBeenCalledTimes(1);
expect(bazValidate).toHaveBeenCalledTimes(1);
});
it("should warn when used without type prop and rendering radio, checkbox or multiple select indirectly", () => {
const errorSpy = jest.spyOn(console, "error").mockImplementation(() => {});
const { getByTestId } = render(
{() => (
{({ input }) => (
)}
{({ input }) => (
)}
{({ input }) => (
{"Option"}
)}
{"Option"}
)}
,
);
// After fix #1050, initialValues work on first render, so select multiple
// correctly gets the array value from initialValues and no longer triggers
// React's "must be an array" warning
expect(errorSpy).toHaveBeenCalledTimes(0);
// Reset the spy to test the actual Field warnings
errorSpy.mockClear();
fireEvent.click(getByTestId("checkbox"), {
target: { type: "checkbox", checked: true },
});
expect(errorSpy).toHaveBeenCalledTimes(1);
expect(errorSpy.mock.calls[0][0]).toBe(
'You must pass `type="checkbox"` prop to your Field(checkboxInput) component.\n' +
'Without it we don\'t know how to unpack your `value` prop - "checkboxValue".',
);
fireEvent.click(getByTestId("radio"), {
target: { type: "radio", value: "radio value" },
});
expect(errorSpy).toHaveBeenCalledTimes(2);
expect(errorSpy.mock.calls[1][0]).toBe(
'You must pass `type="radio"` prop to your Field(radioInput) component.\n' +
'Without it we don\'t know how to unpack your `value` prop - "radioValue".',
);
fireEvent.change(getByTestId("select"), {
target: { value: ["some value"] },
});
expect(errorSpy).toHaveBeenCalledTimes(3);
expect(errorSpy.mock.calls[2][0]).toBe(
'You must pass `type="select"` prop to your Field(selectMultipleInput) component.\n' +
"Without it we don't know how to unpack your `value` prop - [].",
);
fireEvent.change(getByTestId("selectMultipleWithoutRenderProp"), {
target: { value: ["some value"] },
});
// error not given, since we can deduce that it's a "select"
expect(errorSpy).toHaveBeenCalledTimes(3);
errorSpy.mockRestore();
});
it("should formatOnBlur on submit", () => {
const onSubmit = jest.fn();
const { getByTestId, getByText } = render(
{({ handleSubmit }) => (
value && value.toUpperCase()}
formatOnBlur
data-testid="name"
/>
Submit
)}
,
);
expect(getByTestId("name").value).toBe("");
fireEvent.focus(getByTestId("name"));
fireEvent.change(getByTestId("name"), { target: { value: "erik" } });
expect(getByTestId("name").value).toBe("erik");
fireEvent.blur(getByTestId("name"));
expect(getByTestId("name").value).toBe("ERIK");
fireEvent.focus(getByTestId("name"));
fireEvent.change(getByTestId("name"), { target: { value: "ERIKras" } });
expect(getByTestId("name").value).toBe("ERIKras");
expect(onSubmit).not.toHaveBeenCalled();
fireEvent.click(getByText("Submit"));
expect(onSubmit).toHaveBeenCalled();
expect(onSubmit).toHaveBeenCalledTimes(1);
expect(onSubmit.mock.calls[0][0]).toEqual({ name: "ERIKRAS" });
// submit again with no need for format
fireEvent.click(getByText("Submit"));
expect(onSubmit).toHaveBeenCalledTimes(2);
expect(onSubmit.mock.calls[1][0]).toEqual({ name: "ERIKRAS" });
});
it("should allow submission to be cancelled in beforeSubmit", () => {
const onSubmit = jest.fn();
const beforeSubmit = jest.fn(() => false);
const { getByTestId, getByText } = render(
{({ handleSubmit }) => (
Submit
)}
,
);
expect(getByTestId("name").value).toBe("");
fireEvent.focus(getByTestId("name"));
fireEvent.change(getByTestId("name"), { target: { value: "erik" } });
expect(onSubmit).not.toHaveBeenCalled();
expect(beforeSubmit).not.toHaveBeenCalled();
fireEvent.click(getByText("Submit"));
expect(onSubmit).not.toHaveBeenCalled();
expect(beforeSubmit).toHaveBeenCalled();
});
it("update validating flag on async field-level validation", async () => {
const { getByTestId } = render(
{({ handleSubmit }) => (
{
await timeout(5);
return value === "erikras" ? "Username taken" : undefined;
}}
data-testid="name"
/>
{({ meta: { validating } }) => (
{validating === true ? "Spinner" : "Not Validating"}
)}
Submit
)}
,
);
expect(getByTestId("validating")).toHaveTextContent("Spinner");
await sleep(6);
expect(getByTestId("name").value).toBe("");
fireEvent.focus(getByTestId("name"));
expect(getByTestId("validating")).toHaveTextContent("Not Validating");
fireEvent.change(getByTestId("name"), { target: { value: "erik" } });
expect(getByTestId("validating")).toHaveTextContent("Spinner");
await sleep(6);
expect(getByTestId("validating")).toHaveTextContent("Not Validating");
fireEvent.change(getByTestId("name"), { target: { value: "erikras" } });
expect(getByTestId("validating")).toHaveTextContent("Spinner");
await sleep(6);
expect(getByTestId("validating")).toHaveTextContent("Not Validating");
});
it("not call record-level validation on Field mount", () => {
const validate = jest.fn();
const { getByText } = render(
{(showOtherFields) => (
{({ handleSubmit }) => (
{showOtherFields && (
)}
)}
)}
,
);
expect(validate).toHaveBeenCalledTimes(1);
fireEvent.click(getByText("Toggle"));
expect(validate).toHaveBeenCalledTimes(1);
});
it("submit should not throw when field with enabled `formatOnBlur` changes name `prop`", () => {
const onSubmit = jest.fn();
const trim = (value) => value && value.trim();
const { getByTestId, getByText } = render(
{({ handleSubmit }) => (
{(newFieldName) => (
Submit
)}
)}
,
);
fireEvent.click(getByText("Toggle"));
fireEvent.change(getByTestId("field"), {
target: { value: "trailing space " },
});
fireEvent.click(getByText("Submit"));
expect(onSubmit).toHaveBeenCalled();
expect(onSubmit.mock.calls[0][0]).toEqual({ newName: "trailing space" });
});
it("should throw an error if name prop is undefined", () => {
const consoleError = console.error;
console.error = jest.fn(); // Suppress React error boundary warning
expect(() => {
render(
{() => } />}
,
);
}).toThrow("prop name cannot be undefined in component");
console.error = consoleError;
});
it("should support using format/parse with radio controls", () => {
const format = (value) => value && value.toString();
const parse = (value) => value && parseInt(value, 10);
const { getByTestId } = render(
{({ handleSubmit }) => (
)}
,
);
expect(getByTestId("ten").checked).toBe(false);
expect(getByTestId("twenty").checked).toBe(true);
expect(getByTestId("thirty").checked).toBe(false);
});
it("should support using format/parse with checkbox controls", () => {
const format = (value) => value && value.map((x) => x.toString());
const parse = (value) => value && value.map((x) => parseInt(x, 10));
const { getByTestId } = render(
{({ handleSubmit }) => (
)}
,
);
expect(getByTestId("ten").checked).toBe(false);
expect(getByTestId("twenty").checked).toBe(true);
expect(getByTestId("thirty").checked).toBe(true);
});
});
describe("Field.nodeName issue #871", () => {
it("should not crash when field name is 'nodeName'", () => {
const onSubmit = jest.fn();
const { getByTestId } = render(
{({ handleSubmit }) => (
Submit
)}
,
);
const input = getByTestId("nodeName-input");
const submit = getByTestId("submit");
// Should not crash when interacting with the field
expect(() => {
fireEvent.change(input, { target: { value: "test" } });
fireEvent.blur(input);
fireEvent.click(submit);
}).not.toThrow();
// Verify the field value was set correctly
expect(onSubmit).toHaveBeenCalledWith(
{ nodeName: "test" },
expect.any(Object),
expect.any(Function),
);
});
});
================================================
FILE: src/Field.tsx
================================================
import * as React from "react";
import type { FieldProps, FieldRenderProps } from "./types";
import renderComponent from "./renderComponent";
import useField from "./useField";
function FieldComponent<
FieldValue = any,
T extends HTMLElement = HTMLElement,
FormValues = Record,
>(
{
afterSubmit,
allowNull,
beforeSubmit,
children,
component,
data,
defaultValue,
format,
formatOnBlur,
initialValue,
input,
isEqual,
multiple,
name,
parse,
subscription,
type,
validate,
validateFields,
value,
...rest
}: FieldProps,
ref: React.Ref,
) {
const field: FieldRenderProps = useField(name, {
afterSubmit,
allowNull,
beforeSubmit,
component,
data,
defaultValue,
format,
formatOnBlur,
initialValue,
isEqual,
multiple,
parse,
subscription,
type,
validate,
validateFields,
value,
});
// Merge provided input prop with field.input
const mergedField = input
? { ...field, input: { ...field.input, ...input } }
: field;
if (typeof children === "function") {
return (
children as (
props: FieldRenderProps & typeof rest,
) => React.ReactNode
)({ ...mergedField, ...rest });
}
if (typeof component === "string") {
// ignore meta, combine input with any other props
const { name: inputName, ...restInputProps } = mergedField.input;
// Ensure multiple select has array value
if (
component === "select" &&
multiple &&
!Array.isArray(restInputProps.value)
) {
restInputProps.value = [] as any;
}
return React.createElement(component, {
name: inputName, // Pass name explicitly to avoid shadowing DOM properties
...restInputProps,
children,
ref,
...rest,
});
}
if (!name) {
throw new Error("prop name cannot be undefined in component");
}
return renderComponent(
{ children, component, ...rest, ...mergedField },
{},
`Field(${name})`,
);
}
// Create a properly typed forwardRef component that preserves generics
const Field = React.forwardRef(FieldComponent as any) as <
FieldValue = any,
T extends HTMLElement = HTMLElement,
FormValues = Record,
>(
props: FieldProps & { ref?: React.Ref },
) => React.ReactElement | null;
export default Field;
================================================
FILE: src/FormSpy.test.js
================================================
import React from "react";
import { render, fireEvent, cleanup } from "@testing-library/react";
import "@testing-library/jest-dom";
import { ErrorBoundary, Toggle, wrapWith } from "./testUtils";
import Form from "./ReactFinalForm";
import Field from "./Field";
import FormSpy from "./FormSpy";
const onSubmitMock = (_values) => {};
const hasFormApi = (props) => {
expect(props.form).toBeDefined();
expect(typeof props.form.batch).toBe("function");
expect(typeof props.form.blur).toBe("function");
expect(typeof props.form.change).toBe("function");
expect(typeof props.form.focus).toBe("function");
expect(typeof props.form.initialize).toBe("function");
expect(typeof props.form.reset).toBe("function");
};
describe("FormSpy", () => {
afterEach(cleanup);
it("should warn if not used inside a form", () => {
jest.spyOn(console, "error").mockImplementation(() => {});
const errorSpy = jest.fn();
render(
} />
,
);
expect(errorSpy).toHaveBeenCalled();
expect(errorSpy).toHaveBeenCalledTimes(1);
expect(errorSpy.mock.calls[0][0].message).toBe(
"FormSpy must be used inside of a component",
);
console.error.mockRestore();
});
it("should allow subscribing to everything", () => {
const spy = jest.fn();
const { getByTestId } = render(
{() => (
(
))}
/>
)}
,
);
expect(spy).toHaveBeenCalled();
// All forms without restricted subscriptions render twice at first because they
// need to update their validation and touched/modified/visited maps every time
// new fields are registered.
expect(spy).toHaveBeenCalledTimes(2);
hasFormApi(spy.mock.calls[0][0]);
expect(spy.mock.calls[0][0].dirty).toBe(false);
expect(spy.mock.calls[0][0].errors).toEqual({});
expect(spy.mock.calls[0][0].invalid).toBe(false);
expect(spy.mock.calls[0][0].pristine).toBe(true);
expect(spy.mock.calls[0][0].submitFailed).toBe(false);
expect(spy.mock.calls[0][0].submitSucceeded).toBe(false);
expect(spy.mock.calls[0][0].submitting).toBe(false);
expect(spy.mock.calls[0][0].valid).toBe(true);
expect(spy.mock.calls[0][0].validating).toBe(false);
expect(spy.mock.calls[0][0].values).toEqual({});
hasFormApi(spy.mock.calls[1][0]);
expect(spy.mock.calls[1][0].dirty).toBe(false);
expect(spy.mock.calls[1][0].errors).toEqual({});
expect(spy.mock.calls[1][0].invalid).toBe(false);
expect(spy.mock.calls[1][0].pristine).toBe(true);
expect(spy.mock.calls[1][0].submitFailed).toBe(false);
expect(spy.mock.calls[1][0].submitSucceeded).toBe(false);
expect(spy.mock.calls[1][0].submitting).toBe(false);
expect(spy.mock.calls[1][0].valid).toBe(true);
expect(spy.mock.calls[1][0].validating).toBe(false);
expect(spy.mock.calls[1][0].values).toEqual({});
// change value
fireEvent.change(getByTestId("name"), { target: { value: "erikras" } });
expect(spy).toHaveBeenCalledTimes(3);
hasFormApi(spy.mock.calls[2][0]);
expect(spy.mock.calls[2][0].dirty).toBe(true);
expect(spy.mock.calls[2][0].errors).toEqual({});
expect(spy.mock.calls[2][0].invalid).toBe(false);
expect(spy.mock.calls[2][0].pristine).toBe(false);
expect(spy.mock.calls[2][0].submitFailed).toBe(false);
expect(spy.mock.calls[2][0].submitSucceeded).toBe(false);
expect(spy.mock.calls[2][0].submitting).toBe(false);
expect(spy.mock.calls[2][0].valid).toBe(true);
expect(spy.mock.calls[2][0].validating).toBe(false);
expect(spy.mock.calls[2][0].values).toEqual({ name: "erikras" });
});
it("should NOT resubscribe if subscription changes", () => {
const firstSubscription = { values: true, pristine: true };
const secondSubscription = { dirty: true, submitting: true };
const spy = jest.fn();
const { getByText } = render(
{(useAlternateSubscription) => (
{() => (
{wrapWith(spy, (props) => (
))}
)}
)}
,
);
expect(spy).toHaveBeenCalled();
// All forms without restricted subscriptions render twice at first because they
// need to update their validation and touched/modified/visited maps every time
// new fields are registered.
expect(spy).toHaveBeenCalledTimes(2);
hasFormApi(spy.mock.calls[0][0]);
expect(spy.mock.calls[0][0].dirty).toBe(false);
expect(spy.mock.calls[0][0].errors).toEqual({});
expect(spy.mock.calls[0][0].invalid).toBe(false);
expect(spy.mock.calls[0][0].pristine).toBe(true);
expect(spy.mock.calls[0][0].submitFailed).toBe(false);
expect(spy.mock.calls[0][0].submitSucceeded).toBe(false);
expect(spy.mock.calls[0][0].submitting).toBe(false);
expect(spy.mock.calls[0][0].valid).toBe(true);
expect(spy.mock.calls[0][0].validating).toBe(false);
expect(spy.mock.calls[0][0].values).toEqual({
dog: "Odie",
cat: "Garfield",
});
// Reinstate check for spy.mock.calls[1][0] as there are two initial calls
hasFormApi(spy.mock.calls[1][0]);
expect(spy.mock.calls[1][0].dirty).toBeUndefined();
expect(spy.mock.calls[1][0].errors).toBeUndefined();
expect(spy.mock.calls[1][0].invalid).toBeUndefined();
expect(spy.mock.calls[1][0].pristine).toBe(true);
expect(spy.mock.calls[1][0].submitFailed).toBeUndefined();
expect(spy.mock.calls[1][0].submitSucceeded).toBeUndefined();
expect(spy.mock.calls[1][0].submitting).toBeUndefined();
expect(spy.mock.calls[1][0].valid).toBeUndefined();
expect(spy.mock.calls[1][0].validating).toBeUndefined();
expect(spy.mock.calls[1][0].values).toEqual({
dog: "Odie",
cat: "Garfield",
});
fireEvent.click(getByText("Toggle"));
// If FormSpy correctly does not re-render for subscription prop change alone,
// count should remain 3.
expect(spy).toHaveBeenCalledTimes(3);
});
it("should hear changes", () => {
const spy = jest.fn();
const { getByTestId } = render(
{() => (
{wrapWith(spy, (props) => (
))}
)}
,
);
expect(spy).toHaveBeenCalled();
// All forms without restricted subscriptions render twice at first because they
// need to update their validation and touched/modified/visited maps every time
// new fields are registered.
expect(spy).toHaveBeenCalledTimes(2);
hasFormApi(spy.mock.calls[0][0]);
expect(spy.mock.calls[0][0].dirty).toBe(false);
expect(spy.mock.calls[0][0].errors).toEqual({});
expect(spy.mock.calls[0][0].invalid).toBe(false);
expect(spy.mock.calls[0][0].pristine).toBe(true);
expect(spy.mock.calls[0][0].submitFailed).toBe(false);
expect(spy.mock.calls[0][0].submitSucceeded).toBe(false);
expect(spy.mock.calls[0][0].submitting).toBe(false);
expect(spy.mock.calls[0][0].valid).toBe(true);
expect(spy.mock.calls[0][0].values).toEqual({}); // Default/empty for first call
// Check second initial call
hasFormApi(spy.mock.calls[1][0]);
expect(spy.mock.calls[1][0].dirty).toBe(false);
expect(spy.mock.calls[1][0].errors).toBeUndefined();
expect(spy.mock.calls[1][0].invalid).toBeUndefined();
expect(spy.mock.calls[1][0].pristine).toBeUndefined();
expect(spy.mock.calls[1][0].values).toEqual({});
fireEvent.change(getByTestId("name"), { target: { value: "bob" } });
expect(spy).toHaveBeenCalledTimes(3); // Called again for the change
hasFormApi(spy.mock.calls[2][0]);
expect(spy.mock.calls[2][0].dirty).toBe(true);
expect(spy.mock.calls[2][0].errors).toBeUndefined();
expect(spy.mock.calls[2][0].invalid).toBeUndefined();
expect(spy.mock.calls[2][0].pristine).toBeUndefined();
expect(spy.mock.calls[2][0].submitFailed).toBeUndefined();
expect(spy.mock.calls[2][0].submitSucceeded).toBeUndefined();
expect(spy.mock.calls[2][0].submitting).toBeUndefined();
expect(spy.mock.calls[2][0].valid).toBeUndefined();
expect(spy.mock.calls[2][0].values).toEqual({ name: "bob" });
});
it("should unsubscribe on unmount", () => {
// This is mainly here for code coverage. 🧐
const spy = jest.fn();
const { getByText } = render(
{(hidden) => (
{() => (
{!hidden && (
{wrapWith(spy, (props) => (
))}
)}
)}
)}
,
);
expect(spy).toHaveBeenCalled();
// All forms without restricted subscriptions render twice at first because they
// need to update their validation and touched/modified/visited maps every time
// new fields are registered.
expect(spy).toHaveBeenCalledTimes(2);
fireEvent.click(getByText("Toggle"));
expect(spy).toHaveBeenCalledTimes(2);
});
it("should call onChange", () => {
const spy = jest.fn();
const { getByTestId } = render(
{() => (
)}
,
);
expect(spy).toHaveBeenCalled();
expect(spy).toHaveBeenCalledTimes(1);
expect(spy.mock.calls[0][0].dirty).toBe(false);
expect(spy.mock.calls[0][0].errors).toBeUndefined();
expect(spy.mock.calls[0][0].invalid).toBeUndefined();
expect(spy.mock.calls[0][0].pristine).toBeUndefined();
expect(spy.mock.calls[0][0].submitFailed).toBeUndefined();
expect(spy.mock.calls[0][0].submitSucceeded).toBeUndefined();
expect(spy.mock.calls[0][0].submitting).toBeUndefined();
expect(spy.mock.calls[0][0].valid).toBeUndefined();
expect(spy.mock.calls[0][0].validating).toBeUndefined();
expect(spy.mock.calls[0][0].values).toEqual({});
fireEvent.change(getByTestId("name"), { target: { value: "erikras" } });
expect(spy).toHaveBeenCalledTimes(2);
expect(spy.mock.calls[1][0].dirty).toBe(true);
expect(spy.mock.calls[1][0].values).toEqual({ name: "erikras" });
});
it("should not rerender when onChange changes, but SHOULD use latest onChange passed", () => {
const spy = jest.fn();
const { getByTestId } = render(
{({ values }) => (
{
spy(values.name, formState.values.name);
}}
/>
)}
,
);
expect(spy).toHaveBeenCalled();
expect(spy).toHaveBeenCalledTimes(1);
expect(spy.mock.calls[0]).toEqual([undefined, undefined]);
fireEvent.change(getByTestId("name"), { target: { value: "erikras" } });
expect(spy).toHaveBeenCalledTimes(2);
expect(spy.mock.calls[1]).toEqual(["erikras", "erikras"]);
fireEvent.change(getByTestId("name"), {
target: { value: "erikras rulez" },
});
expect(spy).toHaveBeenCalledTimes(3);
expect(spy.mock.calls[2]).toEqual(["erikras rulez", "erikras rulez"]);
});
it("should not render with render prop when given onChange", () => {
const spy = jest.fn();
const renderSpy = jest.fn();
render(
{() => (
)}
,
);
expect(spy).toHaveBeenCalled();
expect(spy).toHaveBeenCalledTimes(1);
expect(renderSpy).not.toHaveBeenCalled();
});
it("should ignore SyntheticEvents on form reset ", () => {
const { getByTestId, getByText } = render(
{() => (
{({ form }) => (
Reset
)}
)}
,
);
expect(getByTestId("name").value).toBe("erikras");
fireEvent.change(getByTestId("name"), {
target: { value: "erikrasmussen" },
});
expect(getByTestId("name").value).toBe("erikrasmussen");
fireEvent.click(getByText("Reset"));
expect(getByTestId("name").value).toBe("erikras");
});
it("should accept new initial values on form reset ", () => {
const { getByTestId, getByText } = render(
{() => (
{({ form }) => (
form.reset({ name: "bob" })}
>
Reset
)}
)}
,
);
expect(getByTestId("name").value).toBe("erikras");
fireEvent.change(getByTestId("name"), {
target: { value: "erikrasmussen" },
});
expect(getByTestId("name").value).toBe("erikrasmussen");
fireEvent.click(getByText("Reset"));
expect(getByTestId("name").value).toBe("bob");
});
it("should allow subscription to be undefined", () => {
// Implementation of this test case is not provided in the original file or the code block
// This test case is assumed to exist based on the code block
});
it("should not throw error when subscribing to active (fix #1055)", () => {
// Issue #1055: Cannot set property active of # which has only a getter
// This was caused by spreading lazy state with getter-only properties into renderProps
const spy = jest.fn();
const { container, getByTestId } = render(
{() => (
{
// Verify that we can access active and values without error
const { active, values } = props;
return active={String(active)} values={JSON.stringify(values)}
;
})}
/>
)}
,
);
// Should render without throwing "Cannot set property active" error
expect(container).toBeTruthy();
expect(spy).toHaveBeenCalled();
// Verify that active and values properties exist and can be accessed
const props = spy.mock.calls[spy.mock.calls.length - 1][0];
expect("active" in props).toBe(true);
expect("values" in props).toBe(true);
// Active should be undefined initially (no field focused)
expect(props.active).toBeUndefined();
// Values should be defined (empty object initially)
expect(props.values).toBeDefined();
expect(props.values).toEqual({});
// When we focus a field, active should be the field name
fireEvent.focus(getByTestId("name"));
expect(spy.mock.calls[spy.mock.calls.length - 1][0].active).toBe("name");
});
});
================================================
FILE: src/FormSpy.tsx
================================================
import renderComponent from "./renderComponent";
import type { FormSpyProps, FormSpyRenderProps } from "./types";
import isSyntheticEvent from "./isSyntheticEvent";
import useForm from "./useForm";
import useFormState from "./useFormState";
function FormSpy>({
onChange,
subscription,
...rest
}: FormSpyProps): React.ReactElement | null {
const reactFinalForm = useForm("FormSpy");
const state = useFormState({ onChange, subscription });
if (onChange) {
return null;
}
const renderProps: FormSpyRenderProps = {
form: {
...reactFinalForm,
reset: (eventOrValues?: any) => {
if (isSyntheticEvent(eventOrValues)) {
// it's a React SyntheticEvent, call reset with no arguments
reactFinalForm.reset();
} else {
reactFinalForm.reset(eventOrValues);
}
},
},
} as FormSpyRenderProps;
return renderComponent(
{
...rest,
...renderProps,
},
state,
"FormSpy",
) as React.ReactElement;
}
export default FormSpy;
================================================
FILE: src/ReactFinalForm.test.js
================================================
import React from "react";
import { render, fireEvent, cleanup, act } from "@testing-library/react";
import "@testing-library/jest-dom";
import deepEqual from "fast-deep-equal";
import { ErrorBoundary, Toggle, wrapWith } from "./testUtils";
import { createForm } from "final-form";
import { Form, Field, version, withTypes } from ".";
const onSubmitMock = (_values) => {};
const timeout = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
async function sleep(ms) {
await act(async () => {
await timeout(ms);
});
}
describe("ReactFinalForm", () => {
afterEach(cleanup);
it("should export version", () => {
expect(version).toBeDefined();
});
it("should export withTypes", () => {
// mostly for code coverage
expect(withTypes).toBeDefined();
expect(withTypes()).toBeDefined();
});
it("should render with render function", () => {
const { getByTestId } = render(
}
/>,
);
expect(getByTestId("myDiv")).toBeDefined();
});
it("should render with children render function", () => {
const { getByTestId } = render(
{() =>
} ,
);
expect(getByTestId("myDiv")).toBeDefined();
});
it("should print a warning with no render or children specified", () => {
jest.spyOn(console, "error").mockImplementation(() => {});
const errorSpy = jest.fn();
render(
,
);
expect(errorSpy).toHaveBeenCalled();
expect(errorSpy).toHaveBeenCalledTimes(1);
expect(errorSpy.mock.calls[0][0].message).toBe(
"Must specify either a render prop, a render function as children, or a component prop to ReactFinalForm",
);
console.error.mockRestore();
});
it("should print a warning with no onSubmit specified", () => {
jest.spyOn(console, "error").mockImplementation(() => {});
const errorSpy = jest.fn();
render(
} />
,
);
expect(errorSpy).toHaveBeenCalled();
expect(errorSpy).toHaveBeenCalledTimes(1);
expect(errorSpy.mock.calls[0][0].message).toBe(
"No onSubmit function specified",
);
console.error.mockRestore();
});
it("should allow render to be a component", () => {
const Component = () =>
;
const { getByTestId } = render(
,
);
expect(getByTestId("myDiv")).toBeDefined();
});
it("should unsubscribe on unmount", () => {
// This is mainly here for code coverage. 🧐
const Container = () => {
const [shown, setShown] = React.useState(true);
return (
{shown &&
} />}
setShown(false)}>
Unmount
);
};
const { getByText } = render(
,
);
fireEvent.click(getByText("Unmount"));
});
it("should render with a field", () => {
const formRender = jest.fn();
const fieldRender = jest.fn();
const { getByTestId } = render(
{wrapWith(formRender, () => (
{wrapWith(fieldRender, ({ input, meta }) => (
<>
{meta.active ? "active" : "inactive"}
>
))}
))}
,
);
const firstName = getByTestId("firstName");
const active = getByTestId("firstNameActive");
expect(firstName).toBeDefined();
expect(active).toBeDefined();
expect(firstName.value).toBe("");
expect(active).toHaveTextContent("inactive");
// All forms render twice at first because they need to update their validation
// and touched/modified/visited maps every time new fields are registered.
expect(formRender).toHaveBeenCalledTimes(2);
expect(fieldRender).toHaveBeenCalledTimes(2);
fireEvent.focus(firstName);
expect(formRender).toHaveBeenCalledTimes(3);
expect(fieldRender).toHaveBeenCalledTimes(3);
expect(active).toHaveTextContent("active");
fireEvent.change(firstName, { target: { value: "E" } });
expect(formRender).toHaveBeenCalledTimes(4);
expect(fieldRender).toHaveBeenCalledTimes(4);
expect(firstName.value).toBe("E");
fireEvent.change(firstName, { target: { value: "Er" } });
expect(formRender).toHaveBeenCalledTimes(5);
expect(fieldRender).toHaveBeenCalledTimes(5);
fireEvent.change(firstName, { target: { value: "Eri" } });
expect(formRender).toHaveBeenCalledTimes(6);
expect(fieldRender).toHaveBeenCalledTimes(6);
fireEvent.change(firstName, { target: { value: "Erik" } });
expect(formRender).toHaveBeenCalledTimes(7);
expect(fieldRender).toHaveBeenCalledTimes(7);
expect(active).toHaveTextContent("active");
fireEvent.blur(firstName);
expect(formRender).toHaveBeenCalledTimes(8);
expect(fieldRender).toHaveBeenCalledTimes(8);
expect(active).toHaveTextContent("inactive");
});
it("should render with a field with a limited subscription", () => {
const formRender = jest.fn();
const fieldRender = jest.fn();
const { getByTestId } = render(
{wrapWith(formRender, () => (
{wrapWith(fieldRender, ({ input, meta }) => (
<>
{meta.active ? "active" : "inactive"}
>
))}
))}
,
);
const firstName = getByTestId("firstName");
const active = getByTestId("firstNameActive");
expect(firstName).toBeDefined();
expect(active).toBeDefined();
expect(firstName.value).toBe("");
expect(active).toHaveTextContent("inactive");
expect(formRender).toHaveBeenCalledTimes(2);
expect(fieldRender).toHaveBeenCalledTimes(2);
fireEvent.focus(firstName);
// not subscribing to active, so no rerender!
expect(formRender).toHaveBeenCalledTimes(2);
expect(fieldRender).toHaveBeenCalledTimes(2);
expect(active).toHaveTextContent("inactive");
fireEvent.change(firstName, { target: { value: "E" } });
expect(formRender).toHaveBeenCalledTimes(2);
expect(fieldRender).toHaveBeenCalledTimes(3);
expect(firstName.value).toBe("E");
fireEvent.change(firstName, { target: { value: "Er" } });
expect(formRender).toHaveBeenCalledTimes(2);
expect(fieldRender).toHaveBeenCalledTimes(4);
fireEvent.change(firstName, { target: { value: "Eri" } });
expect(formRender).toHaveBeenCalledTimes(2);
expect(fieldRender).toHaveBeenCalledTimes(5);
fireEvent.change(firstName, { target: { value: "Erik" } });
expect(formRender).toHaveBeenCalledTimes(2);
expect(fieldRender).toHaveBeenCalledTimes(6);
expect(active).toHaveTextContent("inactive");
fireEvent.blur(firstName);
// no rerender
expect(formRender).toHaveBeenCalledTimes(2);
expect(fieldRender).toHaveBeenCalledTimes(6);
expect(active).toHaveTextContent("inactive");
});
it("should call onSubmit when form is submitted", () => {
const onSubmit = jest.fn();
const { getByTestId, getByText } = render(
{({ handleSubmit }) => (
Submit
)}
,
);
fireEvent.change(getByTestId("firstName"), { target: { value: "Erik" } });
fireEvent.change(getByTestId("lastName"), {
target: { value: "Rasmussen" },
});
expect(onSubmit).not.toHaveBeenCalled();
fireEvent.click(getByText("Submit"));
expect(onSubmit).toHaveBeenCalled();
expect(onSubmit).toHaveBeenCalledTimes(1);
expect(onSubmit.mock.calls[0][0]).toEqual({
firstName: "Erik",
lastName: "Rasmussen",
});
});
// it('should not throw when handleSubmit is called with no event', () => {
// const onSubmit = jest.fn()
// const { getByTestId, getByText } = render(
//
// {({ handleSubmit }) => (
// {
// handleSubmit(values)
// }}
// >
//
//
// Submit
//
// )}
//
// )
// fireEvent.change(getByTestId('firstName'), { target: { value: 'Erik' } })
// fireEvent.change(getByTestId('lastName'), {
// target: { value: 'Rasmussen' }
// })
// expect(onSubmit).not.toHaveBeenCalled()
// fireEvent.click(getByText('Submit'))
// expect(onSubmit).toHaveBeenCalled()
// expect(onSubmit).toHaveBeenCalledTimes(1)
// expect(onSubmit.mock.calls[0][0]).toEqual({
// firstName: 'Erik',
// lastName: 'Rasmussen'
// })
// })
it("should reinitialize when initialValues prop changes", () => {
const initialValues = {
name: "Dr. Jekyll",
};
const alternateInitialValues = {
name: "Mr. Hyde",
};
const { getByTestId, getByText } = render(
{(useAlternateInitialValues) => (
{({ handleSubmit }) => (
)}
)}
,
);
expect(getByTestId("name").value).toBe("Dr. Jekyll");
fireEvent.change(getByTestId("name"), { target: { value: "Dr. Watson" } });
expect(getByTestId("name").value).toBe("Dr. Watson");
fireEvent.click(getByText("Toggle"));
expect(getByTestId("name").value).toBe("Mr. Hyde");
});
it("should NOT reinitialize when initialValues prop doesn't change (shallowly) but rerendered", () => {
const { getByTestId, getByText } = render(
{(useAlternateInitialValues) => (
{({ handleSubmit }) => (
)}
)}
,
);
expect(getByTestId("name").value).toBe("Dr. Jekyll");
fireEvent.change(getByTestId("name"), { target: { value: "Dr. Watson" } });
expect(getByTestId("name").value).toBe("Dr. Watson");
fireEvent.click(getByText("Toggle"));
expect(getByTestId("name").value).toBe("Dr. Watson");
});
it("should reinitialize when initialValues prop changes, deeply", () => {
const initialValues = {
professor: {
name: "Dr. Jekyll",
},
};
const alternateInitialValues = {
professor: {
name: "Mr. Hyde",
},
};
const { getByTestId, getByText } = render(
{(useAlternateInitialValues) => (
{({ handleSubmit }) => (
)}
)}
,
);
expect(getByTestId("name").value).toBe("Dr. Jekyll");
fireEvent.change(getByTestId("name"), { target: { value: "Dr. Watson" } });
expect(getByTestId("name").value).toBe("Dr. Watson");
fireEvent.click(getByText("Toggle"));
expect(getByTestId("name").value).toBe("Mr. Hyde");
});
it("should not reinitialize if initialValues prop is deep equal", () => {
const { getByTestId, getByText } = render(
{(useAlternateInitialValues) => (
{({ handleSubmit }) => (
)}
)}
,
);
expect(getByTestId("name").value).toBe("Dr. Jekyll");
fireEvent.change(getByTestId("name"), { target: { value: "Dr. Watson" } });
expect(getByTestId("name").value).toBe("Dr. Watson");
fireEvent.click(getByText("Toggle"));
expect(getByTestId("name").value).toBe("Dr. Watson");
});
it("should respect keepDirtyOnReinitialize prop when initialValues prop changes", () => {
const initialValues = {
name: "Dr. Jekyll",
};
const alternateInitialValues = {
name: "Mr. Hyde",
};
const { getByTestId, getByText } = render(
{(useAlternateInitialValues) => (
{({ handleSubmit }) => (
)}
)}
,
);
expect(getByTestId("name").value).toBe("Dr. Jekyll");
fireEvent.change(getByTestId("name"), { target: { value: "Dr. Watson" } });
expect(getByTestId("name").value).toBe("Dr. Watson");
fireEvent.click(getByText("Toggle"));
expect(getByTestId("name").value).toBe("Dr. Watson");
});
it("should update when onSubmit changes", () => {
const firstOnSubmit = jest.fn();
const secondOnSubmit = jest.fn();
const { getByTestId, getByText } = render(
{(useAlternateOnSubmit) => (
{({ handleSubmit }) => (
Submit
)}
)}
,
);
fireEvent.change(getByTestId("name"), { target: { value: "Erik" } });
expect(firstOnSubmit).not.toHaveBeenCalled();
fireEvent.click(getByText("Submit"));
expect(firstOnSubmit).toHaveBeenCalled();
expect(firstOnSubmit).toHaveBeenCalledTimes(1);
expect(firstOnSubmit.mock.calls[0][0]).toEqual({
name: "Erik",
});
expect(secondOnSubmit).not.toHaveBeenCalled();
fireEvent.click(getByText("Toggle"));
expect(firstOnSubmit).toHaveBeenCalledTimes(1);
expect(secondOnSubmit).not.toHaveBeenCalled();
fireEvent.click(getByText("Submit"));
expect(firstOnSubmit).toHaveBeenCalledTimes(1);
expect(secondOnSubmit).toHaveBeenCalled();
expect(secondOnSubmit).toHaveBeenCalledTimes(1);
expect(secondOnSubmit.mock.calls[0][0]).toEqual({
name: "Erik",
});
});
it("should warn if decorators change", () => {
const decoratorA = (form) => () => {};
const decoratorB = (form) => () => {};
const decoratorC = (form) => () => {};
const oldDecorators = [decoratorA, decoratorB];
const newDecorators = [decoratorA, decoratorB, decoratorC];
const errorSpy = jest.spyOn(console, "error").mockImplementation(() => {});
const { getByText } = render(
{(useAlternateDecorators) => (
{({ handleSubmit }) => (
Submit
)}
)}
,
);
expect(errorSpy).not.toHaveBeenCalled();
fireEvent.click(getByText("Toggle"));
expect(errorSpy).toHaveBeenCalled();
expect(errorSpy).toHaveBeenCalledWith(
"Form decorators should not change from one render to the next as new values will be ignored",
);
errorSpy.mockRestore();
});
it("should return a promise from handleSubmit when submission is async", () => {
const { getByTestId, getByText } = render(
{
await timeout(2);
}}
>
{({ handleSubmit }) => (
{
const promise = handleSubmit(event);
expect(promise).toBeDefined();
expect(typeof promise.then).toBe("function");
}}
>
Submit
)}
,
);
fireEvent.change(getByTestId("name"), { target: { value: "Erik" } });
fireEvent.click(getByText("Submit"));
});
it("should ignore SyntheticEvents on form reset ", () => {
const { getByTestId, getByText } = render(
{({ handleSubmit, form }) => (
Reset
)}
,
);
expect(getByTestId("name").value).toBe("John");
fireEvent.change(getByTestId("name"), { target: { value: "Paul" } });
expect(getByTestId("name").value).toBe("Paul");
fireEvent.click(getByText("Reset"));
expect(getByTestId("name").value).toBe("John");
});
it("should accept new initial values on form reset ", () => {
const { getByTestId, getByText } = render(
{({ form }) => (
form.reset({ name: "bob" })}>
Reset
)}
,
);
expect(getByTestId("name").value).toBe("erikras");
fireEvent.change(getByTestId("name"), {
target: { value: "erikrasmussen" },
});
expect(getByTestId("name").value).toBe("erikrasmussen");
fireEvent.click(getByText("Reset"));
expect(getByTestId("name").value).toBe("bob");
});
it("should use decorators, and unsubscribe them on unmount", () => {
const unsubscribe = jest.fn();
const decorator = jest.fn(() => unsubscribe);
const { getByText } = render(
{(hideForm) =>
!hideForm && (
{({ handleSubmit }) => (
)}
)
}
,
);
expect(decorator).toHaveBeenCalled();
expect(decorator).toHaveBeenCalledTimes(1);
expect(unsubscribe).not.toHaveBeenCalled();
fireEvent.click(getByText("Toggle"));
expect(unsubscribe).toHaveBeenCalled();
});
it("should all record level validation function to change", () => {
const simpleValidation = (values) => {
const errors = {};
if (!values.name) {
errors.name = "Required";
}
return errors;
};
const complexValidation = (values) => {
const errors = {};
if (!values.name) {
errors.name = "Required";
} else if (values.name.toUpperCase() !== values.name) {
errors.name = "SHOULD BE SHOUTING";
}
return errors;
};
const { getByTestId, getByText } = render(
{(useComplexValidation) => (
{() => (
{({ input, meta }) => (
)}
)}
)}
,
);
expect(getByTestId("name").value).toBe("");
expect(getByTestId("error")).toHaveTextContent("Required");
fireEvent.change(getByTestId("name"), { target: { value: "erikras" } });
expect(getByTestId("error")).toHaveTextContent("");
fireEvent.click(getByText("Toggle"));
expect(getByTestId("error")).toHaveTextContent("SHOULD BE SHOUTING");
fireEvent.change(getByTestId("name"), { target: { value: "ERIKRAS" } });
expect(getByTestId("error")).toHaveTextContent("");
});
it("should show form as invalid on second rerender if field-level validation errors are present", () => {
// Debugging https://github.com/final-form/react-final-form/issues/196
const spy = jest.fn();
render(
{({ handleSubmit, invalid }) => {
spy(invalid);
return (
"Required"}
/>
);
}}
,
);
// On first render, we cannot know about any field level validation rules
// because none of the fields have yet had a chance to render and register.
expect(spy).toHaveBeenCalledTimes(2);
expect(spy.mock.calls[0][0]).toBe(false);
expect(spy.mock.calls[1][0]).toBe(true);
});
it("should work with server-side rendering", () => {
const errorSpy = jest.spyOn(console, "error").mockImplementation(() => {});
render(
(
)}
/>,
);
expect(errorSpy).not.toHaveBeenCalled();
errorSpy.mockRestore();
});
it("should allow change to debug flag", () => {
// this is mostly for code coverage 😰
const debugMock = jest.fn();
const { getByText } = render(
{(debug) => (
{({ handleSubmit }) => (
)}
)}
,
);
fireEvent.click(getByText("Toggle"));
});
it("should allow change to destroyOnUnregister flag", () => {
// this is mostly for code coverage 😰
const { getByText } = render(
{(destroyOnUnregister) => (
{({ handleSubmit }) => (
)}
)}
,
);
fireEvent.click(getByText("Toggle"));
});
it("should allow change to keepDirtyOnReinitialize flag", () => {
// this is mostly for code coverage 😰
const { getByText } = render(
{(keepDirtyOnReinitialize) => (
{({ handleSubmit }) => (
)}
)}
,
);
fireEvent.click(getByText("Toggle"));
});
it("should allow change to validateOnBlur flag", () => {
// this is mostly for code coverage 😰
const { getByText } = render(
{(validateOnBlur) => (
{({ handleSubmit }) => (
)}
)}
,
);
fireEvent.click(getByText("Toggle"));
});
it("should allow change to mutators", () => {
// this is mostly for code coverage 😰
const oldMutators = undefined;
const newMutators = {
clearField: ([name], state, { changeValue }) => {
changeValue(state, name, () => undefined);
},
};
const spy = jest.fn();
const { getByTestId, getByText } = render(
{(swapMutators) => (
{wrapWith(spy, ({ handleSubmit, form }) => (
form.mutators.clearField("name")}
>
Clear Name
))}
)}
,
);
expect(getByTestId("name").value).toBe("");
fireEvent.change(getByTestId("name"), { target: { value: "erikras" } });
expect(getByTestId("name").value).toBe("erikras");
fireEvent.click(getByText("Toggle"));
expect(getByTestId("name").value).toBe("erikras");
fireEvent.click(getByText("Clear Name"));
expect(getByTestId("name").value).toBe("");
});
it("should allow handleSubmit to be called with an object that's not an event", () => {
const onSubmitMock = jest.fn();
const { getByText } = render(
{({ handleSubmit }) => (
{
handleSubmit({});
event.preventDefault(); // so react-testing-library doesn't freak out
}}
>
Submit
)}
,
);
expect(onSubmitMock).not.toHaveBeenCalled();
fireEvent.click(getByText("Submit"));
expect(onSubmitMock).toHaveBeenCalled();
expect(onSubmitMock).toHaveBeenCalledTimes(1);
expect(onSubmitMock.mock.calls[0][0]).toEqual({ name: "erikras" });
});
it("should allow handleSubmit to be called with no parameters", () => {
const onSubmitMock = jest.fn();
const { getByText } = render(
{({ handleSubmit }) => (
{
handleSubmit();
event.preventDefault(); // so react-testing-library doesn't freak out
}}
>
Submit
)}
,
);
expect(onSubmitMock).not.toHaveBeenCalled();
fireEvent.click(getByText("Submit"));
expect(onSubmitMock).toHaveBeenCalled();
expect(onSubmitMock).toHaveBeenCalledTimes(1);
expect(onSubmitMock.mock.calls[0][0]).toEqual({ name: "erikras" });
});
it("should set submitting back to false after submit", async () => {
const onSubmit = jest.fn(async () => {
await timeout(1);
});
const recordSubmitting = jest.fn();
const { getByText } = render(
{({ handleSubmit, submitting }) => {
recordSubmitting(submitting);
return (
Submit
);
}}
,
);
expect(onSubmit).not.toHaveBeenCalled();
expect(recordSubmitting).toHaveBeenCalled();
expect(recordSubmitting).toHaveBeenCalledTimes(2);
expect(recordSubmitting.mock.calls[0][0]).toBe(false);
fireEvent.click(getByText("Submit"));
expect(onSubmit).toHaveBeenCalled();
expect(onSubmit).toHaveBeenCalledTimes(1);
expect(recordSubmitting).toHaveBeenCalledTimes(3);
expect(recordSubmitting.mock.calls[2][0]).toBe(true);
await sleep(5);
expect(recordSubmitting).toHaveBeenCalledTimes(4);
expect(recordSubmitting.mock.calls[3][0]).toBe(false);
});
it("should allow an alternative form api to be passed in", () => {
const onSubmit = jest.fn();
const form = createForm({ onSubmit: onSubmit });
const formMock = jest.spyOn(form, "registerField");
render(
{({ handleSubmit }) => (
)}
,
);
expect(formMock).toHaveBeenCalled();
// called once on first render to get initial state, and then again to subscribe
expect(formMock).toHaveBeenCalledTimes(1);
expect(formMock.mock.calls[0][0]).toBe("name");
expect(formMock.mock.calls[0][2].active).toBe(true); // default subscription
});
it("should not destroy on unregister on initial unregister", () => {
// https://github.com/final-form/react-final-form/issues/523
const { getByTestId } = render(
{({ handleSubmit }) => (
)}
,
);
expect(getByTestId("name")).toBeDefined();
expect(getByTestId("name").value).toBe("erikras");
fireEvent.focus(getByTestId("name"));
expect(getByTestId("name").value).toBe("erikras");
});
it("should not destroy on unregister on initial register/unregister of new field", () => {
// https://github.com/final-form/react-final-form/issues/523
const { getByTestId, queryByTestId, getByText } = render(
{({ handleSubmit }) => (
{(showPassword) =>
showPassword && (
)
}
)}
,
);
expect(getByTestId("name")).toBeDefined();
expect(getByTestId("name").value).toBe("erikras");
fireEvent.focus(getByTestId("name"));
expect(getByTestId("name").value).toBe("erikras");
expect(queryByTestId("password")).toBe(null);
fireEvent.click(getByText("Toggle"));
expect(getByTestId("password").value).toBe("f1nal-f0rm-RULEZ");
fireEvent.focus(getByTestId("password"));
expect(getByTestId("password").value).toBe("f1nal-f0rm-RULEZ");
});
it("should set submitting back to false when onSubmit returns Promise.resolve() immediately (#903)", async () => {
// Regression test for: https://github.com/final-form/react-final-form/issues/903
// When onSubmit is an async function with no awaits (returns Promise),
// submitting should reset to false after the promise resolves.
const onSubmit = jest.fn(async () => {
// async with no await — resolves in the next microtask, no real async delay
});
const recordSubmitting = jest.fn();
const { getByText } = render(
{({ handleSubmit, submitting }) => {
recordSubmitting(submitting);
return (
Submit
);
}}
,
);
fireEvent.click(getByText("Submit"));
// Wait for the microtask Promise to resolve and React to update
await act(async () => {});
// submitting should have gone true then back to false
const calls = recordSubmitting.mock.calls.map((c) => c[0]);
expect(calls).toContain(true); // was submitting at some point
expect(calls[calls.length - 1]).toBe(false); // ends as not submitting
});
it("should set submitting back to false when onSubmit returns Promise.resolve() (#903)", async () => {
const onSubmit = jest.fn(() => Promise.resolve());
const recordSubmitting = jest.fn();
const { getByText } = render(
{({ handleSubmit, submitting }) => {
recordSubmitting(submitting);
return (
Submit
);
}}
,
);
fireEvent.click(getByText("Submit"));
await act(async () => {});
const calls = recordSubmitting.mock.calls.map((c) => c[0]);
expect(calls[calls.length - 1]).toBe(false);
});
});
describe("Issue #914 – nested Field with validator causing Maximum update depth", () => {
it("should not throw Maximum update depth exceeded when nesting Fields with validators", () => {
// https://github.com/final-form/react-final-form/issues/914
const required = (value) => (value ? undefined : "Required");
expect(() => {
render(
{({ handleSubmit }) => (
{({ input }) => (
{({ input: innerInput }) => (
)}
)}
)}
,
);
}).not.toThrow();
});
});
describe("Issue #850 – Cannot update component while rendering different component", () => {
it("should not warn about updating during render when conditionally rendering Field with initialValue", async () => {
// https://github.com/final-form/react-final-form/issues/850
const warnSpy = jest.spyOn(console, "error").mockImplementation(() => {});
const Toggle = ({ children }) => {
const [show, setShow] = React.useState(false);
return (
setShow((s) => !s)}>Toggle
{show && children}
);
};
const { getByText } = render(
{({ handleSubmit }) => (
)}
,
);
// Toggle the conditional field
await act(async () => {
fireEvent.click(getByText("Toggle"));
});
// Should not have any "Cannot update component while rendering" warnings
const updateWhileRenderingWarnings = warnSpy.mock.calls.filter(
(call) =>
call[0] &&
typeof call[0] === "string" &&
call[0].includes("Cannot update"),
);
expect(updateWhileRenderingWarnings).toHaveLength(0);
warnSpy.mockRestore();
});
});
================================================
FILE: src/ReactFinalForm.tsx
================================================
import * as React from "react";
import {
createForm,
formSubscriptionItems,
version as ffVersion,
} from "final-form";
import type {
FormApi,
Config,
FormSubscription,
FormState,
Unsubscribe,
} from "final-form";
import type { FormProps, SubmitEvent } from "./types";
import renderComponent from "./renderComponent";
import useWhenValueChanges from "./useWhenValueChanges";
import useConstant from "./useConstant";
import shallowEqual from "./shallowEqual";
import isSyntheticEvent from "./isSyntheticEvent";
import type { FormRenderProps } from "./types";
import ReactFinalFormContext from "./context";
import { addLazyFormState } from "./getters";
import { version } from "../package.json";
export { version };
const versions = {
"final-form": ffVersion,
"react-final-form": version,
};
export const all = formSubscriptionItems.reduce(
(result: FormSubscription, key: keyof FormSubscription) => {
result[key] = true;
return result;
},
{},
);
function ReactFinalForm>({
debug,
decorators = [],
destroyOnUnregister,
form: alternateFormApi,
initialValues,
initialValuesEqual,
keepDirtyOnReinitialize,
mutators,
onSubmit,
subscription = all,
validate,
validateOnBlur,
...rest
}: FormProps) {
const config: Config = {
debug,
destroyOnUnregister,
initialValues,
keepDirtyOnReinitialize,
mutators,
onSubmit,
validate,
validateOnBlur,
};
const form: FormApi = useConstant(() => {
const f = alternateFormApi || createForm(config);
// pause validation until children register all fields on first render (unpaused in useEffect() below)
f.pauseValidation();
return f;
});
// Get initial state without triggering callbacks during render
const [state, setState] = React.useState>(() => {
// Get initial state synchronously but without callbacks
return form.getState();
});
// save a copy of state that can break through the closure
// on the shallowEqual() line below.
const stateRef = React.useRef>(state);
stateRef.current = state;
React.useEffect(() => {
// We have rendered, so all fields are now registered, so we can unpause validation
form.isValidationPaused() && form.resumeValidation();
const unsubscriptions: Unsubscribe[] = [
form.subscribe((s) => {
setState((prevState) => {
if (!shallowEqual(s, prevState)) {
return s;
}
return prevState;
});
}, subscription),
...(decorators ? decorators.map((decorator) => decorator(form)) : []),
];
return () => {
form.pauseValidation(); // pause validation so we don't revalidate on every field deregistration
unsubscriptions.reverse().forEach((unsubscribe) => unsubscribe());
// don't need to resume validation here; either unmounting, or will re-run this hook with new deps
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
// warn about decorator changes
// istanbul ignore next
if (process.env.NODE_ENV !== "production") {
// You're never supposed to use hooks inside a conditional, but in this
// case we can be certain that you're not going to be changing your
// NODE_ENV between renders, so this is safe.
// eslint-disable-next-line react-hooks/rules-of-hooks
useWhenValueChanges(
decorators,
() => {
console.error(
"Form decorators should not change from one render to the next as new values will be ignored",
);
},
shallowEqual,
);
}
// allow updatable config
useWhenValueChanges(debug, () => {
form.setConfig("debug", debug);
});
useWhenValueChanges(destroyOnUnregister, () => {
form.destroyOnUnregister = !!destroyOnUnregister;
});
useWhenValueChanges(keepDirtyOnReinitialize, () => {
form.setConfig("keepDirtyOnReinitialize", keepDirtyOnReinitialize);
});
useWhenValueChanges(
initialValues,
() => {
form.setConfig("initialValues", initialValues);
},
initialValuesEqual || shallowEqual,
);
useWhenValueChanges(mutators, () => {
form.setConfig("mutators", mutators);
});
useWhenValueChanges(onSubmit, () => {
form.setConfig("onSubmit", onSubmit);
});
useWhenValueChanges(validate, () => {
form.setConfig("validate", validate);
});
useWhenValueChanges(validateOnBlur, () => {
form.setConfig("validateOnBlur", validateOnBlur);
});
const handleSubmit = (event?: SubmitEvent) => {
if (event) {
// sometimes not true, e.g. React Native
if (typeof event.preventDefault === "function") {
event.preventDefault();
}
if (typeof event.stopPropagation === "function") {
// prevent any outer forms from receiving the event too
event.stopPropagation();
}
}
return form.submit();
};
const renderProps: FormRenderProps = {
form: {
...form,
reset: (eventOrValues?: any) => {
if (isSyntheticEvent(eventOrValues)) {
// it's a React SyntheticEvent, call reset with no arguments
form.reset();
} else {
form.reset(eventOrValues);
}
},
},
handleSubmit,
} as FormRenderProps;
addLazyFormState(renderProps, state);
return React.createElement(
ReactFinalFormContext.Provider,
{ value: form },
renderComponent(
{
...rest,
__versions: versions,
},
renderProps,
"ReactFinalForm",
),
);
}
export default ReactFinalForm;
================================================
FILE: src/context.test.js
================================================
import * as React from "react";
import { render } from "@testing-library/react";
import FormContext from "./context";
import ReactFinalForm from "./ReactFinalForm";
describe("FormContext", () => {
it("should provide form context to children", () => {
const spy = jest.fn();
const TestComponent = () => {
const form = React.useContext(FormContext);
spy(form);
return null;
};
render(
{}}>
{({ form }) => (
)}
,
);
expect(spy).toHaveBeenCalledTimes(1);
expect(spy.mock.calls[0][0]).toBeDefined();
expect(typeof spy.mock.calls[0][0].registerField).toBe("function");
});
it("should have undefined as default value", () => {
const spy = jest.fn();
const TestComponent = () => {
const form = React.useContext(FormContext);
spy(form);
return null;
};
render( );
expect(spy).toHaveBeenCalledTimes(1);
expect(spy.mock.calls[0][0]).toBeUndefined();
});
});
================================================
FILE: src/context.ts
================================================
import * as React from "react";
import type { FormApi } from "final-form";
export default React.createContext | undefined>(undefined);
================================================
FILE: src/getValue.test.js
================================================
import { noop } from "lodash";
import getValue from "./getValue";
describe("getValue", () => {
it("should return event.nativeEvent.text if defined and not react-native", () => {
expect(
getValue(
{
preventDefault: noop,
stopPropagation: noop,
nativeEvent: {
text: "foo",
},
},
undefined,
undefined,
false,
),
).toBe("foo");
});
it("should return event.nativeEvent.text if react-native", () => {
expect(
getValue(
{
preventDefault: noop,
stopPropagation: noop,
nativeEvent: {
text: "foo",
},
},
undefined,
undefined,
true,
),
).toBe("foo");
expect(
getValue(
{
preventDefault: noop,
stopPropagation: noop,
nativeEvent: {
text: undefined,
},
},
undefined,
undefined,
true,
),
).toBe(undefined);
expect(
getValue(
{
preventDefault: noop,
stopPropagation: noop,
nativeEvent: {
text: null,
},
},
undefined,
undefined,
true,
),
).toBe(null);
});
it("should return event.target.checked if checkbox with no value parameter", () => {
expect(
getValue(
{
preventDefault: noop,
stopPropagation: noop,
target: {
type: "checkbox",
checked: true,
},
},
undefined,
undefined,
true,
),
).toBe(true);
expect(
getValue(
{
preventDefault: noop,
stopPropagation: noop,
target: {
type: "checkbox",
checked: true,
},
},
undefined,
undefined,
false,
),
).toBe(true);
expect(
getValue(
{
preventDefault: noop,
stopPropagation: noop,
target: {
type: "checkbox",
checked: undefined,
},
},
undefined,
undefined,
true,
),
).toBe(false);
expect(
getValue(
{
preventDefault: noop,
stopPropagation: noop,
target: {
type: "checkbox",
checked: undefined,
},
},
undefined,
undefined,
false,
),
).toBe(false);
});
it("should return add or remove the value to an array if checkbox with value parameter", () => {
expect(
getValue(
{
preventDefault: noop,
stopPropagation: noop,
target: {
type: "checkbox",
checked: true,
},
},
undefined,
"foo",
false,
),
).toEqual(["foo"]);
expect(
getValue(
{
preventDefault: noop,
stopPropagation: noop,
target: {
type: "checkbox",
checked: true,
},
},
["A", "B"],
"C",
false,
),
).toEqual(["A", "B", "C"]);
expect(
getValue(
{
preventDefault: noop,
stopPropagation: noop,
target: {
type: "checkbox",
checked: false,
},
},
["foo"],
"foo",
false,
),
).toEqual([]);
expect(
getValue(
{
preventDefault: noop,
stopPropagation: noop,
target: {
type: "checkbox",
checked: false,
},
},
["A", "B", "C"],
"B",
false,
),
).toEqual(["A", "C"]);
expect(
getValue(
{
preventDefault: noop,
stopPropagation: noop,
target: {
type: "checkbox",
checked: false,
},
},
["A", "B", "C"],
"F",
false,
),
).toEqual(["A", "B", "C"]);
expect(
getValue(
{
preventDefault: noop,
stopPropagation: noop,
target: {
type: "checkbox",
checked: false,
},
},
undefined,
"F",
false,
),
).toBeUndefined();
});
it("should return a number type for numeric inputs, when a value is set", () => {
expect(
getValue(
{
preventDefault: noop,
stopPropagation: noop,
target: {
type: "number",
value: "3.1415",
},
},
undefined,
undefined,
true,
),
).toBe("3.1415");
expect(
getValue(
{
preventDefault: noop,
stopPropagation: noop,
target: {
type: "range",
value: "2.71828",
},
},
undefined,
undefined,
true,
),
).toBe("2.71828");
expect(
getValue(
{
preventDefault: noop,
stopPropagation: noop,
target: {
type: "number",
value: "3",
},
},
undefined,
undefined,
false,
),
).toBe("3");
expect(
getValue(
{
preventDefault: noop,
stopPropagation: noop,
target: {
type: "range",
value: "3.1415",
},
},
undefined,
undefined,
false,
),
).toBe("3.1415");
expect(
getValue(
{
preventDefault: noop,
stopPropagation: noop,
target: {
type: "range",
value: "",
},
},
undefined,
undefined,
false,
),
).toBe("");
});
it("should return selected options if is a multiselect", () => {
const options = [
{ selected: true, value: "foo" },
{ selected: true, value: "bar" },
{ selected: false, value: "baz" },
];
const expected = options
.filter((option) => option.selected)
.map((option) => option.value);
expect(
getValue(
{
preventDefault: noop,
stopPropagation: noop,
target: {
type: "select-multiple",
options,
},
},
undefined,
undefined,
true,
),
).toEqual(expected);
expect(
getValue(
{
preventDefault: noop,
stopPropagation: noop,
target: {
type: "select-multiple",
},
},
undefined,
undefined,
false,
),
).toEqual([]); // no options specified
expect(
getValue(
{
preventDefault: noop,
stopPropagation: noop,
target: {
type: "select-multiple",
options,
},
},
undefined,
undefined,
false,
),
).toEqual(expected);
});
it("should return event.target.value if not file or checkbox", () => {
expect(
getValue(
{
preventDefault: noop,
stopPropagation: noop,
target: {
value: undefined,
},
},
undefined,
undefined,
true,
),
).toBe(undefined);
expect(
getValue(
{
preventDefault: noop,
stopPropagation: noop,
target: {
value: undefined,
},
},
undefined,
undefined,
false,
),
).toBe(undefined);
expect(
getValue(
{
preventDefault: noop,
stopPropagation: noop,
target: {
value: null,
},
},
undefined,
undefined,
true,
),
).toBe(null);
expect(
getValue(
{
preventDefault: noop,
stopPropagation: noop,
target: {
value: null,
},
},
undefined,
undefined,
false,
),
).toBe(null);
expect(
getValue(
{
preventDefault: noop,
stopPropagation: noop,
target: {
value: true,
},
},
undefined,
undefined,
true,
),
).toBe(true);
expect(
getValue(
{
preventDefault: noop,
stopPropagation: noop,
target: {
value: true,
},
},
undefined,
undefined,
false,
),
).toBe(true);
expect(
getValue(
{
preventDefault: noop,
stopPropagation: noop,
target: {
value: false,
},
},
undefined,
undefined,
true,
),
).toBe(false);
expect(
getValue(
{
preventDefault: noop,
stopPropagation: noop,
target: {
value: false,
},
},
undefined,
undefined,
false,
),
).toBe(false);
expect(
getValue(
{
preventDefault: noop,
stopPropagation: noop,
target: {
value: 42,
},
},
undefined,
undefined,
true,
),
).toBe(42);
expect(
getValue(
{
preventDefault: noop,
stopPropagation: noop,
target: {
value: 42,
},
},
undefined,
undefined,
false,
),
).toBe(42);
expect(
getValue(
{
preventDefault: noop,
stopPropagation: noop,
target: {
value: "foo",
},
},
undefined,
undefined,
true,
),
).toBe("foo");
expect(
getValue(
{
preventDefault: noop,
stopPropagation: noop,
target: {
value: "foo",
},
},
undefined,
undefined,
false,
),
).toBe("foo");
});
});
================================================
FILE: src/getValue.ts
================================================
const getSelectedValues = (options: any) => {
const result = [];
if (options) {
for (let index = 0; index < options.length; index++) {
const option = options[index];
if (option.selected) {
result.push(option.value);
}
}
}
return result;
};
const getValue = (
event: React.ChangeEvent,
currentValue: any,
valueProp: any,
isReactNative: boolean,
) => {
if (
!isReactNative &&
(event as any).nativeEvent &&
((event as any).nativeEvent as any).text !== undefined
) {
return ((event as any).nativeEvent as any).text;
}
if (isReactNative && (event as any).nativeEvent) {
return ((event as any).nativeEvent as any).text;
}
const detypedEvent: any = event;
const {
target: { type, value, checked },
} = detypedEvent;
switch (type) {
case "checkbox":
if (valueProp !== undefined) {
// we are maintaining an array, not just a boolean
if (checked) {
// add value to current array value
return Array.isArray(currentValue)
? currentValue.concat(valueProp)
: [valueProp];
} else {
// remove value from current array value
if (!Array.isArray(currentValue)) {
return currentValue;
}
const index = currentValue.indexOf(valueProp);
if (index < 0) {
return currentValue;
} else {
return currentValue
.slice(0, index)
.concat(currentValue.slice(index + 1));
}
}
} else {
// it's just a boolean
return !!checked;
}
case "select-multiple":
return getSelectedValues((event.target as any).options);
default:
return value;
}
};
export default getValue;
================================================
FILE: src/getters.ts
================================================
import type { FormState, FieldState } from "final-form";
const addLazyState = (
dest: Record,
state: Record,
keys: string[],
): void => {
keys.forEach((key) => {
Object.defineProperty(dest, key, {
get: () => state[key],
enumerable: true,
});
});
};
export const addLazyFormState = (
dest: Record,
state: FormState,
): void =>
addLazyState(dest, state, [
"active",
"dirty",
"dirtyFields",
"dirtySinceLastSubmit",
"dirtyFieldsSinceLastSubmit",
"error",
"errors",
"hasSubmitErrors",
"hasValidationErrors",
"initialValues",
"invalid",
"modified",
"modifiedSinceLastSubmit",
"pristine",
"submitError",
"submitErrors",
"submitFailed",
"submitSucceeded",
"submitting",
"touched",
"valid",
"validating",
"values",
"visited",
]);
export const addLazyFieldMetaState = (
dest: Record,
state: FieldState,
): void =>
addLazyState(dest, state, [
"active",
"data",
"dirty",
"dirtySinceLastSubmit",
"error",
"initial",
"invalid",
"length",
"modified",
"modifiedSinceLastSubmit",
"pristine",
"submitError",
"submitFailed",
"submitSucceeded",
"submitting",
"touched",
"valid",
"validating",
"visited",
]);
================================================
FILE: src/index.ts
================================================
import * as React from "react";
import Form from "./ReactFinalForm";
import FormSpy from "./FormSpy";
export { default as Field } from "./Field";
export { default as Form, version } from "./ReactFinalForm";
export { default as FormSpy } from "./FormSpy";
export { default as useField } from "./useField";
export { default as useFormState } from "./useFormState";
export { default as useForm } from "./useForm";
export function withTypes>() {
return {
Form: Form as React.ComponentType>,
FormSpy: FormSpy as React.ComponentType<
import("./types").FormSpyProps
>,
};
}
export * from "./types";
================================================
FILE: src/isReactNative.ts
================================================
const isReactNative: boolean = !!(
typeof window !== "undefined" &&
window.navigator &&
window.navigator.product &&
window.navigator.product === "ReactNative"
);
export default isReactNative;
================================================
FILE: src/isSyntheticEvent.ts
================================================
const isSyntheticEvent = (candidate: any): boolean =>
!!(candidate && typeof candidate.stopPropagation === "function");
export default isSyntheticEvent;
================================================
FILE: src/renderComponent.test.js
================================================
import renderComponent from "./renderComponent";
describe("renderComponent", () => {
it("should pass both render and children prop", () => {
const children = "some children";
const render = () => {};
const props = {
component: () => null,
children,
render,
};
const name = "TestComponent";
const result = renderComponent(props, {}, name);
expect(result.props).toEqual({ children, render });
});
it("should include children when rendering with render", () => {
const children = "some children";
const render = jest.fn();
const props = {
children,
render,
};
const name = "TestComponent";
renderComponent(props, {}, name);
expect(render).toHaveBeenCalled();
expect(render).toHaveBeenCalledTimes(1);
expect(render.mock.calls[0][0].children).toBe(children);
});
it("should throw error if no render strategy is provided", () => {
const children = "some children";
const props = {
children,
};
const name = "TestComponent";
expect(() => renderComponent(props, {}, name)).toThrow(
`Must specify either a render prop, a render function as children, or a component prop to ${name}`,
);
});
it("should not evaluate any of the keys given in the second argument", () => {
const children = "some children";
const render = jest.fn();
const props = {
children,
render,
};
const getA = jest.fn();
const getB = jest.fn();
const name = "TestComponent";
renderComponent(
props,
{
get a() {
getA();
return 1;
},
get b() {
getB();
return 2;
},
},
name,
);
expect(render).toHaveBeenCalled();
expect(render).toHaveBeenCalledTimes(1);
expect(render.mock.calls[0][0].children).toBe(children);
expect(getA).not.toHaveBeenCalled();
expect(getB).not.toHaveBeenCalled();
});
it("should not overwrite getter-only properties when using component prop", () => {
// This test reproduces issue #1055
// When lazyProps has getter-only properties (like 'active'), and props contains
// a property with the same name, it should not attempt to overwrite the getter
const Component = () => null;
const props = {
component: Component,
active: "value-from-props", // This would cause "Cannot set property active" error
customProp: "custom",
};
const lazyProps = {};
Object.defineProperty(lazyProps, "active", {
get: () => "value-from-getter",
enumerable: true,
// Note: no setter - this is getter-only
});
const name = "TestComponent";
// Should not throw "Cannot set property active"
let result;
expect(() => {
result = renderComponent(props, lazyProps, name);
}).not.toThrow();
// Check the React element was created with correct props
expect(result.type).toBe(Component);
// The getter-only property should remain and use the getter value
expect(result.props.active).toBe("value-from-getter");
// Custom props should still be passed through
expect(result.props.customProp).toBe("custom");
});
it("should handle getter-only properties in all render paths", () => {
const lazyProps = {};
Object.defineProperty(lazyProps, "active", {
get: () => "getter-value",
enumerable: true,
});
// Test with render prop
const render = jest.fn();
renderComponent(
{ render, active: "prop-value" },
lazyProps,
"TestComponent",
);
expect(render).toHaveBeenCalled();
expect(render.mock.calls[0][0].active).toBe("getter-value");
// Test with children function
const children = jest.fn();
renderComponent(
{ children, active: "prop-value" },
lazyProps,
"TestComponent",
);
expect(children).toHaveBeenCalled();
expect(children.mock.calls[0][0].active).toBe("getter-value");
// Test with component prop
const Component = () => null;
const result = renderComponent(
{ component: Component, active: "prop-value" },
lazyProps,
"TestComponent",
);
expect(result.type).toBe(Component);
expect(result.props.active).toBe("getter-value");
});
});
================================================
FILE: src/renderComponent.ts
================================================
import * as React from "react";
import type { RenderableProps } from "./types";
// shared logic between components that use either render prop,
// children render function, or component prop
export default function renderComponent(
props: RenderableProps & Partial & Record,
lazyProps: Record,
name: string,
): React.ReactNode {
const { render, children, component, ...rest } = props;
if (component) {
// FIX: Don't use Object.assign which tries to overwrite getters
// Instead, create a new object with lazyProps descriptors first,
// then add non-conflicting properties from rest
const result = {} as any;
Object.defineProperties(result, Object.getOwnPropertyDescriptors(lazyProps));
const restDescriptors = Object.getOwnPropertyDescriptors(rest);
for (const key in restDescriptors) {
if (!(key in result)) {
Object.defineProperty(result, key, restDescriptors[key]);
}
}
result.children = children;
result.render = render;
return React.createElement(component, result);
}
if (render) {
const result = {} as T;
Object.defineProperties(
result,
Object.getOwnPropertyDescriptors(lazyProps),
);
// Only add properties from rest that don't already exist
const restDescriptors = Object.getOwnPropertyDescriptors(rest);
for (const key in restDescriptors) {
if (!(key in (result as any))) {
Object.defineProperty(result as any, key, restDescriptors[key]);
}
}
if (children !== undefined) {
(result as any).children = children;
}
return render(result);
}
if (typeof children !== "function") {
throw new Error(
`Must specify either a render prop, a render function as children, or a component prop to ${name}`,
);
}
const result = {} as T;
Object.defineProperties(result, Object.getOwnPropertyDescriptors(lazyProps));
// Only add properties from rest that don't already exist
const restDescriptors = Object.getOwnPropertyDescriptors(rest);
for (const key in restDescriptors) {
if (!(key in (result as any))) {
Object.defineProperty(result as any, key, restDescriptors[key]);
}
}
return children(result);
}
================================================
FILE: src/shallowEqual.test.js
================================================
import shallowEqual from "./shallowEqual";
describe("shallowEqual", () => {
it("returns false if either argument is null", () => {
expect(shallowEqual(null, {})).toBe(false);
expect(shallowEqual({}, null)).toBe(false);
});
it("returns true if both arguments are null or undefined", () => {
expect(shallowEqual(null, null)).toBe(true);
expect(shallowEqual(undefined, undefined)).toBe(true);
});
it("returns true if arguments are shallow equal", () => {
expect(shallowEqual({ a: 1, b: 2, c: 3 }, { a: 1, b: 2, c: 3 })).toBe(true);
});
it("returns false if arguments are not objects and not equal", () => {
expect(shallowEqual(1, 2)).toBe(false);
});
it("returns false if only one argument is not an object", () => {
expect(shallowEqual(1, {})).toBe(false);
});
it("returns false if first argument has too many keys", () => {
expect(shallowEqual({ a: 1, b: 2, c: 3 }, { a: 1, b: 2 })).toBe(false);
});
it("returns false if second argument has too many keys", () => {
expect(shallowEqual({ a: 1, b: 2 }, { a: 1, b: 2, c: 3 })).toBe(false);
});
it("returns true if values are not primitives but are ===", () => {
let obj = {};
expect(shallowEqual({ a: 1, b: 2, c: obj }, { a: 1, b: 2, c: obj })).toBe(
true,
);
});
it("returns false if arguments are not shallow equal", () => {
expect(shallowEqual({ a: 1, b: 2, c: {} }, { a: 1, b: 2, c: {} })).toBe(
false,
);
});
it("should treat objects created by `Object.create(null)` like any other plain object", () => {
function Foo() {
this.a = 1;
}
Foo.prototype.constructor = null;
const object2 = { a: 1 };
expect(shallowEqual(new Foo(), object2)).toBe(true);
const object1 = Object.create(null);
object1.a = 1;
expect(shallowEqual(object1, object2)).toBe(true);
});
});
================================================
FILE: src/shallowEqual.ts
================================================
const shallowEqual = (a: any, b: any): boolean => {
if (a === b) {
return true;
}
if (typeof a !== "object" || !a || typeof b !== "object" || !b) {
return false;
}
var keysA = Object.keys(a);
var keysB = Object.keys(b);
if (keysA.length !== keysB.length) {
return false;
}
var bHasOwnProperty = Object.prototype.hasOwnProperty.bind(b);
for (var idx = 0; idx < keysA.length; idx++) {
var key = keysA[idx];
if (!bHasOwnProperty(key) || a[key] !== b[key]) {
return false;
}
}
return true;
};
export default shallowEqual;
================================================
FILE: src/testUtils.ts
================================================
import React from "react";
export const wrapWith =
(mock: (...args: T) => void, fn: (...args: T) => any) =>
(...args: T) => {
mock(...args);
return fn(...args);
};
/** A simple container component that allows boolean to be toggled with a button */
export function Toggle({
children,
}: {
children: (on: boolean) => React.ReactNode;
}) {
const [on, setOn] = React.useState(false);
return React.createElement(
"div",
null,
children(on),
React.createElement("button", { onClick: () => setOn(!on) }, "Toggle"),
);
}
interface ErrorBoundaryProps {
spy: (error: Error) => void;
children: React.ReactNode;
}
export class ErrorBoundary extends React.Component {
componentDidCatch(error: Error) {
this.props.spy(error);
}
render() {
return this.props.children;
}
}
================================================
FILE: src/types.ts
================================================
import * as React from "react";
import type {
FormApi,
Config,
Decorator,
FormState,
FormSubscription,
FieldSubscription,
FieldValidator,
} from "final-form";
type SupportedInputs = "input" | "select" | "textarea";
export interface ReactContext> {
reactFinalForm: FormApi;
}
export interface FieldInputProps<
FieldValue = any,
T = any,
> {
name: string;
onBlur: (event?: React.FocusEvent) => void;
onChange: (event: React.ChangeEvent | any) => void;
onFocus: (event?: React.FocusEvent) => void;
value: FieldValue;
checked?: boolean;
multiple?: boolean;
type?: string;
}
export interface FieldRenderProps<
FieldValue = any,
T = any,
_FormValues = any,
> {
input: FieldInputProps;
meta: {
active?: boolean;
data?: Record;
dirty?: boolean;
dirtySinceLastSubmit?: boolean;
error?: any;
initial?: any;
invalid?: boolean;
length?: number;
modified?: boolean;
modifiedSinceLastSubmit?: boolean;
pristine?: boolean;
submitError?: any;
submitFailed?: boolean;
submitSucceeded?: boolean;
submitting?: boolean;
touched?: boolean;
valid?: boolean;
validating?: boolean;
visited?: boolean;
};
}
// Re-export of FieldMetaState for backwards compatibility
// (removed from original sources in v7.0.0 but re-exported here)
export type FieldMetaState = FieldRenderProps['meta'];
export interface SubmitEvent {
preventDefault?: () => void;
stopPropagation?: () => void;
}
export interface FormRenderProps>
extends FormState {
handleSubmit: (
event?: SubmitEvent,
) => Promise | undefined> | undefined;
form: FormApi;
}
export interface FormSpyRenderProps>
extends FormState {
form: FormApi;
}
export interface RenderableProps {
component?: React.ComponentType | SupportedInputs;
children?: ((props: T) => React.ReactNode) | React.ReactNode;
render?: (props: T) => React.ReactNode;
}
export interface FormProps>
extends Config,
RenderableProps> {
subscription?: FormSubscription;
decorators?: Decorator[];
form?: FormApi;
initialValuesEqual?: (
a?: Record,
b?: Record,
) => boolean;
__versions?: Record;
}
export interface UseFieldAutoConfig {
afterSubmit?: () => void;
allowNull?: boolean;
beforeSubmit?: () => void | false;
component?: RenderableProps["component"];
data?: Record;
defaultValue?: any;
format?: (value: any, name: string) => any;
formatOnBlur?: boolean;
initialValue?: any;
isEqual?: (a: any, b: any) => boolean;
multiple?: boolean;
parse?: (value: any, name: string) => any;
type?: string;
validate?: FieldValidator;
validateFields?: string[];
value?: any;
}
export interface UseFieldConfig extends UseFieldAutoConfig {
subscription?: FieldSubscription;
}
export interface FieldProps<
FieldValue = any,
T = any,
_FormValues = Record,
> extends UseFieldConfig,
Omit>, "children"> {
name: string;
children?: RenderableProps>["children"];
input?: Partial>; // Allow overriding input props
[key: string]: any; // Allow additional props for HTML elements
}
export interface UseFormStateParams> {
onChange?: (formState: FormState) => void;
subscription?: FormSubscription;
}
export interface FormSpyProps>
extends UseFormStateParams,
RenderableProps> {}
export interface FormSpyPropsWithForm>
extends FormSpyProps {
reactFinalForm: FormApi;
}
================================================
FILE: src/useConstant.ts
================================================
import React from "react";
/**
* A simple hook to create a constant value that lives for
* the lifetime of the component.
*
* Plagiarized from https://github.com/Andarist/use-constant
*
* Do NOT reuse this code unless you know what you're doing.
* Use Andarist's hook; it's more fault tolerant to things like
* falsy values.
*
* @param init - A function to generate the value
*/
export default function useConstant(init: () => T): T {
const ref = React.useRef(undefined);
if (!ref.current) {
ref.current = init();
}
return ref.current;
}
================================================
FILE: src/useConstantCallback.test.js
================================================
import * as React from "react";
import { render, cleanup, act } from "@testing-library/react";
import "@testing-library/jest-dom";
import useConstantCallback from "./useConstantCallback";
describe("useConstantCallback", () => {
afterEach(cleanup);
it("should give the same instance on every render, even as params/deps change", () => {
const callback = jest.fn();
const MyComponent = () => {
const [name, setName] = React.useState("John");
const [age, setAge] = React.useState(20);
const [isAdmin, setAdmin] = React.useState(false);
const constantCallback = useConstantCallback((time) => {
expect(typeof time).toBe("number");
callback(name, age, isAdmin);
});
const callbackRef = React.useRef(constantCallback);
expect(callbackRef.current).toBe(constantCallback);
return (
constantCallback(Date.now())}
>
Call
setName("Paul")}>
{name}
setAge(25)}>
{age}
setAdmin(true)}>
{isAdmin ? "Yes" : "No"}
);
};
const { getByTestId } = render( );
const call = getByTestId("call");
const changeName = getByTestId("changeName");
const changeAge = getByTestId("changeAge");
const changeAdmin = getByTestId("changeAdmin");
expect(changeName).toHaveTextContent("John");
expect(changeAge).toHaveTextContent(20);
expect(changeAdmin).toHaveTextContent("No");
expect(callback).not.toHaveBeenCalled();
call.click();
expect(callback).toHaveBeenCalled();
expect(callback).toHaveBeenCalledTimes(1);
expect(callback.mock.calls[0][0]).toBe("John");
expect(callback.mock.calls[0][1]).toBe(20);
expect(callback.mock.calls[0][2]).toBe(false);
expect(changeName).toHaveTextContent("John");
expect(changeAge).toHaveTextContent(20);
expect(changeAdmin).toHaveTextContent("No");
act(() => {
changeName.click();
});
expect(callback).toHaveBeenCalledTimes(1);
expect(changeName).toHaveTextContent("Paul");
expect(changeAge).toHaveTextContent(20);
expect(changeAdmin).toHaveTextContent("No");
call.click();
expect(callback).toHaveBeenCalledTimes(2);
expect(callback.mock.calls[1][0]).toBe("Paul");
expect(callback.mock.calls[1][1]).toBe(20);
expect(callback.mock.calls[1][2]).toBe(false);
act(() => {
changeAge.click();
});
expect(callback).toHaveBeenCalledTimes(2);
expect(changeName).toHaveTextContent("Paul");
expect(changeAge).toHaveTextContent(25);
expect(changeAdmin).toHaveTextContent("No");
call.click();
expect(callback).toHaveBeenCalledTimes(3);
expect(callback.mock.calls[2][0]).toBe("Paul");
expect(callback.mock.calls[2][1]).toBe(25);
expect(callback.mock.calls[2][2]).toBe(false);
act(() => {
changeAdmin.click();
});
expect(callback).toHaveBeenCalledTimes(3);
expect(changeName).toHaveTextContent("Paul");
expect(changeAge).toHaveTextContent(25);
expect(changeAdmin).toHaveTextContent("Yes");
call.click();
expect(callback).toHaveBeenCalledTimes(4);
expect(callback.mock.calls[3][0]).toBe("Paul");
expect(callback.mock.calls[3][1]).toBe(25);
expect(callback.mock.calls[3][2]).toBe(true);
});
});
================================================
FILE: src/useConstantCallback.ts
================================================
import * as React from "react";
/**
* Creates a callback, even with closures, that will be
* instance === for the lifetime of the component, always
* calling the most recent version of the function and its
* closures.
*/
export default function useConstantCallback any>(
callback: T,
): T {
const ref = React.useRef(callback);
React.useEffect(() => {
ref.current = callback;
});
return React.useCallback(
(...args: any[]) => ref.current.apply(null, args),
[],
) as T;
}
================================================
FILE: src/useField.dynamic-name-869.test.js
================================================
/**
* @jest-environment jsdom
*/
// Tests for dynamic Field name changes (issue #869).
// Covers text inputs, checkboxes, and radio buttons.
import React from 'react'
import { render, cleanup, act } from '@testing-library/react'
import '@testing-library/jest-dom'
import Form from './ReactFinalForm'
import Field from './Field'
describe('useField - Dynamic Name (Issue #869)', () => {
afterEach(cleanup)
it('should keep name and value in sync when field name changes', () => {
const renderSpy = jest.fn()
const TestComponent = ({ fieldName }) => {
return (
{}}
initialValues={{ a: 'value-a', b: 'value-b' }}
>
{() => (
{({ input }) => {
// Log every render to track name/value sync
renderSpy(input.name, input.value)
return
}}
)}
)
}
const { rerender } = render( )
// Initial render - field 'a'
expect(renderSpy).toHaveBeenCalledWith('a', 'value-a')
renderSpy.mockClear()
// Change field name from 'a' to 'b'
act(() => {
rerender( )
})
// Verify all renders after name change have name='b' and value='value-b'
const calls = renderSpy.mock.calls
// Ensure Field actually rendered
expect(calls.length).toBeGreaterThan(0)
// After rerender with fieldName="b", ALL calls should be for field 'b'
calls.forEach(call => {
const [name, value] = call
expect(name).toBe('b')
expect(value).toBe('value-b')
})
})
it('should have correct value immediately after name change (no stale renders)', () => {
const TestComponent = ({ fieldName }) => {
return (
{}}
initialValues={{ a: 'value-a', b: 'value-b' }}
>
{() => (
{({ input }) => (
{input.name}
{input.value}
)}
)}
)
}
const { rerender, getByTestId } = render( )
expect(getByTestId('name')).toHaveTextContent('a')
expect(getByTestId('value')).toHaveTextContent('value-a')
// Change field name
act(() => {
rerender( )
})
// Immediately after rerender, name and value should be in sync
expect(getByTestId('name')).toHaveTextContent('b')
expect(getByTestId('value')).toHaveTextContent('value-b')
})
it('should keep name and checked in sync when checkbox field name changes', () => {
const renderSpy = jest.fn()
const TestComponent = ({ fieldName }) => {
return (
{}}
initialValues={{ a: true, b: false }}
>
{() => (
{({ input }) => {
// Log every render to track name/checked sync
renderSpy(input.name, input.checked)
return
}}
)}
)
}
const { rerender } = render( )
// Initial render - field 'a' checked
expect(renderSpy).toHaveBeenCalledWith('a', true)
renderSpy.mockClear()
// Change field name from 'a' to 'b'
act(() => {
rerender( )
})
// Verify all renders after name change have name='b' and checked=false
const calls = renderSpy.mock.calls
// Ensure Field actually rendered
expect(calls.length).toBeGreaterThan(0)
// After rerender with fieldName="b", ALL calls should be for field 'b'
calls.forEach(call => {
const [name, checked] = call
expect(name).toBe('b')
expect(checked).toBe(false)
})
})
it('should have correct checked immediately after checkbox name change', () => {
const TestComponent = ({ fieldName }) => {
return (
{}}
initialValues={{ a: true, b: false }}
>
{() => (
{({ input }) => (
{input.name}
{String(input.checked)}
)}
)}
)
}
const { rerender, getByTestId } = render( )
expect(getByTestId('name')).toHaveTextContent('a')
expect(getByTestId('checked')).toHaveTextContent('true')
// Change field name
act(() => {
rerender( )
})
// Immediately after rerender, name and checked should be in sync
expect(getByTestId('name')).toHaveTextContent('b')
expect(getByTestId('checked')).toHaveTextContent('false')
})
it('should keep name and checked in sync when radio field name changes', () => {
const renderSpy = jest.fn()
const TestComponent = ({ fieldName }) => {
return (
{}}
initialValues={{ a: 'option1', b: 'option2' }}
>
{() => (
{({ input }) => {
// Log every render to track name/checked sync
renderSpy(input.name, input.checked)
return
}}
)}
)
}
const { rerender } = render( )
// Initial render - field 'a' has value 'option1', not checked for 'option2'
expect(renderSpy).toHaveBeenCalledWith('a', false)
renderSpy.mockClear()
// Change field name from 'a' to 'b'
act(() => {
rerender( )
})
// Verify all renders after name change have name='b' and checked=true
const calls = renderSpy.mock.calls
// Ensure Field actually rendered
expect(calls.length).toBeGreaterThan(0)
// After rerender with fieldName="b", ALL calls should be for field 'b'
// Field 'b' has value 'option2', so radio with value="option2" should be checked
calls.forEach(call => {
const [name, checked] = call
expect(name).toBe('b')
expect(checked).toBe(true)
})
})
it('should have correct checked immediately after radio name change', () => {
const TestComponent = ({ fieldName }) => {
return (
{}}
initialValues={{ a: 'option1', b: 'option2' }}
>
{() => (
{({ input }) => (
{input.name}
{String(input.checked)}
)}
)}
)
}
const { rerender, getByTestId } = render( )
expect(getByTestId('name')).toHaveTextContent('a')
expect(getByTestId('checked')).toHaveTextContent('false')
// Change field name
act(() => {
rerender( )
})
// Immediately after rerender, name and checked should be in sync
expect(getByTestId('name')).toHaveTextContent('b')
expect(getByTestId('checked')).toHaveTextContent('true')
})
})
================================================
FILE: src/useField.test.js
================================================
import * as React from "react";
import { render, fireEvent, cleanup } from "@testing-library/react";
import "@testing-library/jest-dom";
import { ErrorBoundary } from "./testUtils";
import Form from "./ReactFinalForm";
import Field from "./Field";
import { useField } from "./index";
import { act } from "react";
const onSubmitMock = (_values) => {};
describe("useField", () => {
afterEach(cleanup);
// Most of the functionality of useField is tested in Field.test.js
// This file is only for testing its use as a hook in other components
it("should warn if not used inside a form", () => {
jest.spyOn(console, "error").mockImplementation(() => {});
const errorSpy = jest.fn();
const MyFieldComponent = () => {
useField("name");
return
;
};
render(
,
);
expect(errorSpy).toHaveBeenCalled();
expect(errorSpy).toHaveBeenCalledTimes(1);
expect(errorSpy.mock.calls[0][0].message).toBe(
"useField must be used inside of a component",
);
console.error.mockRestore();
});
it("should subscribe to all by default", () => {
const MyFieldListener = () => {
const { input, meta } = useField("name");
expect(meta.active).toBe(false);
expect(meta.data).toEqual({});
expect(meta.dirty).toBe(false);
expect(meta.dirtySinceLastSubmit).toBe(false);
expect(meta.error).toBeUndefined();
expect(meta.initial).toBeUndefined();
expect(meta.invalid).toBe(false);
expect(meta.length).toBeUndefined();
expect(meta.modified).toBe(false);
expect(meta.modifiedSinceLastSubmit).toBe(false);
expect(meta.pristine).toBe(true);
expect(meta.submitError).toBeUndefined();
expect(meta.submitFailed).toBe(false);
expect(meta.submitSucceeded).toBe(false);
expect(meta.submitting).toBe(false);
expect(meta.touched).toBe(false);
expect(meta.valid).toBe(true);
expect(meta.validating).toBe(false);
expect(meta.visited).toBe(false);
expect(input.value).toBe("");
return null;
};
render(
{() => (
)}
,
);
});
it("should track field state", () => {
const spy = jest.fn();
const MyFieldListener = () => {
spy(useField("name").input.value);
return null;
};
const { getByTestId } = render(
{() => (
)}
,
);
expect(getByTestId("name").value).toBe("");
// All forms without restricted subscriptions render twice at first because they
// need to update their validation and touched/modified/visited maps every time
// new fields are registered.
expect(spy).toHaveBeenCalledTimes(2);
expect(spy.mock.calls[0][0]).toBe("");
expect(spy.mock.calls[1][0]).toBe("");
fireEvent.change(getByTestId("name"), { target: { value: "erikras" } });
expect(getByTestId("name").value).toBe("erikras");
expect(spy).toHaveBeenCalledTimes(3);
expect(spy.mock.calls[2][0]).toBe("erikras");
});
it("should allow for creation of render-controlled components", () => {
const spy = jest.fn();
const MemoizedDirtyDisplay = React.memo(({ dirty }) => {
spy(dirty);
return {dirty ? "Dirty" : "Pristine"}
;
});
const MyFieldListener = () => {
const field = useField("name", { subscription: { dirty: true } });
return ;
};
const { getByTestId } = render(
{() => (
)}
,
);
expect(getByTestId("name").value).toBe("");
expect(getByTestId("dirty")).toHaveTextContent("Pristine");
expect(spy).toHaveBeenCalledTimes(1);
expect(spy.mock.calls[0][0]).toBe(false);
// simulate typing
fireEvent.change(getByTestId("name"), { target: { value: "e" } });
expect(getByTestId("dirty")).toHaveTextContent("Dirty");
expect(spy).toHaveBeenCalledTimes(2);
expect(spy.mock.calls[1][0]).toBe(true);
fireEvent.change(getByTestId("name"), { target: { value: "er" } });
fireEvent.change(getByTestId("name"), { target: { value: "eri" } });
fireEvent.change(getByTestId("name"), { target: { value: "erik" } });
fireEvent.change(getByTestId("name"), { target: { value: "erikr" } });
fireEvent.change(getByTestId("name"), { target: { value: "erikra" } });
fireEvent.change(getByTestId("name"), { target: { value: "erikras" } });
// dirty flag hasn't changed since the first character
expect(spy).toHaveBeenCalledTimes(2);
// make pristine again
fireEvent.change(getByTestId("name"), { target: { value: "" } });
expect(getByTestId("dirty")).toHaveTextContent("Pristine");
expect(spy).toHaveBeenCalledTimes(3);
expect(spy.mock.calls[2][0]).toBe(false);
});
it("should give same instance of handlers as value changes", () => {
const spy = jest.fn();
const MyField = ({ name }) => {
const { input } = useField(name, { subscription: { value: true } });
const { onChange, onFocus, onBlur } = input;
spy(onChange, onFocus, onBlur);
return ;
};
render(
{() => (
)}
,
);
expect(spy).toHaveBeenCalledTimes(2);
expect(spy.mock.calls[1][0]).toBe(spy.mock.calls[0][0]); // onChange
expect(spy.mock.calls[1][1]).toBe(spy.mock.calls[0][1]); // onFocus
expect(spy.mock.calls[1][2]).toBe(spy.mock.calls[0][2]); // onBlur
const [onChange, onFocus, onBlur] = spy.mock.calls[0];
const setValue = (value) => {
act(() => {
onFocus();
onChange(value);
onBlur();
});
};
setValue("dog");
expect(spy).toHaveBeenCalledTimes(3);
expect(spy.mock.calls[2][0]).toBe(spy.mock.calls[1][0]); // onChange
expect(spy.mock.calls[2][1]).toBe(spy.mock.calls[1][1]); // onFocus
expect(spy.mock.calls[2][2]).toBe(spy.mock.calls[1][2]); // onBlur
setValue("cat");
expect(spy).toHaveBeenCalledTimes(4);
expect(spy.mock.calls[3][0]).toBe(spy.mock.calls[2][0]); // onChange
expect(spy.mock.calls[3][1]).toBe(spy.mock.calls[2][1]); // onFocus
expect(spy.mock.calls[3][2]).toBe(spy.mock.calls[2][2]); // onBlur
});
it("should give same instance of handlers as name changes", () => {
const spy = jest.fn();
const MyField = ({ name }) => {
const { input } = useField(name, { subscription: { value: true } });
const { onChange, onFocus, onBlur } = input;
spy(onChange, onFocus, onBlur);
return ;
};
const { rerender } = render(
{() => (
)}
,
);
expect(spy).toHaveBeenCalledTimes(2);
expect(spy.mock.calls[1][0]).toBe(spy.mock.calls[0][0]); // onChange
expect(spy.mock.calls[1][1]).toBe(spy.mock.calls[0][1]); // onFocus
expect(spy.mock.calls[1][2]).toBe(spy.mock.calls[0][2]); // onBlur
rerender(
{() => (
)}
,
);
expect(spy).toHaveBeenCalledTimes(4);
expect(spy.mock.calls[2][0]).toBe(spy.mock.calls[1][0]); // onChange
expect(spy.mock.calls[2][1]).toBe(spy.mock.calls[1][1]); // onFocus
expect(spy.mock.calls[2][2]).toBe(spy.mock.calls[1][2]); // onBlur
expect(spy.mock.calls[3][0]).toBe(spy.mock.calls[2][0]); // onChange
expect(spy.mock.calls[3][1]).toBe(spy.mock.calls[2][1]); // onFocus
expect(spy.mock.calls[3][2]).toBe(spy.mock.calls[2][2]); // onBlur
});
it("should give same instance of handlers as type changes", () => {
const spy = jest.fn();
const MyField = ({ name, type }) => {
const { input } = useField(name, { subscription: { value: true }, type });
const { onChange, onFocus, onBlur } = input;
spy(onChange, onFocus, onBlur);
return ;
};
const { rerender } = render(
{() => (
)}
,
);
expect(spy).toHaveBeenCalledTimes(2);
expect(spy.mock.calls[1][0]).toBe(spy.mock.calls[0][0]); // onChange
expect(spy.mock.calls[1][1]).toBe(spy.mock.calls[0][1]); // onFocus
expect(spy.mock.calls[1][2]).toBe(spy.mock.calls[0][2]); // onBlur
rerender(
{() => (
)}
,
);
expect(spy).toHaveBeenCalledTimes(4);
expect(spy.mock.calls[2][0]).toBe(spy.mock.calls[1][0]); // onChange
expect(spy.mock.calls[2][1]).toBe(spy.mock.calls[1][1]); // onFocus
expect(spy.mock.calls[2][2]).toBe(spy.mock.calls[1][2]); // onBlur
expect(spy.mock.calls[3][0]).toBe(spy.mock.calls[2][0]); // onChange
expect(spy.mock.calls[3][1]).toBe(spy.mock.calls[2][1]); // onFocus
expect(spy.mock.calls[3][2]).toBe(spy.mock.calls[2][2]); // onBlur
});
it("should give same instance of handlers as formatOnBlur changes", () => {
const spy = jest.fn();
const MyField = ({ name, formatOnBlur }) => {
const { input } = useField(name, {
subscription: { value: true },
formatOnBlur,
defaultValue: "",
});
const { onChange, onFocus, onBlur } = input;
spy(onChange, onFocus, onBlur);
return ;
};
const { rerender } = render(
{() => (
)}
,
);
expect(spy).toHaveBeenCalledTimes(2);
expect(spy.mock.calls[1][0]).toBe(spy.mock.calls[0][0]); // onChange
expect(spy.mock.calls[1][1]).toBe(spy.mock.calls[0][1]); // onFocus
expect(spy.mock.calls[1][2]).toBe(spy.mock.calls[0][2]); // onBlur
rerender(
{() => (
)}
,
);
expect(spy).toHaveBeenCalledTimes(4);
expect(spy.mock.calls[2][0]).toBe(spy.mock.calls[1][0]); // onChange
expect(spy.mock.calls[2][1]).toBe(spy.mock.calls[1][1]); // onFocus
expect(spy.mock.calls[2][2]).toBe(spy.mock.calls[1][2]); // onBlur
expect(spy.mock.calls[3][0]).toBe(spy.mock.calls[2][0]); // onChange
expect(spy.mock.calls[3][1]).toBe(spy.mock.calls[2][1]); // onFocus
expect(spy.mock.calls[3][2]).toBe(spy.mock.calls[2][2]); // onBlur
});
it("should give same instance of handlers as parse changes", () => {
const spy = jest.fn();
const MyField = ({ name, parse }) => {
const { input } = useField(name, {
subscription: { value: true },
parse,
defaultValue: "",
});
const { onChange, onFocus, onBlur } = input;
spy(onChange, onFocus, onBlur);
return ;
};
const { rerender } = render(
{() => (
)}
,
);
expect(spy).toHaveBeenCalledTimes(2);
expect(spy.mock.calls[1][0]).toBe(spy.mock.calls[0][0]); // onChange
expect(spy.mock.calls[1][1]).toBe(spy.mock.calls[0][1]); // onFocus
expect(spy.mock.calls[1][2]).toBe(spy.mock.calls[0][2]); // onBlur
rerender(
{() => (
x} />
)}
,
);
expect(spy).toHaveBeenCalledTimes(4);
expect(spy.mock.calls[2][0]).toBe(spy.mock.calls[1][0]); // onChange
expect(spy.mock.calls[2][1]).toBe(spy.mock.calls[1][1]); // onFocus
expect(spy.mock.calls[2][2]).toBe(spy.mock.calls[1][2]); // onBlur
expect(spy.mock.calls[3][0]).toBe(spy.mock.calls[2][0]); // onChange
expect(spy.mock.calls[3][1]).toBe(spy.mock.calls[2][1]); // onFocus
expect(spy.mock.calls[3][2]).toBe(spy.mock.calls[2][2]); // onBlur
});
it("should give same instance of handlers as format changes", () => {
const spy = jest.fn();
const MyField = ({ name, format }) => {
const { input } = useField(name, {
subscription: { value: true },
format,
defaultValue: "",
});
const { onChange, onFocus, onBlur } = input;
spy(onChange, onFocus, onBlur);
return ;
};
const { rerender } = render(
{() => (
)}
,
);
expect(spy).toHaveBeenCalledTimes(2);
expect(spy.mock.calls[1][0]).toBe(spy.mock.calls[0][0]); // onChange
expect(spy.mock.calls[1][1]).toBe(spy.mock.calls[0][1]); // onFocus
expect(spy.mock.calls[1][2]).toBe(spy.mock.calls[0][2]); // onBlur
rerender(
{() => (
x} />
)}
,
);
expect(spy).toHaveBeenCalledTimes(4);
expect(spy.mock.calls[2][0]).toBe(spy.mock.calls[1][0]); // onChange
expect(spy.mock.calls[2][1]).toBe(spy.mock.calls[1][1]); // onFocus
expect(spy.mock.calls[2][2]).toBe(spy.mock.calls[1][2]); // onBlur
expect(spy.mock.calls[3][0]).toBe(spy.mock.calls[2][0]); // onChange
expect(spy.mock.calls[3][1]).toBe(spy.mock.calls[2][1]); // onFocus
expect(spy.mock.calls[3][2]).toBe(spy.mock.calls[2][2]); // onBlur
});
it("should give same instance of handlers as component changes", () => {
const spy = jest.fn();
const MyField = ({ name, component }) => {
const { input } = useField(name, {
subscription: { value: true },
component,
defaultValue: "",
});
const { onChange, onFocus, onBlur } = input;
spy(onChange, onFocus, onBlur);
return ;
};
const { rerender } = render(
{() => (
)}
,
);
expect(spy).toHaveBeenCalledTimes(2);
expect(spy.mock.calls[1][0]).toBe(spy.mock.calls[0][0]); // onChange
expect(spy.mock.calls[1][1]).toBe(spy.mock.calls[0][1]); // onFocus
expect(spy.mock.calls[1][2]).toBe(spy.mock.calls[0][2]); // onBlur
rerender(
{() => (
)}
,
);
expect(spy).toHaveBeenCalledTimes(4);
expect(spy.mock.calls[2][0]).toBe(spy.mock.calls[1][0]); // onChange
expect(spy.mock.calls[2][1]).toBe(spy.mock.calls[1][1]); // onFocus
expect(spy.mock.calls[2][2]).toBe(spy.mock.calls[1][2]); // onBlur
expect(spy.mock.calls[3][0]).toBe(spy.mock.calls[2][0]); // onChange
expect(spy.mock.calls[3][1]).toBe(spy.mock.calls[2][1]); // onFocus
expect(spy.mock.calls[3][2]).toBe(spy.mock.calls[2][2]); // onBlur
});
it("should handle null values correctly with allowNull", () => {
const spy = jest.fn();
const MyField = ({ name }) => {
const { input } = useField(name, {
subscription: { value: true },
allowNull: true,
});
spy(input.value);
// Convert null to empty string for the input element to avoid React warnings
return (
);
};
const { rerender } = render(
{() => (
)}
,
);
// Change to non-null value
rerender(
{() => (
)}
,
);
// Change back to null
rerender(
{() => (
)}
,
);
const calls = spy.mock.calls.map((call) => call[0]);
expect(calls).toContain(null); // At least one call with null
expect(calls).toContain("test"); // At least one call with 'test'
expect(calls[calls.length - 1]).toBe(null); // Last call is null
});
it("should return Form initialValues on first render (fix #1050)", () => {
const renderSpy = jest.fn();
const MyField = () => {
const { input } = useField("username");
renderSpy(input.value);
return ;
};
const { getByTestId } = render(
{() => (
)}
,
);
// Critical: on the FIRST render, value should be "erikras" not undefined
expect(renderSpy.mock.calls[0][0]).toBe("erikras");
expect(getByTestId("username").value).toBe("erikras");
});
it("should use field initialValue when Form initialValues doesn't have that field (fix #1050)", () => {
const renderSpy = jest.fn();
const MyField = () => {
const { input } = useField("username", { initialValue: "fieldLevel" });
renderSpy(input.value);
return ;
};
const { getByTestId } = render(
{() => (
)}
,
);
// Field-level initialValue should be used as fallback
expect(renderSpy.mock.calls[0][0]).toBe("fieldLevel");
expect(getByTestId("username").value).toBe("fieldLevel");
});
it("should handle nested field paths in Form initialValues (fix #1050)", () => {
const renderSpy = jest.fn();
const MyField = () => {
const { input } = useField("user.name");
renderSpy(input.value);
return ;
};
const { getByTestId } = render(
{() => (
)}
,
);
// Should correctly resolve nested path on first render
expect(renderSpy.mock.calls[0][0]).toBe("erikras");
expect(getByTestId("nested").value).toBe("erikras");
});
it("should handle array field paths in Form initialValues (fix #1050)", () => {
const renderSpy = jest.fn();
const MyField = () => {
const { input } = useField("items[0].name");
renderSpy(input.value);
return ;
};
const { getByTestId } = render(
{() => (
)}
,
);
// Should correctly resolve array path on first render
expect(renderSpy.mock.calls[0][0]).toBe("Apple");
expect(getByTestId("array").value).toBe("Apple");
});
it("should default undefined value to [] for select multiple with type prop (fix react-final-form-arrays #185)", () => {
const renderSpy = jest.fn();
const MySelectField = () => {
const { input } = useField("scopes", { type: "select", multiple: true });
renderSpy(input.value);
return (
Read
Write
);
};
render(
{() => (
)}
,
);
// When no initial value is provided, should default to [] not undefined
// This prevents React warning: "The `value` prop supplied to must be an array if `multiple` is true"
expect(renderSpy.mock.calls[0][0]).toEqual([]);
});
it("should default undefined value to [] for select multiple with component prop", () => {
const renderSpy = jest.fn();
const MySelectField = () => {
const { input } = useField("scopes", { component: "select", multiple: true });
renderSpy(input.value);
return (
Read
Write
);
};
render(
{() => (
)}
,
);
// When no initial value is provided, should default to [] not undefined
// This prevents React warning: "The `value` prop supplied to must be an array if `multiple` is true"
expect(renderSpy.mock.calls[0][0]).toEqual([]);
});
it("should ensure select multiple value is always an array", () => {
const renderSpy = jest.fn();
const MySelectField = () => {
const { input } = useField("scopes", { type: "select", multiple: true });
renderSpy(input.value);
return (
Read
Write
);
};
render(
{() => (
)}
,
);
// Even if the form value is null/undefined, input.value should be []
expect(Array.isArray(renderSpy.mock.calls[0][0])).toBe(true);
});
});
================================================
FILE: src/useField.ts
================================================
import * as React from "react";
import { fieldSubscriptionItems, getIn } from "final-form";
import type { FieldSubscription, FieldState, FormApi } from "final-form";
import type {
UseFieldConfig,
FieldInputProps,
FieldRenderProps,
} from "./types";
import isReactNative from "./isReactNative";
import getValue from "./getValue";
import useForm from "./useForm";
import useLatest from "./useLatest";
import { addLazyFieldMetaState } from "./getters";
import useConstantCallback from "./useConstantCallback";
import shallowEqual from "./shallowEqual";
const all: FieldSubscription = fieldSubscriptionItems.reduce(
(result: any, key) => {
result[key] = true;
return result;
},
{},
);
const defaultFormat = (value: any, _name: string) =>
value === undefined ? "" : value;
const defaultParse = (value: any, _name: string) =>
value === "" ? undefined : value;
function useField<
FieldValue = any,
T extends HTMLElement = HTMLElement,
FormValues = Record,
>(name: string, config: UseFieldConfig = {}): FieldRenderProps {
const {
afterSubmit,
allowNull,
component,
data,
defaultValue,
format = defaultFormat,
formatOnBlur,
initialValue,
multiple,
parse = defaultParse,
subscription = all,
type,
validateFields,
value: _value,
} = config;
const form: FormApi = useForm("useField");
const configRef = useLatest(config);
const register = (
callback: (state: FieldState) => void,
silent: boolean,
) =>
// avoid using `state` const in any closures created inside `register`
// because they would refer `state` from current execution context
// whereas actual `state` would defined in the subsequent `useField` hook
// execution
// (that would be caused by `setState` call performed in `register` callback)
form.registerField(name as keyof FormValues, callback, subscription, {
afterSubmit,
beforeSubmit: () => {
const {
beforeSubmit,
formatOnBlur,
format = defaultFormat,
} = configRef.current;
if (formatOnBlur) {
const fieldState = form.getFieldState(name as keyof FormValues);
if (fieldState) {
const { value } = fieldState;
const formatted = format(value, name);
if (formatted !== value) {
form.change(name as keyof FormValues, formatted);
}
}
}
return beforeSubmit && beforeSubmit();
},
data,
defaultValue,
getValidator: () => configRef.current.validate,
initialValue,
isEqual: configRef.current.isEqual,
silent,
validateFields,
});
// Initialize state with proper field state from Final Form without callbacks
const [state, setState] = React.useState>(() => {
// Get the current field state from Final Form without registering callbacks
const existingFieldState = form.getFieldState(name as keyof FormValues);
if (existingFieldState) {
// If allowNull is true and the initial value was null, preserve it
// (and its formatted version is not null, meaning it was formatted away)
if (allowNull && existingFieldState.initial === null && existingFieldState.value !== null) {
return {
...existingFieldState,
value: null, // Force value back to null
initial: null, // Ensure our local state's 'initial' also reflects this
};
}
return existingFieldState;
}
// FIX #1050: Check Form initialValues before falling back to field initialValue
// If no existing state, create a proper initial state
const formState = form.getState();
// Use getIn to support nested field paths like "user.name" or "items[0].id"
const formInitialValue = formState.initialValues ? getIn(formState.initialValues, name) : undefined;
// Use Form initialValues if available, otherwise use field initialValue
let initialStateValue = formInitialValue !== undefined ? formInitialValue : initialValue;
if ((component === "select" || type === "select") && multiple && initialStateValue === undefined) {
initialStateValue = [];
}
return {
active: false,
blur: () => { },
change: () => { },
data: data || {},
dirty: false,
dirtySinceLastSubmit: false,
error: undefined,
focus: () => { },
initial: initialStateValue,
invalid: false,
length: undefined,
modified: false,
modifiedSinceLastSubmit: false,
name,
pristine: true,
submitError: undefined,
submitFailed: false,
submitSucceeded: false,
submitting: false,
touched: false,
valid: true,
validating: false,
value: initialStateValue,
visited: false,
};
});
React.useEffect(() => {
// Check if field state exists in the form before registering
const existingFieldState = form.getFieldState(name as keyof FormValues);
// If field doesn't exist in form state, it means the field was destroyed
// (e.g., by destroyOnUnregister in StrictMode). In this case, we need to
// explicitly set the value before registering to ensure the initial value
// is applied, even if form thinks initialValues haven't changed.
if (!existingFieldState) {
const formState = form.getState();
const formInitialValue = formState.initialValues ? getIn(formState.initialValues, name) : undefined;
const valueToSet = formInitialValue !== undefined ? formInitialValue : initialValue;
if (valueToSet !== undefined) {
form.change(name as keyof FormValues, valueToSet);
}
}
// Register field after the initial render to avoid setState during render
const unregister = register((newState) => {
setState((prevState) => {
// Only update if the state actually changed
if (!shallowEqual(newState, prevState)) {
return newState;
}
return prevState;
});
}, false);
return unregister;
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [name, data, defaultValue, initialValue]);
const meta: any = {};
addLazyFieldMetaState(meta, state);
const getInputValue = () => {
// Fix #869: If name changed but state hasn't updated yet (effect hasn't run),
// get the value directly from form values to avoid returning stale value
let value = state.name !== name
? getIn(form.getState().values, name)
: state.value;
// Handle null values first
if (value === null && !allowNull) {
value = "";
}
if (formatOnBlur) {
if (component === "input") {
value = defaultFormat(value, name);
}
} else {
// Only format if value is not null when allowNull is true
if (!(allowNull && value === null)) {
value = format(value, name);
}
}
if ((component === "select" || type === "select") && multiple) {
return Array.isArray(value) ? value : [];
}
// For checkboxes and radios, the `value` prop on the input element itself
// is not the array of selected values or the single selected radio value,
// but rather the specific value this input represents if selected.
// The `checked` prop handles the actual selection state.
// So, for `input.value`, we should return `_value` if provided (for individual inputs in a group)
// or the formatted field value otherwise (for standalone inputs).
if ((type === "checkbox" || type === "radio") && _value !== undefined) {
return _value;
}
return value;
};
const getInputChecked = () => {
// Fix #869: Same as getInputValue - sync with current name
let value = state.name !== name
? getIn(form.getState().values, name)
: state.value;
if (type === "checkbox") {
value = parse(value, name);
if (_value === undefined) {
return !!value;
} else {
return !!(Array.isArray(value) && ~value.indexOf(_value));
}
} else if (type === "radio") {
return parse(value, name) === _value;
}
return undefined;
};
const input: FieldInputProps = {
name,
onBlur: useConstantCallback((_event?: React.FocusEvent) => {
state.blur();
if (formatOnBlur) {
/**
* Here we must fetch the value directly from Final Form because we cannot
* trust that our `state` closure has the most recent value. This is a problem
* if-and-only-if the library consumer has called `onChange()` immediately
* before calling `onBlur()`, but before the field has had a chance to receive
* the value update from Final Form.
*/
const fieldState = form.getFieldState(state.name as keyof FormValues);
if (fieldState) {
state.change(format(fieldState.value, state.name));
}
}
}),
onChange: useConstantCallback((event: React.ChangeEvent | any) => {
// istanbul ignore next
if (process.env.NODE_ENV !== "production" && event && event.target) {
const targetType = event.target.type;
const unknown =
~["checkbox", "radio", "select-multiple"].indexOf(targetType) &&
!type &&
component !== "select";
const value: any =
targetType === "select-multiple" ? state.value : _value;
if (unknown) {
console.error(
`You must pass \`type="${targetType === "select-multiple" ? "select" : targetType
}"\` prop to your Field(${name}) component.\n` +
`Without it we don't know how to unpack your \`value\` prop - ${Array.isArray(value) ? `[${value}]` : `"${value}"`
}.`,
);
}
}
const value: any =
event && event.target
? getValue(event, state.value, _value, isReactNative)
: event;
state.change(parse(value, name));
}),
onFocus: useConstantCallback((_event?: React.FocusEvent) =>
state.focus(),
),
get value() {
return getInputValue();
},
get checked() {
return getInputChecked();
},
};
if (multiple) {
input.multiple = multiple;
}
if (type !== undefined) {
input.type = type;
}
const renderProps: FieldRenderProps = { input, meta }; // assign to force type check
return renderProps;
}
export default useField;
================================================
FILE: src/useForm.test.js
================================================
import React from "react";
import { render, cleanup } from "@testing-library/react";
import "@testing-library/jest-dom";
import { ErrorBoundary } from "./testUtils";
import Form from "./ReactFinalForm";
import { useForm } from "./index";
const onSubmitMock = (values) => {};
describe("useForm", () => {
afterEach(cleanup);
it("should warn if not used inside a form", () => {
jest.spyOn(console, "error").mockImplementation(() => {});
const errorSpy = jest.fn();
const MyFormConsumer = () => {
useForm();
return
;
};
render(
,
);
expect(errorSpy).toHaveBeenCalled();
expect(errorSpy).toHaveBeenCalledTimes(1);
expect(errorSpy.mock.calls[0][0].message).toBe(
"useForm must be used inside of a component",
);
console.error.mockRestore();
});
it("should warn with component name if not used inside a form", () => {
jest.spyOn(console, "error").mockImplementation(() => {});
const errorSpy = jest.fn();
const MyFormConsumer = () => {
useForm("MyFormConsumer");
return
;
};
render(
,
);
expect(errorSpy).toHaveBeenCalled();
expect(errorSpy).toHaveBeenCalledTimes(1);
expect(errorSpy.mock.calls[0][0].message).toBe(
"MyFormConsumer must be used inside of a component",
);
console.error.mockRestore();
});
it("should produce form if used inside ", () => {
const MyFormConsumer = () => {
const form = useForm();
expect(form).toBeDefined();
expect(typeof form.change).toBe("function");
expect(typeof form.reset).toBe("function");
return (
{form ? "Got a form!" : "No form!"}
);
};
const { getByTestId } = render(
{() => } ,
);
expect(getByTestId("formCheck")).toHaveTextContent("Got a form!");
});
});
================================================
FILE: src/useForm.ts
================================================
import * as React from "react";
import type { FormApi } from "final-form";
import ReactFinalFormContext from "./context";
function useForm>(
componentName?: string,
): FormApi {
const form: FormApi | undefined = React.useContext(
ReactFinalFormContext,
);
if (!form) {
throw new Error(
`${componentName || "useForm"} must be used inside of a component`,
);
}
return form;
}
export default useForm;
================================================
FILE: src/useFormState.test.js
================================================
import React from "react";
import { render, cleanup } from "@testing-library/react";
import { ErrorBoundary } from "./testUtils";
import { useFormState, Form } from "./index";
describe("useField", () => {
afterEach(cleanup);
// Most of the functionality of useFormState is tested in FormSpy.test.js
// This file is only for testing its use as a hook in other components
it("should warn if not used inside a form", () => {
jest.spyOn(console, "error").mockImplementation(() => {});
const errorSpy = jest.fn();
const MyFormStateComponent = () => {
useFormState();
return
;
};
render(
,
);
expect(errorSpy).toHaveBeenCalled();
expect(errorSpy).toHaveBeenCalledTimes(1);
expect(errorSpy.mock.calls[0][0].message).toBe(
"useFormState must be used inside of a component",
);
console.error.mockRestore();
});
it("state should be enumerable", () => {
const Test = () => {
const state = useFormState();
expect(Object.keys(state).length > 0).toBe(true);
return It worked
;
};
render(
{}}>
{({ handleSubmit }) => (
)}
,
);
});
});
================================================
FILE: src/useFormState.ts
================================================
import * as React from "react";
import type { UseFormStateParams } from "./types";
import type { FormState, FormApi } from "final-form";
import { all } from "./ReactFinalForm";
import useForm from "./useForm";
import { addLazyFormState } from "./getters";
import shallowEqual from "./shallowEqual";
function useFormState>({
onChange,
subscription = all,
}: UseFormStateParams = {}): FormState {
const form: FormApi = useForm("useFormState");
const onChangeRef = React.useRef(onChange);
onChangeRef.current = onChange;
// Initialize with current form state WITHOUT triggering callbacks during render.
// We intentionally use getState() here so render-prop consumers (e.g. {...})
// can read a fully-populated initial state on first render.
const [state, setState] = React.useState>(() =>
form.getState(),
);
// We want `onChange` to be called AFTER render (fixes #809) and only with the
// subscription-filtered state.
const firstSubscriptionRef = React.useRef(true);
const pendingOnChangeRef = React.useRef | null>(null);
const lastOnChangeRef = React.useRef | null>(null);
React.useEffect(() => {
const unsubscribe = form.subscribe((newState) => {
// Ensure we set state at least once from the subscription, even if equal,
// so that `onChange` can be fired from an effect after the first render.
const isFirst = firstSubscriptionRef.current;
if (isFirst) {
firstSubscriptionRef.current = false;
}
pendingOnChangeRef.current = newState;
setState((prevState) => {
if (isFirst || !shallowEqual(newState, prevState)) {
return newState;
}
return prevState;
});
}, subscription);
return unsubscribe;
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
React.useEffect(() => {
const pending = pendingOnChangeRef.current;
if (!pending || !onChangeRef.current) {
return;
}
// Only fire when the subscription has produced a new state and it differs
// from what we've already emitted.
if (lastOnChangeRef.current === null || !shallowEqual(pending, lastOnChangeRef.current)) {
onChangeRef.current(pending);
lastOnChangeRef.current = pending;
}
// Clear pending once we've handled it.
pendingOnChangeRef.current = null;
}, [state]);
const lazyState = {};
addLazyFormState(lazyState, state);
return lazyState as FormState;
}
export default useFormState;
================================================
FILE: src/useLatest.ts
================================================
import React from "react";
export default function useLatest(value: T): { readonly current: T } {
const ref = React.useRef(value);
React.useEffect(() => {
ref.current = value;
});
return ref;
}
================================================
FILE: src/useWhenValueChanges.ts
================================================
import React from "react";
export default function useWhenValueChanges(
value: any,
callback: () => void,
isEqual: (a: any, b: any) => boolean = (a, b) => a === b,
) {
const previous = React.useRef(value);
React.useEffect(() => {
if (!isEqual(value, previous.current)) {
callback();
previous.current = value;
}
});
}
================================================
FILE: tsconfig.build.json
================================================
{
"extends": "./tsconfig.json",
"compilerOptions": {
"noEmit": false,
"declaration": true,
"emitDeclarationOnly": true,
"outDir": "dist",
"declarationDir": "dist",
"rootDir": "src"
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist", "**/*.test.*", "examples"]
}
================================================
FILE: tsconfig.json
================================================
{
"compilerOptions": {
"target": "es5",
"lib": ["dom", "dom.iterable", "es6"],
"allowJs": true,
"skipLibCheck": true,
"esModuleInterop": true,
"allowSyntheticDefaultImports": true,
"strict": true,
"forceConsistentCasingInFileNames": true,
"module": "esnext",
"moduleResolution": "node",
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true,
"jsx": "react-jsx",
"declaration": true,
"outDir": "dist",
"declarationDir": "dist"
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist", "**/*.test.*", "examples"]
}
================================================
FILE: tslint.json
================================================
{
"defaultSeverity": "error",
"extends": ["tslint:recommended"],
"jsRules": {},
"rules": {
"arrow-parens": false,
"interface-name": [true, "never-prefix"],
"no-empty": [true, "allow-empty-functions"],
"no-empty-interface": false,
"quotemark": [true, "single", "jsx-double"],
"trailing-comma": [true, { "multiline": "never", "singleline": "never" }]
},
"rulesDirectory": []
}
================================================
FILE: typescript/Field.test.tsx
================================================
import * as React from "react";
import { Field, FieldRenderProps } from "react-final-form";
const NumberInput: React.FC<{ value?: number }> = () => null;
function FormText1({ input }: FieldRenderProps) {
// renders OK because of the used generic
return ;
}
// FormText2 removed - was testing expected type error
function FieldNumberValue() {
return (
name="numberField">
{({ input }: FieldRenderProps) => (
)}
);
}
function FieldNumberInputValue() {
return (
name="numberField"
parse={(value: number) => value}
>
{({ input }: FieldRenderProps) => (
)}
);
}
================================================
FILE: typescript/FormSpy.test.tsx
================================================
import * as React from "react";
import { FormSpy, FormSpyRenderProps } from "react-final-form";
function submitButtonSpy() {
return (
{({ pristine, submitting, valid }: FormSpyRenderProps) => {
return (
Submit
);
}}
);
}
================================================
FILE: typescript/ReactFinalForm.test.tsx
================================================
/* tslint:disable: no-shadowed-variable */
import { Decorator, Mutator } from "final-form";
import * as React from "react";
import { Field, Form, FormRenderProps } from "react-final-form";
const noop = () => {};
// missing required props
const C1 = () => {
return ;
};
// provided required props
const C2 = () => ;
const onSubmit = async (values: any) => {
// tslint:disable-next-line no-console
console.log(values);
};
// basic
function basic() {
return (
{({ handleSubmit }: FormRenderProps) => (
First Name
)}
);
}
// simple
function simple() {
return (
{({
handleSubmit,
form,
submitting,
pristine,
values,
}: FormRenderProps) => (
Reset
Submit
{JSON.stringify(values)}
)}
);
}
function simpleSubscription() {
return (
{({
handleSubmit,
form,
submitting,
pristine,
values,
}: FormRenderProps) => (
Reset
{JSON.stringify(values)}
)}
);
}
const setValue: Mutator = ([name, newValue], state, { changeValue }) => {
changeValue(state, name, (value) => newValue);
};
function mutated() {
return (
{({
handleSubmit,
form: {
mutators: { setValue },
},
submitting,
pristine,
values,
}: FormRenderProps) => (
setValue("firstName", "Kevin")}
disabled={submitting || pristine}
>
Reset
{JSON.stringify(values)}
)}
);
}
interface UserForm {
firstName: string;
lastName: string;
}
const typedOnSubmit = (values: UserForm) => {
// tslint:disable-next-line no-console
console.log(values);
};
// with typed form data and field
function withTypedFormData() {
return (
onSubmit={typedOnSubmit}>
{({ handleSubmit }: FormRenderProps) => (
First Name
name="firstName"
component="input"
type="text"
placeholder="First Name"
initialValue=""
/>
)}
);
}
const decorator: Decorator = (form) => {
return form.subscribe(({ values }) => values?.firstName, {
values: true,
});
};
// with typed decorator
function withTypedDecorator() {
return decorators={[decorator]} onSubmit={typedOnSubmit} />;
}
// withWrongTypedDecorator removed - was testing expected type error
================================================
FILE: typescript/index.d.ts
================================================
import * as React from "react";
import {
FormApi,
Config,
Decorator,
FormState,
FormSubscription,
FieldSubscription,
FieldValidator,
} from "final-form";
type SupportedInputs = "input" | "select" | "textarea";
export interface ReactContext> {
reactFinalForm: FormApi;
}
export interface FieldInputProps<
FieldValue = any,
T = any,
> {
name: string;
onBlur: (event?: React.FocusEvent) => void;
onChange: (event: React.ChangeEvent | any) => void;
onFocus: (event?: React.FocusEvent) => void;
value: FieldValue;
checked?: boolean;
multiple?: boolean;
type?: string;
}
export interface FieldRenderProps<
FieldValue = any,
T = any,
_FormValues = any,
> {
input: FieldInputProps;
meta: {
active?: boolean;
data?: Record;
dirty?: boolean;
dirtySinceLastSubmit?: boolean;
error?: any;
initial?: any;
invalid?: boolean;
length?: number;
modified?: boolean;
modifiedSinceLastSubmit?: boolean;
pristine?: boolean;
submitError?: any;
submitFailed?: boolean;
submitSucceeded?: boolean;
submitting?: boolean;
touched?: boolean;
valid?: boolean;
validating?: boolean;
visited?: boolean;
};
}
// Re-export of FieldMetaState for backwards compatibility
// (removed from original sources in v7.0.0 but re-exported here)
export type FieldMetaState = FieldRenderProps['meta'];
export interface SubmitEvent {
preventDefault?: () => void;
stopPropagation?: () => void;
}
export interface FormRenderProps>
extends FormState {
handleSubmit: (
event?: SubmitEvent,
) => Promise | undefined> | undefined;
form: FormApi;
}
export interface FormSpyRenderProps>
extends FormState {
form: FormApi;
}
export interface RenderableProps {
component?: React.ComponentType | SupportedInputs;
children?: ((props: T) => React.ReactNode) | React.ReactNode;
render?: (props: T) => React.ReactNode;
}
export interface FormProps>
extends Config,
RenderableProps> {
subscription?: FormSubscription;
decorators?: Decorator[];
form?: FormApi;
initialValuesEqual?: (
a?: Record,
b?: Record,
) => boolean;
}
export interface UseFieldAutoConfig {
afterSubmit?: () => void;
allowNull?: boolean;
beforeSubmit?: () => void | false;
component?: RenderableProps["component"];
data?: Record;
defaultValue?: any;
format?: (value: any, name: string) => any;
formatOnBlur?: boolean;
initialValue?: any;
isEqual?: (a: any, b: any) => boolean;
multiple?: boolean;
parse?: (value: any, name: string) => any;
type?: string;
validate?: FieldValidator;
validateFields?: string[];
value?: any;
}
export interface UseFieldConfig extends UseFieldAutoConfig {
subscription?: FieldSubscription;
}
export interface FieldProps<
FieldValue = any,
T = any,
_FormValues = Record,
> extends UseFieldConfig,
Omit>, "children"> {
name: string;
children?: RenderableProps>["children"];
input?: Partial>;
[key: string]: any;
}
export interface UseFormStateParams> {
onChange?: (formState: FormState) => void;
subscription?: FormSubscription;
}
export interface FormSpyProps>
extends UseFormStateParams,
RenderableProps> {}
export interface FormSpyPropsWithForm>
extends FormSpyProps {
reactFinalForm: FormApi;
}
export const Field: <
FieldValue = any,
T = any,
FormValues = Record,
>(
props: FieldProps,
) => React.ReactElement;
export const Form: >(
props: FormProps,
) => React.ReactElement;
export const FormSpy: >(
props: FormSpyProps,
) => React.ReactElement;
export function useField<
FieldValue = any,
T = any,
FormValues = Record,
>(
name: string,
config?: UseFieldConfig,
): FieldRenderProps;
export function useForm>(
componentName?: string,
): FormApi;
export function useFormState>(
params?: UseFormStateParams,
): FormState;
export function withTypes>(): {
Form: React.FC>;
FormSpy: React.FC>;
};
export const version: string;
================================================
FILE: typescript/tsconfig.json
================================================
{
"compilerOptions": {
"lib": ["es2015", "dom"],
"jsx": "react",
"module": "esnext",
"baseUrl": ".",
"noEmit": true,
"strict": true,
"moduleResolution": "bundler",
"paths": {
"react-final-form": ["../dist/index.d.ts"]
}
},
"include": ["."]
}
================================================
FILE: typescript/useField.test.tsx
================================================
import * as React from "react";
import { useField, FieldRenderProps } from "react-final-form";
const NumberInput: React.FC<{ value?: number }> = () => null;
function NumberFieldValue() {
const { input } = useField("numberField");
return ;
}
function NumberInputValue() {
const { input } = useField("numberField", {
format: (value: string) => Number(value),
parse: (value: number) => String(value),
});
return ;
}
function MyComponent() {
const field: FieldRenderProps = useField("myField");
return ;
}
function MyTypedComponent() {
const field: FieldRenderProps = useField("myField");
return ;
}
function MyTypedComponentWithElement() {
const field: FieldRenderProps = useField<
string,
HTMLInputElement
>("myField");
return ;
}
================================================
FILE: typescript/useFormState.test.tsx
================================================
import { useFormState } from "react-final-form";
import { FormState } from "final-form";
const submittingToLabel = (submitting: boolean) => (submitting ? "Yes" : "No");
function Comp1() {
const { submitting } = useFormState();
return submittingToLabel(submitting || false);
}
function MyComponent() {
const formState: FormState = useFormState();
return null;
}
function MyTypedComponent() {
const formState: FormState<{ name: string }> = useFormState<{
name: string;
}>();
return null;
}