New Case Study:See how Anthropic automated 95% of dependency reviews with Socket.Learn More
Socket
Sign inDemoInstall
Socket

reactive-form-model

Package Overview
Dependencies
Maintainers
1
Versions
7
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

reactive-form-model

rxjs based, framework agnostic, tiny form state management library

  • 0.0.7
  • latest
  • Source
  • npm
  • Socket score

Version published
Maintainers
1
Created
Source

travis ci codecov

What

This is a framework agnostic form model built on top of rxjs.

Features:

  • validator
  • middelware
  • dependency and reaction between fields
  • metadata
    • this is where u put data needed by the view layer, such as select input's option list, and form labels
    • it's generic, u can put anything the view layer needs

Will not be features:

  • type or widget field which indicates what input type this field is

    • as a state management lib, this lib does not care about such information, you should put this into metadata field, e.g.

      import {FormControl} from "reactive-form-model"
      import {of} from "rxjs"
      const password = new FormControl("", { 
          metadata: of({ 
              type: "password" 
          }) 
      })
      
  • Framework support/Component implementation

    • it's real easy to implement, please see the Example to see how to do this.
  • Json-like schema form

    • it's real easy to do

Why

  • zero dependency except rxjs.
  • framework agnostic, can be used with react, vue
    • not angular because ng already have good reactive form model.
  • separate form view and business logic

Example

This codesandbox shows how to use this lib with ant-design's form, form item and input components

basically, u define a model like this (example is in react):

import {of, fromFetch} from "rxjs"
import {publishReplay, refCount} from "rxjs/operators"
import {FormControl, FormControlList, FormControlGroup} from "reactive-form-model"

async function validatePhoneNumber(v){
    return typeof v === 'number' || v.match(/^[0-9_+- ]+$/)
}

export function registrationFormModel(){
    const username = new FormControl("")
    const password = new FormControl("", { 
        metadata: of({ 
            type: "password" 
        }) 
    })
    const passwordConfirm = new FormControl("", { 
        metadata: of({ 
            type: "password" 
        }) 
    })

    const phoneNumbers = new FormControlList([], value=>{
        return new FormControl(value, {
            validator: validatePhoneNumber
        })
    })

    const provinceList = fromFetch("/province").pipe(
        publishReplay(1),
        refCount()
    )

    const addresses = new FormControlList([] as {province: string, city: string}[], value=>{
        const province = new FormControl(value.province, {
            //provice a list for use when rendered, but we don't care what component is used in view layer.
            metadata: provinceList
        })
        const city = new FormControl(value.city, {
            //provice citylist
            metadata: combineLatest([
                province.value,
                provinceList
            ]).pipe(
                map(([selectedProvince, provinceList])=>{
                    const cityList = (selectedProvince && provinceList.find(x=>x.id === selectedProvince))?.cityList
                    return {
                        selectedProvince
                        cityList
                    }
                })
            )
        })
        //clear city after province changed
        province.value.subscribe(()=>{
            city.change("")
        })
        return new FormControlGroup({
            province,
            city,
        })
    })

    return new FormControlGroup({
        username,
        password,
        passwordConfirm,
        phoneNumbers,
        addresses
    }, {
        validator: async v=>{
            if(!v.username){
                return "must fill username"
            }
            if(v.password !== v.passwordConfirm){
                return "password must be equal to passwordConfirm"
            }
        }
    })
}

Then you create a connection component between the model and how to render then, this needs only done once and u can use in all forms.

If you are using ant-design, u can directly copy these.

import { Form, Col, Row, Button } from "antd"
import { FormItemProps } from "antd/lib/form"
import * as React from "react"
import { AbstractControl, ValidationInfo, FormControlList } from "reactive-form-model"
import { CloseOutlined, PlusOutlined, MinusOutlined } from "@ant-design/icons"
import { useSubscription } from "use-subscription"

type FormItemRenderChildren<T, Meta> = (inputProps: { value?: T; onChange?: (v: T) => void }, behavior: Meta | null, err: ValidationInfo) => React.ReactNode

export function FormItem<T, Meta>({
    field,
    children,
    ...rest
}: Omit<FormItemProps, "name" | "children"> & {
    field: AbstractControl<T, Meta>
    children: FormItemRenderChildren<T, Meta>
}) {
    const value = useObservable(field.value)
    const metadata = useObservable(field.metadata)
    const error = useObservable(field.error)

    return (
        <Form.Item labelCol={{span:4}} wrapperCol={{span:20}} hasFeedback help={!!error ? String(error) : undefined} validateStatus={!!error ? "error" : undefined} {...rest}>
            {children(
                {
                    value: value === null ? undefined : value,
                    onChange: field.change,
                },
                meta,
                error
            )}
        </Form.Item>
    )
}


//eslint-disable-next-line
export function FormList<Meta, Children extends AbstractControl<any, any>>({
    field,
    children,
    ...rest
}: Omit<FormItemProps, "name" | "children"> & {
    field: FormControlList<Meta, Children>
    children: (child: Children, arrayMeta: Meta | null, index: number) => React.ReactNode
}) {
    const items = useObservable(field.children)
    const metadata = useObservable(field.metadata)
    const error = useObservable(field.error)
    return (
        <Form.Item  labelCol={{span:4}} wrapperCol={{span:20}} hasFeedback help={!!error ? String(error) : undefined} validateStatus={!!error ? "error" : undefined} {...rest}>
            <Row>
                <Col span={24}>
                    {items.map((x, i) => {
                        return (
                            <Row gutter={8} key={x.key}>
                                <Col>
                                    <Button
                                        icon={<MinusOutlined />}
                                        onClick={() => {
                                            field.delete(i)
                                        }}
                                    ></Button>
                                </Col>
                                <Col span={22}>
                                    {children(x.child, metadata, i)}
                                </Col>
                            </Row>
                        )
                    })}
                </Col>
            </Row>
            <Row>
                <Col span={24}>
                    <Button
                        icon={<PlusOutlined />}
                        onClick={() => {
                            //eslint-disable-next-line
                            field.push({} as any)
                        }}
                    ></Button>
                </Col>
            </Row>
        </Form.Item>
    )
}

function useObservable<T>(ob: Observable<T>){
    return useSubscription(useMemo(()=>{
        let value = null
        return {
            getCurrentValue: ()=>value,
            subscribe: cb=>{
                const sub = ob.subscribe((v)=>{
                    value = v
                    cb()
                })
                return ()=>sub.unsubscribe()
            }
        }
    }, [ob]))
}

Last u render it, this needs to be done for every single form.


function App(){
    const form = useMemo(registrationFormModel,[])

    const hasError = useObservable(form.error)

    return <Form>
        <FormItem label={"UserName"} field={form.children.username}>
            {(props)=>{
                return <Input {...props} maxLength={24}>
            }}
        </FormItem>
        <FormItem label={"Password"} field={form.children.password}>
            {(props)=>{
                return <Input {...props} maxLength={24} type="password">
            }}
        </FormItem>
        <FormItem label={"Confirm Password"} field={form.children.passwordConfirm}>
            {(props)=>{
                return <Input {...props} maxLength={24} type="password">
            }}
        </FormItem>
        <FormList label="PhoneNumber" field={form.children.phoneNumbers}>
            {field=>{
                return <FormItem field={field}>
                    {(props)=>{
                        return <Input {...props} maxLength={24}>
                    }}
                </FormItem>
            }}
        </FormList>
        <FormList label="Addresses" field={form.children.addresses}>
            {field=>{
                return <>
                    <FormItem field={field.children.province}>
                        {(props, options)=>{
                            return <Select {...props} options={options}>
                        }}
                    </FormItem>
                    <FormItem field={field.children.city}>
                        {(props, options)=>{
                            return <Select {...props} options={options.cityList}>
                        }}
                    </FormItem>
                </>
            }}
        </FormList>
        <Button htmlType="submit" disabled={!!hasErro}>Submit</Button>
    </Form>
}

FAQs

Package last updated on 28 Jan 2021

Did you know?

Socket

Socket for GitHub automatically highlights issues in each pull request and monitors the health of all your open source dependencies. Discover the contents of your packages and block harmful activity before you install or update your dependencies.

Install

Related posts

SocketSocket SOC 2 Logo

Product

  • Package Alerts
  • Integrations
  • Docs
  • Pricing
  • FAQ
  • Roadmap
  • Changelog

Packages

npm

Stay in touch

Get open source security insights delivered straight into your inbox.


  • Terms
  • Privacy
  • Security

Made with ⚡️ by Socket Inc