Search code examples
material-uiffireasonbucklescriptreason-react

How to compose props across component in reason-react bindings?


I am currently writing a material-UI reason-react binding and I want to know how I can re-use previously define Props.

The Select component spreads all of the Input props into itself, in the underlying react-js lib. this is done by spreading props however this is discouraged in ReasonML as typings are lost.

As a temporary solution, I have copied the props from one to another but this is not scalable. I would appreciate if someone could suggest what is the correct way of doing this in Reason-React?

Thanks

Input module definiton:

module Input = {
  [@bs.module "material-ui/Input"] external reactClass : ReasonReact.reactClass = "default";
  let make =
      (
        ~disableUnderline: option(bool)=?,
        ~disabled: option(bool)=?,
        ~error: option(bool)=?,
        ~autoFocus: option(bool)=?,
        ~fullWidth: option(bool)=?,
        ~style: option(ReactDOMRe.style)=?,
        ~value: option(string)=?,
        ~onChange: option((ReactEventRe.Form.t => unit))=?,
        ~placeholder: option(string)=?,
        ~className: option(string)=?,
        ~inputType: option(string)=?,
        children
      ) =>
    ReasonReact.wrapJsForReason(
      ~reactClass,
      ~props=
        Js.Nullable.(
          {
            "disableUnderline": unwrap_bool(disableUnderline),
            "disabled": unwrap_bool(disabled),
            "error": unwrap_bool(error),
            "fullWidth": unwrap_bool(fullWidth),
            "autoFocus": unwrap_bool(autoFocus),
            "style": from_opt(style),
            "placeholder": from_opt(placeholder),
            "className": from_opt(className),
            "type": from_opt(inputType),
            "value": from_opt(value),
            "onChange": from_opt(onChange)
          }
        ),
      children
    );
};

Select module definiton:

    module Select = {
  [@bs.module "material-ui/Select"] external reactClass :  ReasonReact.reactClass = "default";
  let make =
      (
        ~autoWidth: option(bool)=?,
        ~classes: option(Js.t({..}))=?,
        ~className: option(string)=?,
        ~displayEmpty: option(bool)=?,
        ~input: option(ReasonReact.reactElement)=?,
        ~inputClasses: option(Js.t({..}))=?,
        ~native: option(bool)=?,
        ~multiple: option(bool)=?,
        ~menuProps: option(Js.t({..}))=?,
        ~renderValue: option((unit => unit)),
        ~value: option('a)=?,
        ~style: option(ReactDOMRe.style)=?,
        /* Input Props*/
        ~disableUnderline: option(bool)=?,
        ~disabled: option(bool)=?,
        ~error: option(bool)=?,
        ~autoFocus: option(bool)=?,
        ~fullWidth: option(bool)=?,
        ~value: option(string)=?,
        ~onChange: option((ReactEventRe.Form.t => unit))=?,
        ~placeholder: option(string)=?,
        ~className: option(string)=?,
        ~inputType: option(string)=?,
        children
      ) =>
    ReasonReact.wrapJsForReason(
      ~reactClass,
      ~props=
        Js.Nullable.(
          {            
            "autoWidth": unwrap_bool(autoWidth),
            "classes": from_opt(classes),
            "className": from_opt(className),
            "displayEmpty": unwrap_bool(displayEmpty),
            "input": from_opt(input),
            "InputClasses": from_opt(inputClasses),
            "native": unwrap_bool(native),
            "multiple": unwrap_bool(multiple),
            "MenuProps": from_opt(menuProps),
            "renderValue": from_opt(renderValue),
            "value": from_opt(value),
            "style": from_opt(style),
            /* Input Props*/
            "disableUnderline": unwrap_bool(disableUnderline),
            "disabled": unwrap_bool(disabled),
            "error": unwrap_bool(error),
            "fullWidth": unwrap_bool(fullWidth),
            "autoFocus": unwrap_bool(autoFocus),
            "style": from_opt(style),
            "placeholder": from_opt(placeholder),
            "className": from_opt(className),
            "type": from_opt(inputType),
            "value": from_opt(value),
            "onChange": from_opt(onChange)
          }
        ),
      children
    );
};

Solution

  • You can use currying and Js.Obj.assign to achieve this:

    let common = (reactClass, props, ~commonProp1, ~commonProp2, children) =>
      ReasonReact.wrapJsForReason(
        ~reactClass,
        ~props=Js.Obj.assign(props, {
          "commonProp1": commonProp1,
          "commonProp2": commonProp2
        }),
        children
      );
    
    module Input = {
      [@bs.module "material-ui/Input"] external reactClass :  ReasonReact.reactClass = "default";
      let make = (~inputProp1, ~inputProp2) => common(reactClass, {
        "inputProp1": inputProp1,
        "inputProp2": inputProp2
      });
    };
    
    module Select = {
      [@bs.module "material-ui/Select"] external reactClass :  ReasonReact.reactClass = "default";
      let make = (~selectProp1, ~selectProp2) => common(reactClass, {
        "selectProp1": selectProp1,
        "selectProp2": selectProp2
      });
    };
    

    In each make function common is partially applied, and due to currying will "extend" the make function with its own arguments. In effect, the type signature of e.g. Input.make will be ~inputProp1 => ~inputProp2 => ~commonProp1 => ~commonProp2 => ....