├── LICENSE ├── README.md └── ReactDataSheet.fs /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Jordan Marr 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Fable.ReactDataSheet 2 | A Fable wrapper around nadbm/react-datasheet for creating Excel-like tables 3 | 4 | ![image](https://user-images.githubusercontent.com/1030435/149204330-ae6d39d7-2e6e-499b-adf0-e0694f714e5d.png) 5 | 6 | ## Features 7 | * Great for quickly entering data 8 | * Row selection, tab and enter works just like in Excel 9 | * Supports copy/paste of multiple cells to/from Excel 10 | 11 | See documentation for the underlying React library here: 12 | https://github.com/nadbm/react-datasheet 13 | 14 | Demo: 15 | https://nadbm.github.io/react-datasheet/ 16 | 17 | Many (but not all) of the features are supported in this wrapper. 18 | 19 | ## Basic Usage 20 | ```fsharp 21 | 22 | let rows = [| 23 | [| Cell.Create "ABC"; Cell.Create 123 |] 24 | [| Cell.Create "DEF"; Cell.Create 456 |] 25 | |] 26 | 27 | ReactDataSheet [ 28 | Data rows 29 | ] 30 | ``` 31 | 32 | ## More Advanced Usage 33 | ```fsharp 34 | ReactDataSheet [ 35 | Data model.Rows 36 | OnCellsChanged (fun changes added -> 37 | let rows = mergeChanges model.Rows changes added 38 | dispatch (UpdateRows rows) 39 | ) 40 | SheetRenderer (fun e -> 41 | table [Class e.className] [ 42 | thead [] [ 43 | tr [] [ 44 | th [Class "cell"; Style [Background "#e9f0f7"; Width "32px"]] [] 45 | th [Class "cell"; Style [Background "#e9f0f7"; Width "250px"]] [str "Sheet Number"] 46 | th [Class "cell"; Style [Background "#e9f0f7"; Width "820px"]] [str "Sheet Name"] 47 | ] 48 | ] 49 | tbody [] [ 50 | e.children 51 | ] 52 | ] 53 | ) 54 | RowRenderer (fun e -> 55 | tr [] [ 56 | td [Class Classes.cell; Style[Background "#e9f0f7"]] [] 57 | e.children 58 | ] 59 | ) 60 | ] 61 | ``` 62 | 63 | # Installation 64 | * `npm install react-datasheet --save` 65 | * Copy `ReactDataSheet.fs` into your codebase (maybe I'll add a nuget package later) 66 | * Import css file: `importAll "../node_modules/react-datasheet/lib/react-datasheet.css"` 67 | * Profit! 68 | 69 | -------------------------------------------------------------------------------- /ReactDataSheet.fs: -------------------------------------------------------------------------------- 1 | // fsharplint:disable RecordFieldNames 2 | module Fable.ReactDataSheet 3 | 4 | open System 5 | open Fable.Core 6 | open Fable.React 7 | open Fable.React.Helpers 8 | 9 | type ParseResult = string[][] 10 | 11 | let toParseResult (results: string seq) = 12 | results 13 | |> Seq.map (fun s -> [| s |]) 14 | |> Seq.toArray 15 | 16 | type ReactDatasheetProps = 17 | /// An array of cell arrays. 18 | | Data of Row [] 19 | /// Provides the ability to override specific cells view mode. 20 | | ValueRenderer of (Cell -> RowIndex -> ColumnIndex -> ReactElement) 21 | /// Provides the ability to override specific cells edit mode. 22 | | DataRenderer of (Cell -> RowIndex -> ColumnIndex -> ReactElement) 23 | /// Provides the ability to handle changes in specific cells. 24 | | OnCellsChanged of (CellsChangedArgs array -> CellsAddedArgs array -> unit) 25 | /// Determines whether the keyboard can navigate to a cell. 26 | | IsCellNavigable of (Cell -> RowIndex -> ColumnIndex -> bool) 27 | | Overflow of Overflow 28 | | Selected of Selection 29 | | OnSelect of (Selection -> unit) 30 | /// A global cell viewer override (affects all cells). 31 | | ValueViewer of (ValueViewerProps -> string) 32 | /// A global cell editor override (affects all cells). 33 | | DataEditor of (DataEditorProps -> ReactElement) 34 | | SheetRenderer of (SheetRendererProps -> ReactElement) 35 | | RowRenderer of (RowRendererProps -> ReactElement) 36 | /// Provides the ability to transform pasted data. Usage ex: [ "1"; "2" ] |> toParseResult 37 | | ParsePaste of (string -> ParseResult) 38 | 39 | and Row = Cell [] 40 | and ColumnIndex = int 41 | and RowIndex = int 42 | and 43 | [] 44 | [] 45 | Overflow = | Wrap | NoWrap | Clip 46 | 47 | and Cell = { 48 | value: obj 49 | ``component``: ReactElement option 50 | } 51 | with 52 | static member Create(value, ?cmp) = 53 | { Cell.value = value 54 | Cell.``component`` = cmp } 55 | 56 | /// Creates a read-only cell. 57 | static member CreateRO(value) = 58 | { Cell.value = value 59 | Cell.``component`` = Some (span [] [ str $"{value}" ]) } 60 | 61 | and CellsChangedArgs = { 62 | cell: Cell 63 | row: RowIndex 64 | col: ColumnIndex 65 | /// The new value 66 | value: obj 67 | } 68 | 69 | and CellsAddedArgs = { 70 | row: RowIndex 71 | col: ColumnIndex 72 | /// The new value 73 | value: obj 74 | } 75 | 76 | and Location = { 77 | i: RowIndex 78 | j: ColumnIndex 79 | } 80 | 81 | and Selection = { 82 | start: Location 83 | ``end``: Location 84 | } 85 | with 86 | static member Create(startRow, startCol, endRow, endCol) = 87 | { start = {i = startRow; j = startCol} 88 | ``end`` = {i = endRow; j = endCol} } 89 | 90 | and DataEditorProps = { 91 | value: obj 92 | row: RowIndex 93 | col: ColumnIndex 94 | cell: Cell 95 | onChange: (string -> unit) 96 | onKeyDown: (Browser.Types.KeyboardEvent -> unit) 97 | onCommit: (obj -> unit) 98 | onRevert: (unit -> unit) 99 | } 100 | 101 | and ValueViewerProps = { 102 | value: obj 103 | row: RowIndex 104 | col: ColumnIndex 105 | cell: obj 106 | } 107 | 108 | and SheetRendererProps = { 109 | data: Row [] 110 | className: string 111 | children: ReactElement 112 | } 113 | 114 | and RowRendererProps = { 115 | row: int 116 | cells: Cell [] 117 | children: ReactElement 118 | } 119 | 120 | module Classes = 121 | let cell = "cell" 122 | 123 | let defaultValueRenderer (cell: Cell) (row: RowIndex) (col: ColumnIndex) = 124 | match cell.value with 125 | | null -> str "" 126 | | value -> str $"{value}" 127 | 128 | let prepareProps (props: ReactDatasheetProps seq) = 129 | // Default the ValueRenderer (so the user doesn't have to add it every time) 130 | let props = 131 | if props |> Seq.exists(function | ValueRenderer _ -> true | _ -> false) 132 | then props 133 | else Seq.append (seq { ValueRenderer defaultValueRenderer }) props 134 | 135 | JsInterop.keyValueList CaseRules.LowerFirst props 136 | 137 | let ReactDataSheet props = 138 | ofImport "default" "react-datasheet" (prepareProps props) [] 139 | 140 | let defaultDataEditor (props: DataEditorProps) = 141 | ofImport "DataEditor" "react-datasheet" props [] 142 | 143 | open System 144 | open Microsoft.FSharp.Collections 145 | 146 | /// A helper function that can be used within OnCellsChanged to update Data with new and edited cells. 147 | let mergeChanges (rows: Row[]) (changes: CellsChangedArgs []) (added: CellsAddedArgs[]) = 148 | let rows = rows |> Array.copy 149 | 150 | for c in changes do 151 | rows.[c.row].[c.col] <- 152 | match c.cell.``component`` with 153 | | Some comp -> Cell.Create(c.value, comp) 154 | | None -> Cell.Create(c.value) 155 | 156 | let added = if added |> isNull then [||] else added // React lib returns null if empty 157 | 158 | let maxRowIdx = 159 | if added = Array.empty then -1 160 | else added |> Array.map (fun a -> a.row) |> Array.max 161 | 162 | let newRows = 163 | if maxRowIdx > (rows.Length - 1) then 164 | let startRowIdx = rows.Length 165 | [|for rowIdx in [startRowIdx .. maxRowIdx] do 166 | match rows |> Array.tryHead with 167 | | Some firstRow -> 168 | firstRow |> Array.map (fun c -> { c with value = "" }) 169 | | None -> 170 | [||] 171 | |] 172 | else 173 | [||] 174 | 175 | 176 | let data = Array.append rows newRows 177 | 178 | for a in added do 179 | if a.row < data.Length then 180 | let row = data.[a.row] 181 | if a.col < row.Length then 182 | let cell = data.[a.row].[a.col] 183 | data.[a.row].[a.col] <- 184 | { cell with value = a.value } 185 | 186 | data 187 | 188 | 189 | 190 | --------------------------------------------------------------------------------