Effector

Effector

  • Docs
  • Try
  • API
  • Blog
  • Twitter
  • GitHub

›React

Introduction

  • Installation
  • Core Concepts
  • Prior Art

Recipes

  • Recipes: Index
  • Usage with TypeScript
  • Migrating to Effector

    • From Redux

    React

    • Example
    • Example: Effects
    • Example: Forms
    • Gate: a bridge between props and store
    • Example: TODO creator
    • Example: Dynamic form schema

    React Native

    • React Native Example

Other

  • Troubleshooting
  • Glossary

FAQ

  • FAQ
Edit

Dynamic form schema

Try it


const submitForm = createEvent()

const saveForm = createEffect({
  handler(data) {
    localStorage.setItem(
      'form_state/2',
      JSON.stringify(data, null, 2)
    )
  }
})

const loadForm = createEffect({
  handler() {
    return JSON.parse(
      localStorage.getItem('form_state/2')
    )
  }
})

const mainForm = createStore({})
    .on(loadForm.done, (state, {result}) => {
    let changed = false
    state = {...state}
    for (const key in result) {
      const {value} = result[key]
      if (value == null) continue
      if (state[key] === value) continue
      changed = true
      state[key] = value
    }
    if (!changed) return
    return state
  })

const mainFormApi = createApi(mainForm, {
  upsertField(state, name) {
    if (name in state) return
    return {...state, [name]: ''}
  },
  changeField(state, [name, value]) {
    if (state[name] === value) return
    return {...state, [name]: value}
  },
  addField(state, [name, value = '']) {
    if (state[name] === value) return
    return {...state, [name]: value}
  },
  deleteField(state, name) {
    if (!(name in state)) return
    state = {...state}
    delete state[name]
    return state
  },
})

const types = createStore({
  username: 'text',
  email: 'text',
  password: 'text',
})
    .on(mainFormApi.addField, (state, [name, value, type]) => {
    if (state[name] === type) return
    return {...state, [name]: value}
  })
    .on(mainFormApi.deleteField, (state, name) => {
    if (!(name in state)) return
    state = {...state}
    delete state[name]
    return state
  })
    .on(loadForm.done, (state, {result}) => {
    let changed = false
    state = {...state}
    for (const key in result) {
        const {type} = result[key]
      if (type == null) continue
      if (state[key] === type) continue
      changed = true
      state[key] = type
    }
    if (!changed) return
    return state
  })

const fields = types.map(state => Object.keys(state))

const changeFieldInput = mainFormApi.changeField.prepend(
  e => [
    e.currentTarget.name,
    e.currentTarget.type === 'checkbox'
        ? e.currentTarget.checked
        : e.currentTarget.value,
  ]
)
const submitField = mainFormApi.addField.prepend(
  e => [
    e.currentTarget.fieldname.value,
    e.currentTarget.fieldtype.value === 'checkbox'
        ? e.currentTarget.fieldvalue.checked
        : e.currentTarget.fieldvalue.value,
    e.currentTarget.fieldtype.value,
  ]
)
const submitRemoveField = mainFormApi.deleteField.prepend(
  e => e.currentTarget.field.value
)
submitForm.watch(e => {
  e.preventDefault()
})
submitField.watch(e => {
  e.preventDefault()
  e.currentTarget.reset()
})
submitRemoveField.watch(e => {
  e.preventDefault()
})


sample({
  source: {
    values: mainForm,
    types,
  },
  clock: merge([
    submitForm,
    submitField,
    submitRemoveField,
  ]),
  target: saveForm,
  fn({values, types}) {
    const result = {}
    for (const [key, value] of Object.entries(values)) {
      result[key] = {
        value,
        type: types[key],
      }
    }
    return result
  },
})


const addMessage = createEvent()
const message = restore(addMessage, 'done')
const showTooltip = createEffect({
  handler: () => new Promise(rs => setTimeout(rs, 1500)),
})

forward({
  from: addMessage,
  to: showTooltip,
})
forward({
  from: submitField,
  to: addMessage.prepend(() => 'added'),
})
forward({
  from: submitRemoveField,
  to: addMessage.prepend(() => 'removed'),
})
forward({
  from: submitForm,
  to: addMessage.prepend(() => 'saved'),
})

loadForm.finally.watch(() => {
  ReactDOM.render(
    <App />,
    document.getElementById('root')
  )
})


function useFormField(name) {
  const type = useStoreMap({
    store: types,
    keys: [name],
    fn(state, [field]) {
      if (field in state) return state[field]
      return 'text'
    },
  })
  const value = useStoreMap({
    store: mainForm,
    keys: [name],
    fn(state, [field]) {
      if (field in state) return state[field]
      return ''
    },
  })
  mainFormApi.upsertField(name)
  return [value, type]
}

function Form() {
  const pending = useStore(saveForm.pending)
  return (
    <form onSubmit={submitForm} data-form autocomplete="off">
      <header>
        <h4>Form</h4>
      </header>
      {useList(fields, name => (
        <InputField name={name} />
      ))}
      
      <input
        type="submit"
        value="save form"
        disabled={pending}
      />
    </form>
  )
}

function InputField({name}) {
  const [value, type] = useFormField(name)
  let input = null
  switch (type) {
    case 'checkbox':
      input = (
        <input 
          id={name} 
          name={name} 
          value={name}
          checked={value}
          onChange={changeFieldInput} 
          type="checkbox" 
        />
      )
      break
    case 'text':
    default:
      input = (
        <input 
          id={name} 
          name={name} 
          value={value} 
          onChange={changeFieldInput} 
          type="text" 
        />
      )
  }
  return (
    <>
      <label htmlFor={name} style={{display: 'block'}}>
        <strong>{name}</strong>
      </label>
      {input}
    </>
  )
}

const changeFieldType = createEvent()
const fieldType = createStore('text')
    .on(changeFieldType, (_, e) => e.currentTarget.value)
    .reset(submitField)
function FieldForm() {
  const currentFieldType = useStore(fieldType)
  const fieldValue = currentFieldType === 'checkbox'
    ? (
      <input
        id="fieldvalue"
        name="fieldvalue"
        type="checkbox"
      />
    )
    : (
        <input
        id="fieldvalue"
        name="fieldvalue"
        type="text"
        defaultValue=""
      />
    )
  return (
    <form onSubmit={submitField} autocomplete="off" data-form>
        <header>
          <h4>Insert new field</h4>
        </header>
        <label htmlFor="fieldname"><strong>name</strong></label>
        <input
          id="fieldname"
          name="fieldname"
          type="text"
          required
          defaultValue=""
        />
        <label htmlFor="fieldvalue"><strong>value</strong></label>
        {fieldValue}
        <label htmlFor="fieldtype"><strong>type</strong></label>
        <select id="fieldtype" name="fieldtype" onChange={changeFieldType}>
          <option value="text">text</option>
          <option value="checkbox">checkbox</option>
        </select>
        <input
          type="submit"
          value="insert"
        />
    </form>
  )
}


function RemoveFieldForm() {
  return (
    <form onSubmit={submitRemoveField} data-form>
      <header>
        <h4>Remove field</h4>
      </header>
      <label htmlFor="field"><strong>name</strong></label>
      <select id="field" name="field" required>
        {useList(fields, name => (
          <option value={name}>{name}</option>
        ))}
      </select>
      <input
        type="submit"
        value="remove"
      />
    </form>
  )
}

const Tooltip = () => {
  const visible = useStore(showTooltip.pending)
  const text = useStore(message)
  return (
    <span data-tooltip={text} data-visible={visible}/>
  )
}
const App = () => (
  <>
    <Tooltip/>
    <div id="app">
      <Form/>
      <FieldForm />
      <RemoveFieldForm/>
    </div>
  </>
)

await loadForm()

css`
    [data-tooltip]:before {
        display: block;
        background: white;
        width: min-content;
        content: attr(data-tooltip);
        position: sticky;
        top: 0;
        left: 50%;
        color: darkgreen;
        font-family: sans-serif;
        font-weight: 800;
        font-size: 20px;
        padding: 5px 5px;
        transition: transform 100ms ease-out;
    }
    [data-tooltip][data-visible="true"]:before {
        transform: translate(0px, 0.5em);
    }
    [data-tooltip][data-visible="false"]:before {
        transform: translate(0px, -2em);
    }
    [data-form] {
        display: contents;
    }
    [data-form] > header {
        grid-column: 1 / span 2;
    }
    [data-form] > header > h4 {
        margin-block-end: 0;
    }
    [data-form] label {
        grid-column: 1;
        justify-self: end;
    }
    [data-form] input:not([type="submit"]),
    [data-form] select {
        grid-column: 2;
    }

    [data-form] input[type="submit"] {
        grid-column: 2;
        justify-self: end;
        width: fit-content;
    }

    #app {
    width: min-content;
        display: grid;
        grid-column-gap: 5px;
        grid-row-gap: 8px;
        grid-template-columns: repeat(2, 3fr);
    }
`

function css(tags, ...attrs) {
  const value = style(tags, ...attrs)
  const node = document.createElement('style')
  node.id = 'insertedStyle'
  node.appendChild(document.createTextNode(value))
  const sheet = document.getElementById('insertedStyle')
  if (sheet) {
    sheet.disabled = true
    sheet.parentNode.removeChild(sheet)
  }
  document.head.appendChild(node)

  function style(tags, ...attrs) {
    if (tags.length === 0) return ''
    let result = ' ' + tags[0]
    for (let i = 0; i < attrs.length; i++) {
      result += attrs[i]
      result += tags[i + 1]
    }
    return result
  }
}
← Example: TODO creatorReact Native Example →
Effector
Docs
Getting StartedAPI Reference
Community
User ShowcaseStack OverflowGitterTwitter
More
GitHubStar
Copyright © 2019 zerobias