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
The following code for App.js is roughly the sample from the component docs, and works nicely:
import React from 'react';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.
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;
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.