Source: data/FormExt.js

import React, { useCallback, useEffect, useMemo, useRef, useState } from "react";
import { booleanString, useReactorNavigation, useWebAuthn } from "../../_core";
import { FormContext, processIncludeVariables, useValidationSchema, uuid } from "../..";
import objectPath from "object-path";
import { useSelector } from "react-redux";

/**
 * Componente que renderiza un formulario con opciones extendidas.
 * Todos los input dentro de Form deben contener la propiedad name con el identificador del campo
 * @name Reactor.Components.Data.FormExt
 * @param {string!} action Nombre de la operación de destino
 * @param {JsonString} data Datos para agregarse al form y evitar inputs de tipo hidden
 * @param {string?} include_variables Lista de variables que se incluyen en la data (separadas por espacio)
 * @param {string?} context Contexto contenedor de las variables que se van a incluir, es requerido si se incluyen variables
 * @param {NavigationTarget} [target = "menu"] Destino del resultado 
 * @param {DirectionType} direction Dirección en la que va a mostrarse el menú
 * @param {string?} offcanvas_class Clase css que se va a dar al menú 
 * @param {string!} to Nombre del FrameDiv de destino. Requerido para el uso de FormExt
 * @param {BooleanString} [use_token = "false"] Si es true guarda el token que devuelve Orquesta, sirve para luego poder continuar el flujo con otro form, si es false no se guarda y el flujo termina
 * @param {BooleanString} [use_prev_token = "true"] Si es true se envía el token que está en memoria, sirve para continuar la ejecución de un flujo, si es false el token se envía null
 * @param {BooleanString} [use_spinner = "true"] Si es true se muestra un spinner mientras se espera la respuesta del servidor
 * @param {string} [change_delay = "0"] Establece un tiempo de espera muy corto para que los cambios se vean reflejados en los inputs controlados por React
 * @param {BooleanString} [debug = "false"] Establece modo de depuracion
 * @param {BooleanString} [webauthn_check = "false"] Establece si debe requerir autenticacion por medio de webauthn para ejecutar el evento submit
 * @class
 * @example
<form_ext 
  action="ProductoDepositar" 
  target="menu" 
  direction="bottom" 
  offcanvas_class="rounded-m" 
  to="ProductoDepositar" 
  use_token="true" 
  use_prev_token="false" 
  use_spinner="false" 
  include_variables="variable1 variable2" 
  context="un_contexto"
> 
  <div class="menu-size" style="height:auto;" >
    <div class="content mt-0 mb-2">
      <div class="form-custom form-label form-icon">
        <i class="bi bi-check-circle font-13"></i>
        <select class="form-select rounded-xs" name="medioPago" value="{{opcionSeleccionada}}" aria-label="">
          <optgroup label="Medios de Pago">          
            {{opciones|OpcionCombo}}
          </optgroup>
        </select>
        <label for="medioPago" class="form-label-always-active color-highlight font-11">Medio de pago</label>
      </div>
      <div class="pb-3"></div>
      <div class="form-custom form-label form-icon">
        <i class="bi bi-hash font-14"></i>
        <input type="number" class="form-control rounded-xs" name="importe" placeholder="ingrese el monto">
        <label for="importe" class="form-label-always-active color-highlight font-11">Importe</label>
        <span class="font-10">( Moneda: $ ARG )</span>
      </div>
      <div class="pb-3"></div>
      <input 
        type="submit" 
        class="form-control rounded-xs btn btn-full gradient-highlight shadow-bg shadow-bg-s" 
        value="Depositar" 
        offcanvas_class="rounded-m"
      />
    </div>
  </div>
</form_ext>
*/

export const FormExt = ({ action, direction, target, children, offcanvas_class, data = "{}", to = "_", use_token = "false", use_prev_token = "true", use_spinner = "true", change_delay = "0", include_variables, context, debug = "false", webauthn_check = "false"}) => {
  
  const variablesState = useSelector(state => context === "_" ? state.app.variables : state.app.variables[context] ) || null;
  const navigation = useReactorNavigation({target, to: action, direction, offcanvasClass: offcanvas_class});
  const ref = useRef();
  const [ formName ] = useState(() => `form_${uuid()}`);
  const [ state, setState ] = useState({ send: 0, sender: "", timer: 0, formInputData: null, formInputDataStr: "", sendEvent: false, delay: null })
  const validation = useValidationSchema({debug});
  const { assertionCheck } = useWebAuthn();

  useEffect(()=>{
    return ()=>{
      ref.current = null;
    }
  }, [])

  const getVariables = useCallback(() => {
    return context && processIncludeVariables({variablesState, include_variables}); 
  }, [context, variablesState, include_variables]);

  const getDelay = useMemo(() => {
    if(state.delay === "0" || state.delay === 0){
      return 0;
    }
    if(state.delay) return state.delay * 1;
    if(change_delay){
      return change_delay * 1;
    }
    return 0;
  }, [state.delay, change_delay])


  useEffect(() => {
    if(!to === "_" || !to || state.sender === "" || state.send === 0 || !state.sendEvent ) return;
    if(state.timer !== 0){
      clearTimeout(state.timer);
    }
    const delay = getDelay;
    let newTimer;
    if(delay === 0){
      if(state.formInputDataStr !== ""){
        const newInputFormData = {...getFormData(ref.current), ...JSON.parse(data), event: "change", element: state.sender, ...getVariables()};
        navigation.ToFrameDiv(newInputFormData, action, formName, to, null, use_token === "true", use_prev_token === "true", use_spinner === "true"  ) 
      }
      newTimer = 0;
    }else{
      newTimer = setTimeout(() => {
        const newInputFormData = {...getFormData(ref.current), ...JSON.parse(data), event: "change", element: state.sender, ...getVariables()};
        setState(state=>({...state, formInputData: newInputFormData}));
        navigation.ToFrameDiv(newInputFormData, action, formName, to, null, use_token === "true", use_prev_token === "true", use_spinner === "true"  ) 
      }, delay);  
    }
    setState(state => ({...state, timer: newTimer}) );
    return () => clearTimeout(newTimer);
  }, [state.send, state.sendEvent, getDelay, state.sender, state.formInputDataStr])

  const submit = (data) => {
    if(booleanString(webauthn_check)){
      assertionCheck()
        .then(()=>{
          navigation.execute(data);
        })
        .catch(()=>{});
    }else{
      navigation.execute(data);
    }    
  }

  const handleSubmit = (event) => {
    const allData = {...getFormData(ref.current), ...JSON.parse(data), event: "submit", ...getVariables()};
    event?.preventDefault();
    validation.isValidFn(allData, submit);
  }
  
  const handleChange = (event) => {
      const newInputFormData = {...getFormData(ref.current), ...JSON.parse(data), event: "change", element: event.target.name, ...getVariables()};
      const newSendEvent = (!event?.target?.getAttribute) || event?.target?.getAttribute("use_change") !== "false";
      const newDelay = (newSendEvent && event?.target?.getAttribute) ? (event.target.getAttribute("delay") ? event.target.getAttribute("delay") * 1 : null) : null;
      setState(state => ({...state, send: state.send + 1, sender: event && event.target && event.target.name, sendEvent: newSendEvent, delay: newDelay,  formInputData: newInputFormData, formInputDataStr: JSON.stringify(newInputFormData)}) );
  }
 
  return (
    <FormContext.Provider value={{submit: handleSubmit, change: handleChange, formInputData: state.formInputData, validation}}>
      <form 
        ref={ref}
        onSubmit={handleSubmit}
        onChange={handleChange}
      >
        {children}
      </form>
    </FormContext.Provider>
  )
}

export const getFormData = (form) => {
  if(!form) return {};
  const formData = new FormData(form);
  const formEntries = Object.fromEntries(formData.entries());
  let result = {};
  Object.keys( formEntries ).forEach(key => {
    objectPath.set(result, key, formEntries[key]);
  });
  return result;
}