Source: templates/Live.js

import React, { useCallback, useMemo } from "react";
import { LiveError, LivePreview, LiveProvider } from "react-live";
import { components } from "./Template.helpers";
import { GenericContext, booleanString, emptyObject, processOptions, useReactorNavigation, useVars, emptyArray, processFn } from "../../_core";
import { useDispatch, useSelector } from "react-redux";
import { appActions } from "../../redux";
import { _ } from "../../_core/hooks/useLodash";
import objectPath from "object-path";
import { getStyleObjectFromString } from "../../utils";
const immutable = require("object-path-immutable");

const merge = require('deepmerge');

const functions = {
  lodash: () => _,
  objectPath: () => objectPath,
  immutable: () => immutable,
}

/**
 * Componente que transforma un texto en un componente React dándole un contexto limitado para su funcionamiento.
 * @name Reactor.Components.Templates.Live
 * @param {string} template Un código react que tendra acceso a un contexto limitado (componentes, React y useVariables)
 * @param {JsonString} [div_options = "{}"] Propiedades que se envian al componente div que envuelve el live
 * @param {JsonString} [params = "{}"] Parametros que se envian al componente live (solo cuando se llama a un componente live desde el parametro template)
 * @class 
 * @example 
<live template='
  () => {
    const [count, setCount] = React.useState(0);
    React.useEffect(()=>{
      var interval = setInterval(() => {
        setCount(count => count + 1)
      }, 1000);
      return ()=>clearInterval(interval);
    }, [])
    return (
      <center>
        <h3>
          {count}
        </h3>
      </center>
    )

  }
'>
</live>
**************************
codigo de template live

() => {
  //params:
      //variable: string, nombre de la variable que se va a modificar
      //context: string, contexto al que pertenece la variable
      //defaultValue: number, el valor por defecto
      //count: cantidad de botones
      //max: number, el valor maximo posible

  //inicializacion de parametros
  const { 
    variable = "botoneta", 
    context = "live", 
    defaultValue,
    count = 5, 
    max = Number.MAX_SAFE_INTEGER, 
  } = params;

  //variables de contexto
  const [value, setValue] = useVars({variable, context, defaultValue});
  const [editing, setEditing] = useVars({variable: `${variable}_editing`, context, defaultValue: false});
  const [disabled, setDisabled] = useVars({variable: `${variable}_disabled`, context, defaultValue: false});

  //referencias interna
  const inputRef = React.useRef();
  
  const botoncitos = Array(count).fill(0).map((n, index) => index + 1);

  //cuando hace click en el boton mas disminuye en uno el valor (si no es menor a cero)  
  const handleMinusClick = (event) => {
    if(disabled) return;
    event.preventDefault();
    const currentValue = value || 0;
    setValue(currentValue - 1 < 0 ? 0 : currentValue - 1);
  }
  
  //cuando hace click en el boton mas aumenta en uno el valor (si no excede params.max)  
  const handlePlusClick = (event) => {
    if(disabled) return;
    event.preventDefault();
    const currentValue = value || 0;
    setValue(currentValue + 1 > max ? currentValue : currentValue + 1);
  }
  
  //cuando hace click en el boton del numero establece ese numero como valor
  const handleNumberClick = (number, event) => {
    if(disabled) return;
    event.preventDefault();
    setValue(number);
  }

  //cuando hace click en el numero inicializa la edicion a mano, hace foco en el input y deshabilita el control
  const handleValueClick = (event) => {
    if(disabled) return;
    event.preventDefault();
    setEditing(true);
    setTimeout(()=>inputRef.current.focus(), 1);
  }
  
  //cuando se hace foco en el input se selecciona todo el texto
  const handleEditingFocus = () => {
    inputRef.current.select();
  };

  //cuando se pierde el foco del input se sale de la edicion
  const handleEditingBlur = () => {
    setEditing(false);
  };
  
  //cuando esta editando y se cambia el valor lo guarda temporalmente (siempre que sea valido)
  const handleEditingChangeValue = (event) => {
    const validate = inputRef.current.validity.valid && 
                      ( event.target.value === "" ||
                        (
                          parseInt(event.target.value) >= 0 && 
                          parseInt(event.target.value) <= max
                        )
                      );
    if(validate){
      setValue(event.target.value);
    }
  }
  

  return (
    <div className="botoneta wrapper">
      {botoncitos.map(n => <a href="#" className={`botoneta button ${(n === value ? "active" : "")}`} onClick={(event) => handleNumberClick(n, event)}>{n}</a>)}
      <a href="#" className="botoneta button minus" onClick={handleMinusClick}>-</a>
      {editing &&
        <input className="botoneta input" ref={inputRef} type="number" value={value} onChange={handleEditingChangeValue} onFocus={handleEditingFocus} onBlur={handleEditingBlur} pattern="^\d+$"/>
      }
      {!editing &&
        <div className="botoneta value" onClick={handleValueClick}>{value || 0}</div>
      }
      <a href="#" className="botoneta button plus" onClick={handlePlusClick}>+</a>
      
      {!value &&
        children
      }
    </div>
  )
}
**************************
para llamarlo desde otro template:
<live 
  template="LiveBotoneta" 
  params='{"count": 8, "variable": "noches", "context": "test", "max": 10, "defaultValue": 2}' 
  show_errors="true"
>
  <h1>hola</h1>
</live>

*/

const regexp = /{{((\w+)(.\w+|\[\d+\])+?)}}/g
const resolveMatches = (templateBody, templates) => {
  if(!templateBody) return "";
  const matches = templateBody.match(regexp);
  return !matches ? templateBody : matches.reduce((prevValue, match) => {
    const matchText = match.replace(/{{|}}/g, "");
    const nameTemplate = matchText.toLowerCase();
    const template = templates[nameTemplate];
    if(!template) return prevValue;
    const content = resolveMatches(template?.body || "", templates);
    return prevValue.replace(match, content);
  }, templateBody);
}

const Live = ({template, params, children, show_errors = "false", no_div}) => {
  const templates = useSelector(state => state.app.templates);
  const dispatch = useDispatch();

  const code = useMemo(() => {
    const templateBody = templates[template?.toLowerCase()]?.body;
    if(!!templateBody){
      return resolveMatches(templateBody, templates);
    }else{
      return template;
    }
  }, [template, templates]);

  const scope = useMemo(() => ({
    useVars,
    useReactorNavigation,
    getComponent: (name) => components[name](),
    getAsyncContent: (params) => {
      const {operation, data, token} = params;
      return dispatch(appActions.getAsyncContent(operation, token, data));
    },
    getAsyncContentUntoken: (params) => {
      const {operation, data, token} = params;
      return dispatch(appActions.getAsyncContentUntoken(operation, data, token));
    },
    LiveContext: GenericContext,
    children,
    params: processOptions(params, emptyObject, emptyObject),
    emptyObject,
    emptyArray, 
    merge,
    getFunction: (name) => functions[name](),
    getStyleObjectFromString,
    processFn,
  }), [children, params]);

  const component = useMemo(() => booleanString(no_div) ? NoDivComponent : "div", [no_div]);

  return (
    <>
      {scope &&
        <LiveProvider disabled={true} code={code} noInline={false} scope={scope}>
          <LivePreview Component={component}/>
          {booleanString(show_errors) && <LiveError />}
        </LiveProvider>
      }
    </>
  );
}

const NoDivComponent = ({children}) =>{

  return (
    <>{children}</>
  )
}

export default Live;