diff --git a/frameworks/keyed/grain/.gitignore b/frameworks/keyed/grain/.gitignore
new file mode 100644
index 000000000..30efe1997
--- /dev/null
+++ b/frameworks/keyed/grain/.gitignore
@@ -0,0 +1,10 @@
+/bower_components/
+/node_modules/
+/.pulp-cache/
+/output/
+/generated-docs/
+/.psc-package/
+/.psc*
+/.purs*
+/.psa*
+/.spago
diff --git a/frameworks/keyed/grain/index.html b/frameworks/keyed/grain/index.html
new file mode 100644
index 000000000..3e051d400
--- /dev/null
+++ b/frameworks/keyed/grain/index.html
@@ -0,0 +1,12 @@
+
+
+
+
+ Grain keyed
+
+
+
+
+
+
+
diff --git a/frameworks/keyed/grain/package.json b/frameworks/keyed/grain/package.json
new file mode 100644
index 000000000..9db1c6d19
--- /dev/null
+++ b/frameworks/keyed/grain/package.json
@@ -0,0 +1,26 @@
+{
+ "name": "js-framework-benchmark-grain",
+ "version": "1.0.0",
+ "description": "Grain demo",
+ "main": "index.js",
+ "js-framework-benchmark": {
+ "frameworkVersion": "1.0.0"
+ },
+ "scripts": {
+ "spago": "spago",
+ "clean": "rm -rf output",
+ "build-prod": "spago bundle-app -t dist/bundle.js && terser --compress --mangle --enclose --output=dist/main.js -- dist/bundle.js"
+ },
+ "author": "Shinya Takahashi ",
+ "license": "MIT",
+ "homepage": "https://github.com/krausest/js-framework-benchmark",
+ "repository": {
+ "type": "git",
+ "url": "https://github.com/krausest/js-framework-benchmark.git"
+ },
+ "dependencies": {
+ "purescript": "^0.13.8",
+ "spago": "^0.15.3",
+ "terser": "^4.8.0"
+ }
+}
diff --git a/frameworks/keyed/grain/packages.dhall b/frameworks/keyed/grain/packages.dhall
new file mode 100644
index 000000000..5142c8c2e
--- /dev/null
+++ b/frameworks/keyed/grain/packages.dhall
@@ -0,0 +1,131 @@
+{-
+Welcome to your new Dhall package-set!
+
+Below are instructions for how to edit this file for most use
+cases, so that you don't need to know Dhall to use it.
+
+## Warning: Don't Move This Top-Level Comment!
+
+Due to how `dhall format` currently works, this comment's
+instructions cannot appear near corresponding sections below
+because `dhall format` will delete the comment. However,
+it will not delete a top-level comment like this one.
+
+## Use Cases
+
+Most will want to do one or both of these options:
+1. Override/Patch a package's dependency
+2. Add a package not already in the default package set
+
+This file will continue to work whether you use one or both options.
+Instructions for each option are explained below.
+
+### Overriding/Patching a package
+
+Purpose:
+- Change a package's dependency to a newer/older release than the
+ default package set's release
+- Use your own modified version of some dependency that may
+ include new API, changed API, removed API by
+ using your custom git repo of the library rather than
+ the package set's repo
+
+Syntax:
+Replace the overrides' "{=}" (an empty record) with the following idea
+The "//" or "⫽" means "merge these two records and
+ when they have the same value, use the one on the right:"
+-------------------------------
+let overrides =
+ { packageName =
+ upstream.packageName // { updateEntity1 = "new value", updateEntity2 = "new value" }
+ , packageName =
+ upstream.packageName // { version = "v4.0.0" }
+ , packageName =
+ upstream.packageName // { repo = "https://www.example.com/path/to/new/repo.git" }
+ }
+-------------------------------
+
+Example:
+-------------------------------
+let overrides =
+ { halogen =
+ upstream.halogen // { version = "master" }
+ , halogen-vdom =
+ upstream.halogen-vdom // { version = "v4.0.0" }
+ }
+-------------------------------
+
+### Additions
+
+Purpose:
+- Add packages that aren't already included in the default package set
+
+Syntax:
+Replace the additions' "{=}" (an empty record) with the following idea:
+-------------------------------
+let additions =
+ { package-name =
+ { dependencies =
+ [ "dependency1"
+ , "dependency2"
+ ]
+ , repo =
+ "https://example.com/path/to/git/repo.git"
+ , version =
+ "tag ('v4.0.0') or branch ('master')"
+ }
+ , package-name =
+ { dependencies =
+ [ "dependency1"
+ , "dependency2"
+ ]
+ , repo =
+ "https://example.com/path/to/git/repo.git"
+ , version =
+ "tag ('v4.0.0') or branch ('master')"
+ }
+ , etc.
+ }
+-------------------------------
+
+Example:
+-------------------------------
+let additions =
+ { benchotron =
+ { dependencies =
+ [ "arrays"
+ , "exists"
+ , "profunctor"
+ , "strings"
+ , "quickcheck"
+ , "lcg"
+ , "transformers"
+ , "foldable-traversable"
+ , "exceptions"
+ , "node-fs"
+ , "node-buffer"
+ , "node-readline"
+ , "datetime"
+ , "now"
+ ]
+ , repo =
+ "https://github.com/hdgarrood/purescript-benchotron.git"
+ , version =
+ "v7.0.0"
+ }
+ }
+-------------------------------
+-}
+
+
+let upstream =
+ https://github.com/purescript/package-sets/releases/download/psc-0.13.8/packages.dhall sha256:0e95ec11604dc8afc1b129c4d405dcc17290ce56d7d0665a0ff15617e32bbf03
+
+let overrides =
+ { grain =
+ upstream.grain // { dependencies = [ "web-html" ], version = "v1.0.0" }
+ }
+
+let additions = {=}
+
+in upstream // overrides // additions
diff --git a/frameworks/keyed/grain/spago.dhall b/frameworks/keyed/grain/spago.dhall
new file mode 100644
index 000000000..b3f04dae4
--- /dev/null
+++ b/frameworks/keyed/grain/spago.dhall
@@ -0,0 +1,9 @@
+{-
+Welcome to a Spago project!
+You can edit this file as you like.
+-}
+{ name = "js-framework-benchmark-grain"
+, dependencies = [ "grain", "random" ]
+, packages = ./packages.dhall
+, sources = [ "src/**/*.purs" ]
+}
diff --git a/frameworks/keyed/grain/src/Main.purs b/frameworks/keyed/grain/src/Main.purs
new file mode 100644
index 000000000..ad8b5eacc
--- /dev/null
+++ b/frameworks/keyed/grain/src/Main.purs
@@ -0,0 +1,260 @@
+module Main where
+
+import Prelude
+
+import Control.Monad.Rec.Class (Step(..), tailRecM)
+import Data.Array (delete, length, snoc, unsafeIndex, updateAtIndices, (!!))
+import Data.Maybe (Maybe(..))
+import Data.Newtype (class Newtype, over)
+import Data.Tuple (Tuple(..))
+import Effect (Effect)
+import Effect.Random (randomInt)
+import Grain (class GlobalGrain, GProxy(..), VNode, fromConstructor, mount, useFinder, useUpdater, useValue)
+import Grain.Markup as H
+import Partial.Unsafe (unsafePartial)
+import Web.DOM.Element (toNode)
+import Web.DOM.ParentNode (QuerySelector(..), querySelector)
+import Web.HTML (window)
+import Web.HTML.HTMLDocument (toParentNode)
+import Web.HTML.Window (document)
+
+main :: Effect Unit
+main = do
+ maybeEl <- window >>= document <#> toParentNode >>= querySelector (QuerySelector "#main")
+ case maybeEl of
+ Nothing -> pure unit
+ Just el ->
+ mount view $ toNode el
+
+view :: VNode
+view =
+ H.div # H.className "container" # H.kids
+ [ jumbotronView
+ , rowsView
+ , H.span
+ # H.className "preloadicon glyphicon glyphicon-remove"
+ # H.prop "aria-hidden" "true"
+ ]
+
+jumbotronView :: VNode
+jumbotronView = H.component do
+ findState <- useFinder (GProxy :: _ State)
+ updateState <- useUpdater (GProxy :: _ State)
+
+ let run_ count = do
+ State s <- findState
+ state <- addRows count $ State s { rows = [], selectedId = 0 }
+ updateState \_ -> state
+
+ run = run_ 1000
+
+ runLots = run_ 10000
+
+ add = do
+ state <- findState >>= addRows 1000
+ updateState \_ -> state
+
+ updateEveryTenth i rows =
+ case rows !! i of
+ Just row ->
+ updateEveryTenth (i + 10) $ updateAtIndices
+ [ Tuple i row { label = row.label <> " !!!" } ]
+ rows
+ Nothing -> rows
+
+ update_ = updateState $ over State \s -> s
+ { rows = updateEveryTenth 0 s.rows
+ }
+
+ clear =
+ updateState $ over State _ { rows = [], selectedId = 0 }
+
+ swapRows =
+ updateState $ over State \s ->
+ case s.rows !! 1, s.rows !! 998 of
+ Just row1, Just row2 ->
+ s { rows =
+ updateAtIndices
+ [ Tuple 1 row2, Tuple 998 row1 ]
+ s.rows
+ }
+ _, _ -> s
+
+ pure $ H.div # H.className "jumbotron" # H.kids
+ [ H.div # H.className "row" # H.kids
+ [ H.div # H.className "col-md-6" # H.kids
+ [ H.h1 # H.kids [ H.text "Grain keyed" ]
+ ]
+ , H.div # H.className "col-md-6" # H.kids
+ [ H.div # H.className "row" # H.kids
+ [ buttonView { id: "run", label: "Create 1,000 rows", onClick: run }
+ , buttonView { id: "runlots", label: "Create 10,000 rows", onClick: runLots }
+ , buttonView { id: "add", label: "Append 1,000 rows", onClick: add }
+ , buttonView { id: "update", label: "Update every 10th row", onClick: update_ }
+ , buttonView { id: "clear", label: "Clear", onClick: clear }
+ , buttonView { id: "swaprows", label: "Swap Rows", onClick: swapRows }
+ ]
+ ]
+ ]
+ ]
+
+rowsView :: VNode
+rowsView = H.component do
+ State { rows, selectedId } <- useValue (GProxy :: _ State)
+ updateState <- useUpdater (GProxy :: _ State)
+
+ let select row = updateState $ over State _ { selectedId = row.id }
+ remove row = updateState $ over State \s -> s
+ { rows = delete row s.rows
+ }
+
+ pure $ H.table
+ # H.className "table table-hover table-striped test-data"
+ # H.kids
+ [ H.tbody # H.kids
+ (rows <#> \row -> rowView { row, selected: row.id == selectedId, onSelect: select row, onRemove: remove row })
+ ]
+
+rowView :: { row :: Row, selected :: Boolean, onSelect :: Effect Unit, onRemove :: Effect Unit } -> VNode
+rowView { row, selected, onSelect, onRemove } =
+ let idTxt = show row.id
+ in H.tr
+ # H.key idTxt
+ # H.fingerprint (row.label <> if selected then "t" else "f")
+ # H.className (if selected then "danger" else "")
+ # H.kids
+ [ H.td # H.className "col-md-1" # H.kids [ H.text idTxt ]
+ , H.td # H.className "col-md-4" # H.kids
+ [ H.a # H.onClick (\_ -> onSelect) # H.kids [ H.text row.label ]
+ ]
+ , H.td # H.className "col-md-1" # H.kids
+ [ H.a # H.onClick (\_ -> onRemove) # H.kids [ iconView ]
+ ]
+ , H.td # H.className "col-md-6"
+ ]
+
+buttonView :: { id :: String, label :: String, onClick :: Effect Unit } -> VNode
+buttonView { id, label, onClick } =
+ H.div # H.className "col-sm-6 smallpad" # H.kids
+ [ H.button
+ # H.id id
+ # H.type_ "button"
+ # H.className "btn btn-primary btn-block"
+ # H.onClick (\_ -> onClick)
+ # H.kids [ H.text label ]
+ ]
+
+iconView :: VNode
+iconView =
+ H.fingerprint "remove-icon" $ H.span
+ # H.className "glyphicon glyphicon-remove"
+ # H.prop "aria-hidden" "true"
+
+type Row =
+ { id :: Int
+ , label :: String
+ }
+
+newtype State = State
+ { nextId :: Int
+ , rows :: Array Row
+ , selectedId :: Int
+ }
+
+derive instance newtypeState :: Newtype State _
+
+instance globalGrainState :: GlobalGrain State where
+ typeRefOf _ = fromConstructor State
+ initialState _ = pure $ State
+ { nextId: 1
+ , rows: []
+ , selectedId: 0
+ }
+
+addRows :: Int -> State -> Effect State
+addRows count (State s) =
+ tailRecM go { state: s, addedCount: 0 }
+ where
+ go { state, addedCount }
+ | addedCount >= count =
+ pure $ Done $ State state
+ | otherwise = do
+ a <- sample adjectives
+ c <- sample colours
+ n <- sample nouns
+ let id = state.nextId
+ label = a <> " " <> c <> " " <> n
+ state' = state
+ { nextId = state.nextId + 1
+ , rows = snoc state.rows { id, label }
+ }
+ pure $ Loop
+ { state: state'
+ , addedCount: addedCount + 1
+ }
+
+sample :: forall a. Array a -> Effect a
+sample xs = do
+ i <- randomInt 0 $ length xs - 1
+ pure $ unsafePartial $ unsafeIndex xs i
+
+adjectives :: Array String
+adjectives =
+ [ "pretty"
+ , "large"
+ , "big"
+ , "small"
+ , "tall"
+ , "short"
+ , "long"
+ , "handsome"
+ , "plain"
+ , "quaint"
+ , "clean"
+ , "elegant"
+ , "easy"
+ , "angry"
+ , "crazy"
+ , "helpful"
+ , "mushy"
+ , "odd"
+ , "unsightly"
+ , "adorable"
+ , "important"
+ , "inexpensive"
+ , "cheap"
+ , "expensive"
+ , "fancy"
+ ]
+
+colours :: Array String
+colours =
+ [ "red"
+ , "yellow"
+ , "blue"
+ , "green"
+ , "pink"
+ , "brown"
+ , "purple"
+ , "brown"
+ , "white"
+ , "black"
+ , "orange"
+ ]
+
+nouns :: Array String
+nouns =
+ [ "table"
+ , "chair"
+ , "house"
+ , "bbq"
+ , "desk"
+ , "car"
+ , "pony"
+ , "cookie"
+ , "sandwich"
+ , "burger"
+ , "pizza"
+ , "mouse"
+ , "keyboard"
+ ]
diff --git a/frameworks/keyed/halogen/.gitignore b/frameworks/keyed/halogen/.gitignore
new file mode 100644
index 000000000..2b9d6603c
--- /dev/null
+++ b/frameworks/keyed/halogen/.gitignore
@@ -0,0 +1,11 @@
+# Dependencies
+.psci_modules
+.spago
+bower_components
+node_modules
+
+# Generated files
+.psci
+.psc*
+output
+
diff --git a/frameworks/keyed/halogen/index.html b/frameworks/keyed/halogen/index.html
new file mode 100644
index 000000000..89fa48742
--- /dev/null
+++ b/frameworks/keyed/halogen/index.html
@@ -0,0 +1,11 @@
+
+
+
+
+ Halogen v5.0.0
+
+
+
+
+
+
diff --git a/frameworks/keyed/halogen/package.json b/frameworks/keyed/halogen/package.json
new file mode 100644
index 000000000..46235b3c5
--- /dev/null
+++ b/frameworks/keyed/halogen/package.json
@@ -0,0 +1,30 @@
+{
+ "name": "js-framework-benchmark-keyed-halogen",
+ "version": "1.0.0",
+ "description": "Purescript Halogen JS Benchmark",
+ "main": "index.js",
+ "js-framework-benchmark": {
+ "frameworkVersion": "5.0.0"
+ },
+ "scripts": {
+ "postinstall": "spago install",
+ "clean": "rm -rf output .spago node_modules",
+ "build": "spago build",
+ "build-prod": "spago bundle-app --to output/bundle.js"
+ },
+ "keywords": [
+ "purescript",
+ "halogen"
+ ],
+ "author": "Thomas Honeyman ",
+ "license": "ISC",
+ "homepage": "https://github.com/krausest/js-framework-benchmark",
+ "repository": {
+ "type": "git",
+ "url": "https://github.com/krausest/js-framework-benchmark.git"
+ },
+ "devDependencies": {
+ "purescript": "0.13.6",
+ "spago": "0.14.0"
+ }
+}
diff --git a/frameworks/keyed/halogen/packages.dhall b/frameworks/keyed/halogen/packages.dhall
new file mode 100644
index 000000000..46e8ae3c0
--- /dev/null
+++ b/frameworks/keyed/halogen/packages.dhall
@@ -0,0 +1,11 @@
+let upstream =
+ https://github.com/purescript/package-sets/releases/download/psc-0.13.6-20200331/packages.dhall sha256:350af1fdc68c91251138198f03ceedc4f8ed6651ee2af8a2177f87bcd64570d4
+
+let overrides =
+ { halogen =
+ upstream.halogen // { version = "v5.0.0-rc.8" }
+ }
+
+let additions = {=}
+
+in upstream // overrides // additions
diff --git a/frameworks/keyed/halogen/spago.dhall b/frameworks/keyed/halogen/spago.dhall
new file mode 100644
index 000000000..96b768b17
--- /dev/null
+++ b/frameworks/keyed/halogen/spago.dhall
@@ -0,0 +1,5 @@
+{ name = "js-framework-benchmark-halogen"
+, dependencies = [ "halogen" ]
+, packages = ./packages.dhall
+, sources = [ "src/**/*.purs" ]
+}
diff --git a/frameworks/keyed/halogen/src/Main.js b/frameworks/keyed/halogen/src/Main.js
new file mode 100644
index 000000000..97b2ad72f
--- /dev/null
+++ b/frameworks/keyed/halogen/src/Main.js
@@ -0,0 +1,24 @@
+function _random(max) {
+ return Math.round(Math.random() * 1000) % max;
+}
+
+exports.createRandomNRowsImpl = function(
+ adjectives,
+ colours,
+ nouns,
+ count,
+ lastId
+) {
+ var data = [];
+ for (var i = 0; i < count; i++)
+ data.push({
+ id: ++lastId,
+ label:
+ adjectives[_random(adjectives.length)] +
+ " " +
+ colours[_random(colours.length)] +
+ " " +
+ nouns[_random(nouns.length)]
+ });
+ return data;
+};
diff --git a/frameworks/keyed/halogen/src/Main.purs b/frameworks/keyed/halogen/src/Main.purs
new file mode 100644
index 000000000..680c54537
--- /dev/null
+++ b/frameworks/keyed/halogen/src/Main.purs
@@ -0,0 +1,272 @@
+module Main where
+
+import Prelude
+
+import Data.Array as Array
+import Data.Maybe (Maybe(..))
+import Data.Tuple (Tuple(..))
+import Effect (Effect)
+import Effect.Aff (Aff)
+import Effect.Uncurried (EffectFn5, runEffectFn5)
+import Halogen as H
+import Halogen.Aff as HA
+import Halogen.HTML as HH
+import Halogen.HTML.Elements.Keyed as HK
+import Halogen.HTML.Events as HE
+import Halogen.HTML.Properties as HP
+import Halogen.VDom.Driver (runUI)
+
+main :: Effect Unit
+main = HA.runHalogenAff do
+ body <- HA.awaitBody
+ runUI app unit body
+
+data Action
+ = Create Int
+ | AppendOneThousand
+ | UpdateEveryTenth
+ | Clear
+ | Swap
+ | Remove Int
+ | Select Int
+
+type State =
+ { rows :: Array Row
+ , lastId :: Int
+ , selectedId :: Int
+ }
+
+type Row =
+ { id :: Int
+ , label :: String
+ }
+
+app :: forall q i o. H.Component HH.HTML q i o Aff
+app = H.mkComponent
+ { initialState
+ , render
+ , eval: H.mkEval $ H.defaultEval { handleAction = handleAction }
+ }
+ where
+ initialState :: _ -> State
+ initialState _ = { rows: [], lastId: 0, selectedId: 0 }
+
+ render :: forall ps. State -> H.ComponentHTML Action ps Aff
+ render state =
+ HH.div
+ [ class_ "container" ]
+ [ jumbotron
+ , HH.table
+ [ class_ "table table-hover table-striped test-data" ]
+ [ HK.tbody_ do
+ state.rows <#> \row ->
+ Tuple (show row.id) (renderRow state.selectedId row)
+ ]
+ , footer
+ ]
+
+ handleAction :: forall ps. Action -> H.HalogenM State Action ps o Aff Unit
+ handleAction = case _ of
+ Create amount -> do
+ state <- H.get
+ newRows <- H.liftEffect $ createRandomNRows amount state.lastId
+ H.modify_ _ { rows = newRows, lastId = state.lastId + amount }
+
+ AppendOneThousand -> do
+ state <- H.get
+ let amount = 1000
+ newRows <- H.liftEffect $ createRandomNRows amount state.lastId
+ H.modify_ _ { rows = state.rows <> newRows, lastId = state.lastId + amount }
+
+ UpdateEveryTenth -> do
+ let
+ updateLabel ix row =
+ if ix `mod` 10 == 0 then row { label = row.label <> " !!!" } else row
+
+ H.modify_ \state -> state { rows = Array.mapWithIndex updateLabel state.rows }
+
+ Clear ->
+ H.modify_ _ { rows = [] }
+
+ Swap -> do
+ state <- H.get
+ case swapRows state.rows 1 998 of
+ Nothing -> pure unit
+ Just rows -> H.modify_ _ { rows = rows }
+
+ Remove id ->
+ H.modify_ \state ->
+ state { rows = Array.filter (\r -> r.id /= id) state.rows }
+
+ Select id -> do
+ state <- H.get
+ if state.selectedId == id then
+ pure unit
+ else
+ H.modify_ _ { selectedId = id }
+
+type ActionButton = { id :: String, label :: String, action :: Action }
+
+buttons :: Array ActionButton
+buttons =
+ [ { id: "run", label: "Create 1,000 rows", action: Create 1000 }
+ , { id: "runlots", label: "Create 10,000 rows", action: Create 10000 }
+ , { id: "add", label: "Append 1,000 rows", action: AppendOneThousand }
+ , { id: "update", label: "Update every 10th row", action: UpdateEveryTenth }
+ , { id: "clear", label: "Clear", action: Clear }
+ , { id: "swaprows", label: "Swap Rows", action: Swap }
+ ]
+
+renderActionButton :: forall ps. ActionButton -> H.ComponentHTML Action ps Aff
+renderActionButton { id, label, action } =
+ HH.div
+ [ class_ "col-sm-6 smallpad" ]
+ [ HH.button
+ [ HP.type_ HP.ButtonButton
+ , class_ "btn btn-primary btn-block"
+ , HP.id_ id
+ , HP.attr (HH.AttrName "ref") "text"
+ , HE.onClick \_ -> Just action
+ ]
+ [ HH.text label ]
+ ]
+
+renderRow :: forall ps. Int -> Row -> H.ComponentHTML Action ps Aff
+renderRow selectedId row =
+ HH.tr
+ (if selectedId == row.id then
+ [ class_ "danger"
+ , HP.attr (HH.AttrName "selected") "true"
+ ]
+ else
+ [ ]
+ )
+ [ HH.td colMd1 [ HH.text (show row.id) ]
+ , HH.td colMd4 [ HH.a [ HE.onClick \_ -> Just (Select row.id) ] [ HH.text row.label ] ]
+ , HH.td colMd1 [ HH.a [ HE.onClick \_ -> Just (Remove row.id) ] removeIcon ]
+ , spacer
+ ]
+
+removeIcon :: forall ps. Array (H.ComponentHTML Action ps Aff)
+removeIcon =
+ [ HH.span
+ [ class_ "glyphicon glyphicon-remove"
+ , HP.attr (HH.AttrName "aria-hidden") "true"
+ ]
+ []
+ ]
+
+class_ :: forall r i. String -> HH.IProp ( class :: String | r ) i
+class_ = HP.class_ <<< HH.ClassName
+
+colMd1 :: forall r i. Array (HH.IProp ( class :: String | r ) i)
+colMd1 = [ class_ "col-md-1" ]
+
+colMd4 :: forall r i. Array (HH.IProp ( class :: String | r) i)
+colMd4 = [ class_ "col-md-4" ]
+
+spacer :: forall p i. HH.HTML p i
+spacer = HH.td [ class_ "col-md-6" ] []
+
+footer :: forall ps. H.ComponentHTML Action ps Aff
+footer =
+ HH.span
+ [ class_ "preloadicon glyphicon glyphicon-remove"
+ , HP.attr (HH.AttrName "aria-hidden") "true"
+ ]
+ []
+
+jumbotron :: forall ps. H.ComponentHTML Action ps Aff
+jumbotron =
+ HH.div
+ [ class_ "jumbotron" ]
+ [ HH.div
+ [ class_ "row" ]
+ [ HH.div
+ [ class_ "col-md-6" ]
+ [ HH.h1_ [ HH.text "Halogen 5.0.0 (keyed)" ] ]
+ , HH.div [ class_ "col-md-6" ] do
+ map (HH.lazy renderActionButton) buttons
+ ]
+ ]
+
+updateEveryTenth :: Array Row -> Array Row
+updateEveryTenth =
+ Array.mapWithIndex updateRowLabel
+ where
+ updateRowLabel ix row =
+ if ix `mod` 10 == 0 then row { label = row.label <> " !!!" } else row
+
+swapRows :: Array Row -> Int -> Int -> Maybe (Array Row)
+swapRows arr index1 index2 = do
+ rowA <- arr Array.!! index1
+ rowB <- arr Array.!! index2
+ arrA <- Array.updateAt index1 rowB arr
+ arrB <- Array.updateAt index2 rowA arrA
+ pure arrB
+
+foreign import createRandomNRowsImpl :: EffectFn5 (Array String) (Array String) (Array String) Int Int (Array Row)
+
+createRandomNRows :: Int -> Int -> Effect (Array Row)
+createRandomNRows n lastId = runEffectFn5 createRandomNRowsImpl adjectives colours nouns n lastId
+
+adjectives :: Array String
+adjectives =
+ [ "pretty"
+ , "large"
+ , "big"
+ , "small"
+ , "tall"
+ , "short"
+ , "long"
+ , "handsome"
+ , "plain"
+ , "quaint"
+ , "clean"
+ , "elegant"
+ , "easy"
+ , "angry"
+ , "crazy"
+ , "helpful"
+ , "mushy"
+ , "odd"
+ , "unsightly"
+ , "adorable"
+ , "important"
+ , "inexpensive"
+ , "cheap"
+ , "expensive"
+ , "fancy"
+ ]
+
+colours :: Array String
+colours =
+ [ "red"
+ , "yellow"
+ , "blue"
+ , "green"
+ , "pink"
+ , "brown"
+ , "purple"
+ , "brown"
+ , "white"
+ , "black"
+ , "orange"
+ ]
+
+nouns :: Array String
+nouns =
+ [ "table"
+ , "chair"
+ , "house"
+ , "bbq"
+ , "desk"
+ , "car"
+ , "pony"
+ , "cookie"
+ , "sandwich"
+ , "burger"
+ , "pizza"
+ , "mouse"
+ , "keyboard"
+ ]
diff --git a/frameworks/non-keyed/grain/.gitignore b/frameworks/non-keyed/grain/.gitignore
new file mode 100644
index 000000000..30efe1997
--- /dev/null
+++ b/frameworks/non-keyed/grain/.gitignore
@@ -0,0 +1,10 @@
+/bower_components/
+/node_modules/
+/.pulp-cache/
+/output/
+/generated-docs/
+/.psc-package/
+/.psc*
+/.purs*
+/.psa*
+/.spago
diff --git a/frameworks/non-keyed/grain/index.html b/frameworks/non-keyed/grain/index.html
new file mode 100644
index 000000000..c8497b1bd
--- /dev/null
+++ b/frameworks/non-keyed/grain/index.html
@@ -0,0 +1,12 @@
+
+
+
+
+ Grain non-keyed
+
+
+
+
+
+
+
diff --git a/frameworks/non-keyed/grain/package.json b/frameworks/non-keyed/grain/package.json
new file mode 100644
index 000000000..9db1c6d19
--- /dev/null
+++ b/frameworks/non-keyed/grain/package.json
@@ -0,0 +1,26 @@
+{
+ "name": "js-framework-benchmark-grain",
+ "version": "1.0.0",
+ "description": "Grain demo",
+ "main": "index.js",
+ "js-framework-benchmark": {
+ "frameworkVersion": "1.0.0"
+ },
+ "scripts": {
+ "spago": "spago",
+ "clean": "rm -rf output",
+ "build-prod": "spago bundle-app -t dist/bundle.js && terser --compress --mangle --enclose --output=dist/main.js -- dist/bundle.js"
+ },
+ "author": "Shinya Takahashi ",
+ "license": "MIT",
+ "homepage": "https://github.com/krausest/js-framework-benchmark",
+ "repository": {
+ "type": "git",
+ "url": "https://github.com/krausest/js-framework-benchmark.git"
+ },
+ "dependencies": {
+ "purescript": "^0.13.8",
+ "spago": "^0.15.3",
+ "terser": "^4.8.0"
+ }
+}
diff --git a/frameworks/non-keyed/grain/packages.dhall b/frameworks/non-keyed/grain/packages.dhall
new file mode 100644
index 000000000..5142c8c2e
--- /dev/null
+++ b/frameworks/non-keyed/grain/packages.dhall
@@ -0,0 +1,131 @@
+{-
+Welcome to your new Dhall package-set!
+
+Below are instructions for how to edit this file for most use
+cases, so that you don't need to know Dhall to use it.
+
+## Warning: Don't Move This Top-Level Comment!
+
+Due to how `dhall format` currently works, this comment's
+instructions cannot appear near corresponding sections below
+because `dhall format` will delete the comment. However,
+it will not delete a top-level comment like this one.
+
+## Use Cases
+
+Most will want to do one or both of these options:
+1. Override/Patch a package's dependency
+2. Add a package not already in the default package set
+
+This file will continue to work whether you use one or both options.
+Instructions for each option are explained below.
+
+### Overriding/Patching a package
+
+Purpose:
+- Change a package's dependency to a newer/older release than the
+ default package set's release
+- Use your own modified version of some dependency that may
+ include new API, changed API, removed API by
+ using your custom git repo of the library rather than
+ the package set's repo
+
+Syntax:
+Replace the overrides' "{=}" (an empty record) with the following idea
+The "//" or "⫽" means "merge these two records and
+ when they have the same value, use the one on the right:"
+-------------------------------
+let overrides =
+ { packageName =
+ upstream.packageName // { updateEntity1 = "new value", updateEntity2 = "new value" }
+ , packageName =
+ upstream.packageName // { version = "v4.0.0" }
+ , packageName =
+ upstream.packageName // { repo = "https://www.example.com/path/to/new/repo.git" }
+ }
+-------------------------------
+
+Example:
+-------------------------------
+let overrides =
+ { halogen =
+ upstream.halogen // { version = "master" }
+ , halogen-vdom =
+ upstream.halogen-vdom // { version = "v4.0.0" }
+ }
+-------------------------------
+
+### Additions
+
+Purpose:
+- Add packages that aren't already included in the default package set
+
+Syntax:
+Replace the additions' "{=}" (an empty record) with the following idea:
+-------------------------------
+let additions =
+ { package-name =
+ { dependencies =
+ [ "dependency1"
+ , "dependency2"
+ ]
+ , repo =
+ "https://example.com/path/to/git/repo.git"
+ , version =
+ "tag ('v4.0.0') or branch ('master')"
+ }
+ , package-name =
+ { dependencies =
+ [ "dependency1"
+ , "dependency2"
+ ]
+ , repo =
+ "https://example.com/path/to/git/repo.git"
+ , version =
+ "tag ('v4.0.0') or branch ('master')"
+ }
+ , etc.
+ }
+-------------------------------
+
+Example:
+-------------------------------
+let additions =
+ { benchotron =
+ { dependencies =
+ [ "arrays"
+ , "exists"
+ , "profunctor"
+ , "strings"
+ , "quickcheck"
+ , "lcg"
+ , "transformers"
+ , "foldable-traversable"
+ , "exceptions"
+ , "node-fs"
+ , "node-buffer"
+ , "node-readline"
+ , "datetime"
+ , "now"
+ ]
+ , repo =
+ "https://github.com/hdgarrood/purescript-benchotron.git"
+ , version =
+ "v7.0.0"
+ }
+ }
+-------------------------------
+-}
+
+
+let upstream =
+ https://github.com/purescript/package-sets/releases/download/psc-0.13.8/packages.dhall sha256:0e95ec11604dc8afc1b129c4d405dcc17290ce56d7d0665a0ff15617e32bbf03
+
+let overrides =
+ { grain =
+ upstream.grain // { dependencies = [ "web-html" ], version = "v1.0.0" }
+ }
+
+let additions = {=}
+
+in upstream // overrides // additions
diff --git a/frameworks/non-keyed/grain/spago.dhall b/frameworks/non-keyed/grain/spago.dhall
new file mode 100644
index 000000000..b3f04dae4
--- /dev/null
+++ b/frameworks/non-keyed/grain/spago.dhall
@@ -0,0 +1,9 @@
+{-
+Welcome to a Spago project!
+You can edit this file as you like.
+-}
+{ name = "js-framework-benchmark-grain"
+, dependencies = [ "grain", "random" ]
+, packages = ./packages.dhall
+, sources = [ "src/**/*.purs" ]
+}
diff --git a/frameworks/non-keyed/grain/src/Main.purs b/frameworks/non-keyed/grain/src/Main.purs
new file mode 100644
index 000000000..94f1700d2
--- /dev/null
+++ b/frameworks/non-keyed/grain/src/Main.purs
@@ -0,0 +1,259 @@
+module Main where
+
+import Prelude
+
+import Control.Monad.Rec.Class (Step(..), tailRecM)
+import Data.Array (delete, length, snoc, unsafeIndex, updateAtIndices, (!!))
+import Data.Maybe (Maybe(..))
+import Data.Newtype (class Newtype, over)
+import Data.Tuple (Tuple(..))
+import Effect (Effect)
+import Effect.Random (randomInt)
+import Grain (class GlobalGrain, GProxy(..), VNode, fromConstructor, mount, useFinder, useUpdater, useValue)
+import Grain.Markup as H
+import Partial.Unsafe (unsafePartial)
+import Web.DOM.Element (toNode)
+import Web.DOM.ParentNode (QuerySelector(..), querySelector)
+import Web.HTML (window)
+import Web.HTML.HTMLDocument (toParentNode)
+import Web.HTML.Window (document)
+
+main :: Effect Unit
+main = do
+ maybeEl <- window >>= document <#> toParentNode >>= querySelector (QuerySelector "#main")
+ case maybeEl of
+ Nothing -> pure unit
+ Just el ->
+ mount view $ toNode el
+
+view :: VNode
+view =
+ H.div # H.className "container" # H.kids
+ [ jumbotronView
+ , rowsView
+ , H.span
+ # H.className "preloadicon glyphicon glyphicon-remove"
+ # H.prop "aria-hidden" "true"
+ ]
+
+jumbotronView :: VNode
+jumbotronView = H.component do
+ findState <- useFinder (GProxy :: _ State)
+ updateState <- useUpdater (GProxy :: _ State)
+
+ let run_ count = do
+ State s <- findState
+ state <- addRows count $ State s { rows = [], selectedId = 0 }
+ updateState \_ -> state
+
+ run = run_ 1000
+
+ runLots = run_ 10000
+
+ add = do
+ state <- findState >>= addRows 1000
+ updateState \_ -> state
+
+ updateEveryTenth i rows =
+ case rows !! i of
+ Just row ->
+ updateEveryTenth (i + 10) $ updateAtIndices
+ [ Tuple i row { label = row.label <> " !!!" } ]
+ rows
+ Nothing -> rows
+
+ update_ = updateState $ over State \s -> s
+ { rows = updateEveryTenth 0 s.rows
+ }
+
+ clear =
+ updateState $ over State _ { rows = [], selectedId = 0 }
+
+ swapRows =
+ updateState $ over State \s ->
+ case s.rows !! 1, s.rows !! 998 of
+ Just row1, Just row2 ->
+ s { rows =
+ updateAtIndices
+ [ Tuple 1 row2, Tuple 998 row1 ]
+ s.rows
+ }
+ _, _ -> s
+
+ pure $ H.div # H.className "jumbotron" # H.kids
+ [ H.div # H.className "row" # H.kids
+ [ H.div # H.className "col-md-6" # H.kids
+ [ H.h1 # H.kids [ H.text "Grain non-keyed" ]
+ ]
+ , H.div # H.className "col-md-6" # H.kids
+ [ H.div # H.className "row" # H.kids
+ [ buttonView { id: "run", label: "Create 1,000 rows", onClick: run }
+ , buttonView { id: "runlots", label: "Create 10,000 rows", onClick: runLots }
+ , buttonView { id: "add", label: "Append 1,000 rows", onClick: add }
+ , buttonView { id: "update", label: "Update every 10th row", onClick: update_ }
+ , buttonView { id: "clear", label: "Clear", onClick: clear }
+ , buttonView { id: "swaprows", label: "Swap Rows", onClick: swapRows }
+ ]
+ ]
+ ]
+ ]
+
+rowsView :: VNode
+rowsView = H.component do
+ State { rows, selectedId } <- useValue (GProxy :: _ State)
+ updateState <- useUpdater (GProxy :: _ State)
+
+ let select row = updateState $ over State _ { selectedId = row.id }
+ remove row = updateState $ over State \s -> s
+ { rows = delete row s.rows
+ }
+
+ pure $ H.table
+ # H.className "table table-hover table-striped test-data"
+ # H.kids
+ [ H.tbody # H.kids
+ (rows <#> \row -> rowView { row, selected: row.id == selectedId, onSelect: select row, onRemove: remove row })
+ ]
+
+rowView :: { row :: Row, selected :: Boolean, onSelect :: Effect Unit, onRemove :: Effect Unit } -> VNode
+rowView { row, selected, onSelect, onRemove } =
+ let idTxt = show row.id
+ in H.tr
+ # H.fingerprint (row.label <> if selected then "t" else "f")
+ # H.className (if selected then "danger" else "")
+ # H.kids
+ [ H.td # H.className "col-md-1" # H.kids [ H.text idTxt ]
+ , H.td # H.className "col-md-4" # H.kids
+ [ H.a # H.onClick (\_ -> onSelect) # H.kids [ H.text row.label ]
+ ]
+ , H.td # H.className "col-md-1" # H.kids
+ [ H.a # H.onClick (\_ -> onRemove) # H.kids [ iconView ]
+ ]
+ , H.td # H.className "col-md-6"
+ ]
+
+buttonView :: { id :: String, label :: String, onClick :: Effect Unit } -> VNode
+buttonView { id, label, onClick } =
+ H.div # H.className "col-sm-6 smallpad" # H.kids
+ [ H.button
+ # H.id id
+ # H.type_ "button"
+ # H.className "btn btn-primary btn-block"
+ # H.onClick (\_ -> onClick)
+ # H.kids [ H.text label ]
+ ]
+
+iconView :: VNode
+iconView =
+ H.fingerprint "remove-icon" $ H.span
+ # H.className "glyphicon glyphicon-remove"
+ # H.prop "aria-hidden" "true"
+
+type Row =
+ { id :: Int
+ , label :: String
+ }
+
+newtype State = State
+ { nextId :: Int
+ , rows :: Array Row
+ , selectedId :: Int
+ }
+
+derive instance newtypeState :: Newtype State _
+
+instance globalGrainState :: GlobalGrain State where
+ typeRefOf _ = fromConstructor State
+ initialState _ = pure $ State
+ { nextId: 1
+ , rows: []
+ , selectedId: 0
+ }
+
+addRows :: Int -> State -> Effect State
+addRows count (State s) =
+ tailRecM go { state: s, addedCount: 0 }
+ where
+ go { state, addedCount }
+ | addedCount >= count =
+ pure $ Done $ State state
+ | otherwise = do
+ a <- sample adjectives
+ c <- sample colours
+ n <- sample nouns
+ let id = state.nextId
+ label = a <> " " <> c <> " " <> n
+ state' = state
+ { nextId = state.nextId + 1
+ , rows = snoc state.rows { id, label }
+ }
+ pure $ Loop
+ { state: state'
+ , addedCount: addedCount + 1
+ }
+
+sample :: forall a. Array a -> Effect a
+sample xs = do
+ i <- randomInt 0 $ length xs - 1
+ pure $ unsafePartial $ unsafeIndex xs i
+
+adjectives :: Array String
+adjectives =
+ [ "pretty"
+ , "large"
+ , "big"
+ , "small"
+ , "tall"
+ , "short"
+ , "long"
+ , "handsome"
+ , "plain"
+ , "quaint"
+ , "clean"
+ , "elegant"
+ , "easy"
+ , "angry"
+ , "crazy"
+ , "helpful"
+ , "mushy"
+ , "odd"
+ , "unsightly"
+ , "adorable"
+ , "important"
+ , "inexpensive"
+ , "cheap"
+ , "expensive"
+ , "fancy"
+ ]
+
+colours :: Array String
+colours =
+ [ "red"
+ , "yellow"
+ , "blue"
+ , "green"
+ , "pink"
+ , "brown"
+ , "purple"
+ , "brown"
+ , "white"
+ , "black"
+ , "orange"
+ ]
+
+nouns :: Array String
+nouns =
+ [ "table"
+ , "chair"
+ , "house"
+ , "bbq"
+ , "desk"
+ , "car"
+ , "pony"
+ , "cookie"
+ , "sandwich"
+ , "burger"
+ , "pizza"
+ , "mouse"
+ , "keyboard"
+ ]