znamenica/dneslov

View on GitHub
app/components/Form.jsx

Summary

Maintainability
A
0 mins
Test Coverage
import { Component } from 'react'
import PropTypes from 'prop-types'
import * as uuid from 'uuid/v1'
import { merge } from 'merge-anything'
import axios from 'axios'
import { mixin } from 'lodash-decorators'

import Validation from 'Validation'
import { matchCodes } from 'matchers'
import { renderElement } from 'render'
import ErrorSpan from 'ErrorSpan'

@mixin(Validation)
export default class Form extends Component {
   static defaultProps = {
      data: null,
      meta: null,
      validations: {},
   }

   static propTypes = {
      validations: PropTypes.object
   }

   // state is treated as a query, which has non-serialized form without '*_attributes' and with uuided hashes
   state = { prevProps: null }

   valid = false

   static getDerivedStateFromProps(props, state) {
      if (props !== state.prevProps) {
         return({
            prevProps: props,
            query: Form.deserializedHash(props.data, Form.metaToScheme(props.meta)),
            lastUpdatedPath: null,
            error: ""
         })
      } else {
         return null
      }
   }

   // system
   constructor() {
      super()

      this.onSubmit = this.onSubmit.bind(this)
      this.onChildChanged = this.onChildChanged.bind(this)
   }

   componentDidMount() {
      console.log("[componentDidMount] <<<")
      document.addEventListener('dneslov-update-path', this.onChildChanged, { passive: true })
      document.addEventListener('dneslov-record-submit', this.onSubmit)
   }

   componentWillUnmount() {
      console.log("[componentWillUnmount] <<<")
      document.removeEventListener('dneslov-update-path', this.onChildChanged)
      document.removeEventListener('dneslov-record-submit', this.onSubmit)
   }

   componentDidUpdate() {
      var valid = document.querySelectorAll('.error:not(:empty)').length == 0

      if (valid && !this.valid || !valid && this.valid) {
         let ce = new CustomEvent('dneslov-form-valid', { detail: { valid: valid }})

         this.valid = valid
         document.dispatchEvent(ce)
      }

      if (this.state.lastUpdatedPath) {
         let ce = new CustomEvent('dneslov-path-stored', { detail: this.state.lastUpdatedPath })

         document.dispatchEvent(ce)
      }
   }

   // custom
   serializedQuery() {
      return Form.serializedHash(this.state.query)
   }

   // events
   onChildChanged(e) {
      console.debug("[onChildChanged] <<<", e)
      let newState = {
         query: merge({}, this.state.query, e.detail.value),
         lastUpdatedPath: e.detail.path
      }

      console.log("[onChildChanged] > new state", newState)
      this.setState(newState)
   }

   onSubmitSuccess(response) {
      let ce = new CustomEvent('dneslov-record-stored', { detail: response.data })

      document.dispatchEvent(ce)
   }

   onSubmitError(error) {
      console.debug("[onSubmitError] <<<", error)
      let error_text

      if (error.response.responseJSON) {
         console.log("[onSubmitError] * response json", error.response.responseJSON)
         let errors = []

         Object.entries(error.response.responseJSON).forEach(([key, value]) => {
            errors.push(value.map((e) => { return key + " " + e }))
         })

         error_text = errors.join(", ")
      } else if (error.response.responseText) {
         console.log("[onSubmitError] * response text", error.response.responseText)
         error_text = JSON.stringify(error.response.responseText)
      } else {
         console.log("[onSubmitError] * response data", error.response.data)
         error_text = error.response.statusText + ": " + JSON.stringify(error.response.data)
      }

      this.setState({ error: error_text })
   }

   onSubmit(e) {
      e.stopPropagation()
      e.preventDefault()

      let record = this.serializedQuery(),
          request = { data: {}},
          request_url_base = '/' + this.props.remoteNames,
          id = this.state.query.id

      request.data[this.props.remoteName] = record

      if (id) {
         request.method = 'put'
         request.url = request_url_base + '/' + id + '.json'
      } else {
         request.method = 'post'
         request.url = request_url_base + '.json'
      }

      console.log("Form submit", request)

      axios(request)
        .then(this.onSubmitSuccess.bind(this))
        .catch(this.onSubmitError.bind(this))
   }

   // serialization to send the JSON out
   // converts {} to []
   static serializedHash(hash) {
      let result = {}, subkey

      Object.entries(hash).forEach(([key, value]) => {
         console.log(key, value, value && value.constructor.name)
         switch(value && value.constructor.name) {
         case 'Array':
            subkey = Object.keys(value)[0]
            if (subkey && subkey.match(/\w{8}-\w{4}-\w{4}-\w{4}-\w{12}/)) {
               result[key + '_attributes'] = Object.values(value).map((v) => {
                  return value && value.constructor.name === "Object" && this.serializedHash(v) || v
               })
            } else {
               result[key + '_attributes'] = value
            }
            break
         case 'Object':
            subkey = Object.keys(value)[0]
            if (subkey && subkey.match(/\w{8}-\w{4}-\w{4}-\w{4}-\w{12}/)) {
               result[key + '_attributes'] = Object.values(value).map((v) => {
                  return value && value.constructor.name === "Object" && this.serializedHash(v) || v
               })
            } else {
               result[key + '_attributes'] = this.serializedHash(value)
            }
            break
         default:
            result[key] = value
         }
      })

      console.log(result)
      return result
   }

   static deserializeArray(value, scheme) {
      console.debug("[deserializedArray] <<< value: ", value, ", scheme: ", scheme)
      let result = Object.entries(scheme).reduce((preResult, [target, subscheme]) => {
         let array
         console.debug("[deserializedArray] ** value class:", value[0] && value[0].constructor.name)
         console.debug("[deserializedArray] ** subscheme:", subscheme)
         if (subscheme && subscheme["_filter"]) {
            array = value.filter((v) => {
               console.debug("[deserializedArray] ** filter v:", v)
               return subscheme["_filter"].all(([sKey, sValue]) => {
                  return sKey.match(/^_/) || v[sKey] && v[sKey] == sValue
               })
            })
         } else {
            array = value
         }

         console.debug("[deserializedArray] ** array:", array)
         let resValue = array.reduce((s, v, index) => {
            s[uuid()] = merge({ _pos: index }, Form.deserializedHash(v, subscheme))
            return s
         }, {})

         console.debug("[deserializedArray] ** resValue:", resValue)

         if (resValue && !resValue.isBlank()) {
            return merge(preResult, { [target]: resValue })
         } else {
            return preResult
         }
      }, {})

      console.debug("[deserializedArray] >>>", result)

      return result
   }

   // deserialization of parsed JSON to fill the form
   // converts [] to {}
   static deserializedHash(hash, schemeIn) {
      console.debug("[deserializedHash] <<< hash:", hash, "schemeIn:", schemeIn)

      let entries = Object.entries(hash)
      let result = entries.reduce((preResult, [key, value]) => {
         let preValue = {}, tmpValue, scheme

         console.log("[deserializedHash] *", key, "[", (value && value.constructor.name), "]", value)

         switch(value && value.constructor.name) {
            case 'Array':
               if (schemeIn) {
                  if (value[0] instanceof Object) {
                     scheme = (schemeIn || { [key]: null }).select((keyIn, valueIn) => {
                        return keyIn == key || valueIn["_source"] == key
                     })
                     preValue = Form.deserializeArray(value, scheme)
                  } else if (value[0]) {
                     preValue = { [key]: value }
                  } else {
                     preValue = { [key]: {} }
                  }
               } else {
                  preValue = { [key]: value }
               }
               break
            case 'Object':
               if (schemeIn) {
                  tmpValue = merge({ _pos: entries.indexOf([key, value]) }, Form.deserializedHash(value, schemeIn[key]))
                  preValue = { [key]: tmpValue }
               } else {
                  preValue = { [key]: value }
               }
               break
            default:
               preValue = { [key]: value }
            }

         console.debug("[deserializedHash] (new) preResult:", merge(preResult, preValue))
         return merge(preResult, preValue)
      }, {})

      console.debug("[deserializedHash] >>>", result)
      return result
   }

   static metaToScheme(meta) {
      return Object.entries(meta).reduce((scheme, [key, value]) => {
         if (value["meta"]) {
            scheme[key] = Form.metaToScheme(value["meta"])
         }

         if (value["filter"] instanceof Object) {
            scheme[key] ||= {}
            scheme[key]["_filter"] = value["filter"]
         }

         if (value["source"]) {
            scheme[key] ||= {}
            scheme[key]["_source"] = value["source"]
         }

         return scheme
      }, {})
   }

   static getCleanState() {
      return {}
   }

   render() {
      console.log("[render] * this.state", this.state, "this.props", this.props)

      return (
         <form>
            <div className='row'>
               {renderElement({value: this.state.query}, this.props.meta)}</div>
            <div className='row'>
               <ErrorSpan
                  appendClassName='form'
                  error={this.state.error} /></div></form>)
   }
}