Monday 15 June 2020

Install a third party npm package for Fable

One of the things I would like to achieve with this new blog is to document the process I use for expanding my skill set. I'm currently in a phase of taking my recent development experience (from the past four years) of on-premise server side F# with EventStore and RX and client side React.js and JavaScript with a view to expanding this to cover containerised cloud based services and using F# for the front end as well.

I have used React and JavaScript enough over the past few years to consider myself to have some solid skills. I'm not a web designer, but I can hopefully put together a pretty competent and reasonable looking web application. Whilst I have used F# for all server side development, so far I haven't done much more than dabble with the likes of Fable, Elmish and the SAFE stack. The reason for this is really a combination of lack of time, and a concern about being able to move as fast with the F# web stack as I can using React, JavaScript and everything npm has to offer.

During this weird time in which we are currently living, I suddenly find myself with enough spare time to make a real go at coming up with a cloud based application with enough scope to really get deep into Fable and Elmish - something representative of a mid-complexity enterprise application.

Importing an npm component for use in Elmish React


The task for today is to find out what is needed to take a third party component from npm, and wrap this as required in order for it to be used in an idiomatic way for Elmish.

The component I have chosen is https://uiwjs.github.io/react-md-editor/ which is an editor component allowing for editing and display of Markdown. It's sufficiently complex to be a reasonable test of this process - if I can get this working nicely, I should then be able to import pretty much anything else I need, and it should also give me a reference for working out how quickly I can bring in these components in the future.

Try it first in JavaScript


For me, I feel the first step should be to try this out in a "raw" React / JavaScript setting - so that I can see how the component is supposed to work, and make sure I fully understand its interfaces and configuration. I will create an empty create-react-app web app and add this package to it.

The following code for App.js is roughly the sample from the component docs, and works nicely:
import React from 'react';
import './App.css';
import MDEditor from '@uiw/react-md-editor';
function App() {
  const [markdown,setMarkdown] = React.useState(`## Welcome to Markdown
This is just some random Markdown text.`);
  return (<div >
              <p>Edit your Markdown below:</p>
              <MDEditor value={markdown} onChange={setMarkdown} preview='edit' />
              <MDEditor.Markdown source={markdown} />
          </div>);
}
export default App;
Its usage is pretty simple, but I will need to understand the properties it takes, so that I can expose a set of F# types which map to each of these, as well as something which wraps the component itself.

Working out what we need to expose


Basically this is a matter of looking into either the github repo or even the code of the npm module itself. In this case the library is written in Typescript and it is pretty easy to work out what we need to expose (from MDEditor.tsx):

export interface MDEditorProps extends 
    Omit<React.HTMLAttributes<HTMLDivElement>, 'onChange'>, IProps {
  /**
   * The Markdown value.
   */
  value?: string;
  
  ...
}
export interface MDEditorState {
  ...
}
export default class MDEditor extends React.PureComponent<MDEditorProps, MDEditorState> {
  ...
}

Create a bare bones F# implementation


It's possible that I'm being a bit dense, but the documentation around this area in Fable is a little threadbare, specifically when looking to build a component usable for React and which matches the pattern used by Elmish. I am very new to this, so it is quite possible I'm trying to fit a round peg into a square hole.

I have taken a combination of the official fable docs as well as this article by Zaid Ajaj for reference here. It also took a bit of digging around in the fable-react repo to get some clues.

open Fable.React
open Fable.React.ReactBindings
open Fable.Core
[<RequireQualifiedAccess>]
module ReactMdEditor =
    type PreviewKind =
        | Live 
        | Edit 
        | Preview 
    type internal MDEditorProps = {
        value: string option
        onChange: (string -> unit) option
        preview: string
    } with static member empty = { value = None; onChange = None; preview = "edit" }
    type internal MDEditorState = interface end
    type Props =
        | Value of string
        | OnChange of (string -> unit)
        | Preview of PreviewKind
    let internal parseProps propList =
        let parseProp (props:MDEditorProps) = function
            | Value s -> { props with value = Some s }
            | OnChange oc -> { props with onChange = Some oc }
            | Preview pk -> { props with preview = (sprintf "%A" pk).ToLower() }
        propList |> List.fold parseProp MDEditorProps.empty
    [<ImportDefault("@uiw/react-md-editor")>]
    let internal mDEditor: PureComponent<MDEditorProps,MDEditorState> = jsNative
    let MDEditor (props:Props list) (children:ReactElement list) = React.createElement(mDEditor, props |> parseProps, children)
The key to getting this working was not just the [<ImportDefault>] attribute, but critically the use of the React.createElement function, which was hidden in Fable.React.ReactBindings.

As I said, I have tried to shape this in a similar manner to, for example, the components in the Fulma library, but it has to be a different approach, as in that case all the elements are created by decorating html elements with classes. Even so, I feel like I am missing a step here, and will quite likely need to revisit this.

That said, I can use something like the following in my view code:
let markdown = """# Heading here
                  - and
                  - a 
                  - list
                  Some text here also."""
[ ReactMdEditor.MDEditor [ReactMdEditor.Value markdown;ReactMdEditor.Props.Preview ReactMdEditor.Edit] []] 


There are further properties to be added here, but the above is enough to get me going for now.

Findings


As I said, I think I am missing something here, but in the end I have this component working now within my application. The steps above are repeatable for other packages, and whilst it's clearly more faff than just typing import x from y, for simple components like this it would likely be a matter of 10 to 30 minutes work to add a few wrapper types.


A new dawn

Unfortunately, after years of neglect, my previous blog 'roundthecampfire' is no more. Whilst I have a backup of the blog text, sadly the images were on azure storage for an account that I closed a while back. Over time, I may try and restore some of the more worthwhile posts here, but the main intent is to start afresh and try to post with a sustainable cadence!