diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..4c69ef2 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,13 @@ +# https://editorconfig.org +root = true + +[*] +indent_style = space +indent_size = 2 +end_of_line = lf +charset = utf-8 +trim_trailing_whitespace = true +insert_final_newline = true + +[*.md] +trim_trailing_whitespace = false \ No newline at end of file diff --git a/.eslintrc.json b/.eslintrc.json new file mode 100644 index 0000000..da4accb --- /dev/null +++ b/.eslintrc.json @@ -0,0 +1,33 @@ +{ + "env": { "browser": true, "es2015": true }, + "extends": "eslint:recommended", + "parserOptions": { "ecmaVersion": 2015, "sourceType": "module" }, + "rules": { + "block-scoped-var": "error", + "no-caller": "error", + "no-extra-parens": "off", + "no-extend-native": "error", + "no-loop-func": "error", + "no-new": "error", + "no-return-assign": "error", + "no-sequences": "error", + "no-shadow": "error", + "no-unused-expressions": "error", + "no-undef": "error", + "no-eq-null": "error", + "indent": ["error", 2, { "SwitchCase": 1 }], + "quotes": ["error", "double"], + "strict": ["error", "global"], + "no-unused-vars": "off", + "no-empty": "off", + "radix": "off", + "no-param-reassign": "off", + "eqeqeq": "off", + "no-bitwise": "off", + "consistent-return": "off", + "guard-for-in": "off", + "no-mixed-spaces-and-tabs": "off", + "no-use-before-define": "off", + "no-dupe-keys": "off" + } +} diff --git a/.gitignore b/.gitignore index 709fd09..c943339 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,6 @@ -/.* +/.psc-ide-port +!/.editorconfig +!/.tidyrc.json !/.gitignore !/.eslintrc.json !/.travis.yml @@ -6,3 +8,6 @@ package-lock.json /bower_components/ /node_modules/ /output/ +/**/*.ast +/Session.vim +/.spago \ No newline at end of file diff --git a/.husky/commit-msg b/.husky/commit-msg new file mode 100755 index 0000000..68aaea1 --- /dev/null +++ b/.husky/commit-msg @@ -0,0 +1,9 @@ +#!/bin/sh +. "$(dirname "$0")/_/husky.sh" + +BRANCH_NAME=$(git branch | grep '*' | sed 's/* //') + +if [[ $BRANCH_NAME != *"no branch"* ]] +then + npx --no -- commitlint --edit "${1}" +fi diff --git a/.husky/prepare-commit-msg b/.husky/prepare-commit-msg new file mode 100755 index 0000000..ab0fd24 --- /dev/null +++ b/.husky/prepare-commit-msg @@ -0,0 +1,12 @@ +#!/bin/bash + +# Adding hooks to prepare commit message. +# This hook triggers Jira input modules. +# Helps in maintaining code compatblity. + +BRANCH_NAME=$(git branch | grep '*' | sed 's/* //') + +if [[ $BRANCH_NAME != *"no branch"* ]] +then + exec < /dev/tty && node_modules/.bin/git-cz --hook || true +fi diff --git a/.tidyrc.json b/.tidyrc.json new file mode 100644 index 0000000..5f6114a --- /dev/null +++ b/.tidyrc.json @@ -0,0 +1,10 @@ +{ + "importSort": "source", + "importWrap": "source", + "indent": 2, + "operatorsFile": null, + "ribbon": 1, + "typeArrowPlacement": "first", + "unicode": "never", + "width": null + } \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..a83b991 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,25 @@ +## [3.0.2](https://bitbucket.org/juspay/purescript-halogen-vdom/compare/v3.0.1...v3.0.2) (2023-01-05) + + +### Bug Fixes + +* PICAF-18952: Updating es-lint rules and adding eslint:fix ([ad1d72e](https://bitbucket.org/juspay/purescript-halogen-vdom/commits/ad1d72e254c25928f66fc8f049669362dadce519)) + +## [3.0.1](https://bitbucket.org/juspay/purescript-halogen-vdom/compare/v3.0.0...v3.0.1) (2022-12-27) + + +### Bug Fixes + +* PICAF-19117: adding branch rule to jenkins ([b0aa985](https://bitbucket.org/juspay/purescript-halogen-vdom/commits/b0aa98593d7e23d5e608ad11b90f42d6935f74b6)) + +# [3.0.0](https://bitbucket.org/juspay/purescript-halogen-vdom/compare/v2.0.1...v3.0.0) (2022-12-23) + + +### Features + +* PICAF-18952: Shifted to PS15 ([f26a8b8](https://bitbucket.org/juspay/purescript-halogen-vdom/commits/f26a8b8274289ba00134378990597f05c3751aa3)) + + +### BREAKING CHANGES + +* Purescript version is changing diff --git a/Jenkinsfile b/Jenkinsfile new file mode 100644 index 0000000..3e3039a --- /dev/null +++ b/Jenkinsfile @@ -0,0 +1,61 @@ +def isVersionBumpCandidateBranch(branch) { + (branch =~ /(main|hotfix-)/) +} + +pipeline { + agent { + label "sdk" + } + environment { + NPM_REPO_NAME = "purescript-halogen-vdom" + GIT_AUTHOR_NAME = 'jenkins.user' + GIT_AUTHOR_EMAIL = 'jenkins.user@juspay.in' + GIT_COMMITTER_NAME = 'jenkins.user' + GIT_COMMITTER_EMAIL = 'jenkins.user@juspay.in' + GIT_USERNAME = 'jenkins.user' + GIT_EMAIL = 'jenkins.user@juspay.in' + + } + + stages { + stage('Checkout') { + steps { + scmSkip(deleteBuild: false, skipPattern:'.*\\[skip ci\\].*') + script { + if (sh(script: "git log -1 --pretty=%B | grep -F -ie '[skip ci]' -e '[ci skip]'", returnStatus: true) == 0) { + currentBuild.result = 'ABORTED' + error 'Aborting because commit message contains [skip ci]' + } + } + } + } + + stage("npm/spago install") { + steps { + script { + sh ("npm config set package-lock=false; npm i") + sh ("npm run spago:install") + } + } + } + + stage("PS compilation") { + steps { + script { + // build packages to ensure compilation / tests work fine before updating version + sh ("npm run compile") + } + } + } + + stage("npm release") { + steps { + script { + if (isVersionBumpCandidateBranch(env.BRANCH_NAME)) { + sh ("npx semantic-release --debug") + } + } + } + } + } +} diff --git a/bower.json b/bower.json index 759b27f..9198126 100644 --- a/bower.json +++ b/bower.json @@ -22,19 +22,19 @@ "output" ], "dependencies": { - "purescript-prelude": "^4.0.0", - "purescript-effect": "^2.0.0", - "purescript-tuples": "^5.0.0", - "purescript-web-html": "^1.0.0", - "purescript-foreign-object": "^1.0.0", - "purescript-maybe": "^4.0.0", - "purescript-unsafe-coerce": "^4.0.0", - "purescript-bifunctors": "^4.0.0", - "purescript-refs": "^4.1.0", - "purescript-foreign": "^5.0.0" + "purescript-prelude": "^6.0.0", + "purescript-effect": "^4.0.0", + "purescript-tuples": "^7.0.0", + "purescript-web-html": "^4.0.0", + "purescript-foreign-object": "^4.0.0", + "purescript-maybe": "^6.0.0", + "purescript-unsafe-coerce": "^6.0.0", + "purescript-bifunctors": "^6.0.0", + "purescript-refs": "^6.0.0", + "purescript-foreign": "^7.0.0" }, "devDependencies": { - "purescript-js-timers": "^4.0.0", - "purescript-exists": "^4.0.0" + "purescript-js-timers": "^6.1.0", + "purescript-exists": "^6.0.0" } } diff --git a/package.json b/package.json index 0e745d5..5bae6d2 100644 --- a/package.json +++ b/package.json @@ -1,14 +1,91 @@ { + "name": "purescript-halogen-vdom", + "version": "3.0.2", "private": true, "scripts": { "clean": "rimraf output && rimraf", - "test": "pulp build -I test -- --censor-lib --strict", - "build": "pulp build -- --censor-lib --strict" + "start": "spago build -w --before 'npm run eslint' --purs-args '--censor-lib --strict'", + "eslint": "eslint src", + "compile": "eslint src && spago build --purs-args '--censor-lib --strict'", + "spago:install": "spago install", + "prepare": "husky install", + "eslint:fix": "eslint src --fix" + }, + "config": { + "commitizen": { + "path": "./node_modules/@digitalroute/cz-conventional-changelog-for-jira", + "jiraPrefix": "PICAF", + "jiraLocation": "post-type", + "jiraAppend": ":" + } + }, + "commitlint": { + "plugins": [ + "commitlint-plugin-jira-rules" + ], + "extends": [ + "jira" + ], + "rules": { + "jira-task-id-max-length": [ + 0 + ], + "jira-commit-message-separator": [ + 0 + ], + "jira-commit-status-case": [ + 0 + ], + "jira-task-id-project-key": [ + 0 + ], + "jira-task-id-separator": [ + 0 + ], + "jira-task-id-case": [ + 0 + ], + "jira-task-id-min-length": [ + 0 + ] + } + }, + "release": { + "branches": [ + "main", + { + "name": "hotfix-[0-9]+.[0-9]+.[0-9]+", + "prerelease": true + } + ], + "repositoryUrl": "git@ssh.bitbucket.juspay.net:picaf/purescript-halogen-vdom.git", + "plugins": [ + "@semantic-release/commit-analyzer", + "@semantic-release/release-notes-generator", + "@semantic-release/npm", + "@semantic-release/changelog", + "@semantic-release/git" + ] }, "devDependencies": { - "pulp": "^12.2.0", - "purescript": "slamdata/node-purescript#0.12", - "purescript-psa": "^0.6.0", - "rimraf": "^2.6.2" + "pulp": "^16.0.2", + "purescript": "^0.15.6", + "rimraf": "^2.6.2", + "@commitlint/cli": "^17.2.0", + "@commitlint/config-conventional": "^17.2.0", + "@digitalroute/cz-conventional-changelog-for-jira": "^7.4.2", + "@semantic-release/changelog": "^6.0.1", + "@semantic-release/commit-analyzer": "^9.0.2", + "@semantic-release/git": "^10.0.1", + "@semantic-release/npm": "^9.0.1", + "@semantic-release/release-notes-generator": "^10.0.3", + "commitlint-config-jira": "^1.6.4", + "commitlint-plugin-jira-rules": "^1.6.4", + "cz-conventional-changelog": "^3.3.0", + "husky": "^8.0.2", + "semantic-release": "^19.0.5", + "spago": "^0.20.9", + "eslint": "^8.29.0", + "purescript-psa": "^0.8.2" } } diff --git a/packages.dhall b/packages.dhall new file mode 100644 index 0000000..0fcd980 --- /dev/null +++ b/packages.dhall @@ -0,0 +1,105 @@ +{- +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. + +## 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: +where `entityName` is one of the following: +- dependencies +- repo +- version +------------------------------- +let upstream = -- +in upstream + with packageName.entityName = "new value" +------------------------------- + +Example: +------------------------------- +let upstream = -- +in upstream + with halogen.version = "master" + with halogen.repo = "https://example.com/path/to/git/repo.git" + + with halogen-vdom.version = "v4.0.0" + with halogen-vdom.dependencies = [ "extra-dependency" ] # halogen-vdom.dependencies +------------------------------- + +### Additions + +Purpose: +- Add packages that aren't already included in the default package set + +Syntax: +where `` is: +- a tag (i.e. "v4.0.0") +- a branch (i.e. "master") +- commit hash (i.e. "701f3e44aafb1a6459281714858fadf2c4c2a977") +------------------------------- +let upstream = -- +in upstream + with new-package-name = + { dependencies = + [ "dependency1" + , "dependency2" + ] + , repo = + "https://example.com/path/to/git/repo.git" + , version = + "" + } +------------------------------- + +Example: +------------------------------- +let upstream = -- +in upstream + with 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.15.3/packages.dhall + sha256:ffc496e19c93f211b990f52e63e8c16f31273d4369dbae37c7cf6ea852d4442f + +in upstream diff --git a/spago.dhall b/spago.dhall new file mode 100644 index 0000000..48cee96 --- /dev/null +++ b/spago.dhall @@ -0,0 +1,21 @@ +{ name = "halogen-vdom" +, dependencies = + [ "effect" + , "prelude" + , "foreign-object" + , "arrays" + , "bifunctors" + , "foreign" + , "functions" + , "maybe" + , "newtype" + , "nullable" + , "refs" + , "tuples" + , "unsafe-coerce" + , "web-dom" + , "web-events" + ] +, packages = ./packages.dhall +, sources = [ "src/**/*.purs" ] +} diff --git a/src/Halogen/VDom/DOM.purs b/src/Halogen/VDom/DOM.purs index 83d7b3a..9be92f3 100644 --- a/src/Halogen/VDom/DOM.purs +++ b/src/Halogen/VDom/DOM.purs @@ -11,16 +11,16 @@ import Prelude import Data.Array as Array import Data.Function.Uncurried as Fn -import Data.Maybe (Maybe(..)) +import Data.Maybe (Maybe(..), fromMaybe) import Data.Nullable (toNullable) import Data.Tuple (Tuple(..), fst) import Effect.Uncurried as EFn +import Foreign (Foreign) import Foreign.Object as Object import Halogen.VDom.Machine (Machine, Step, Step'(..), extract, halt, mkStep, step, unStep) import Halogen.VDom.Machine as Machine -import Halogen.VDom.Types (ElemName(..), Namespace(..), VDom(..), runGraft) +import Halogen.VDom.Types (ElemName(..), FnObject, Namespace(..), ShimmerHolder, VDom(..), runGraft) import Halogen.VDom.Util as Util -import Web.DOM.Document (Document) as DOM import Web.DOM.Element (Element) as DOM import Web.DOM.Element as DOMElement import Web.DOM.Node (Node) as DOM @@ -35,12 +35,16 @@ type VDomBuilder i a w = EFn.EffectFn3 (VDomSpec a w) (VDomMachine a w) i (VDomS type VDomBuilder4 i j k l a w = EFn.EffectFn6 (VDomSpec a w) (VDomMachine a w) i j k l (VDomStep a w) +type VDomBuilder5 i j a w ch = EFn.EffectFn5 (VDomSpec a w) (VDomMachine a w) i j ch (VDomStep a w) + +type VDomBuilder6 i j a w = EFn.EffectFn4 (VDomSpec a w) (VDomMachine a w) i j (VDomStep a w) + -- | Widget machines recursively reference the configured spec to potentially -- | enable recursive trees of Widgets. newtype VDomSpec a w = VDomSpec { buildWidget ∷ VDomSpec a w → Machine w DOM.Node - , buildAttributes ∷ DOM.Element → Machine a Unit - , document ∷ DOM.Document + , buildAttributes ∷ FnObject -> DOM.Element → Machine a Unit + , fnObject :: FnObject } -- | Starts an initial `VDom` machine by providing a `VDomSpec`. @@ -58,9 +62,74 @@ buildVDom spec = build build = EFn.mkEffectFn1 case _ of Text s → EFn.runEffectFn3 buildText spec build s Elem ns n a ch → EFn.runEffectFn6 buildElem spec build ns n a ch + Chunk ns n a ch → EFn.runEffectFn6 buildChunk spec build ns n a ch Keyed ns n a ch → EFn.runEffectFn6 buildKeyed spec build ns n a ch Widget w → EFn.runEffectFn3 buildWidget spec build w Grafted g → EFn.runEffectFn1 build (runGraft g) + Microapp s g ch → EFn.runEffectFn5 buildMicroapp spec build s g ch + +type MicroAppState a w = + { build ∷ VDomMachine a w + , node ∷ DOM.Node + , requestId :: String + , attrs ∷ Step a Unit + , service :: String + , payload :: Maybe Foreign + , children :: Array (VDomStep a w) + } + +buildMicroapp ∷ ∀ a w. VDomBuilder5 String a a w (Maybe (Array (VDom a w))) +buildMicroapp = EFn.mkEffectFn5 \(VDomSpec spec) build s as1 ch → do + -- GET ID, SCHEDULE AN AFTER RENDER CALL TO M-APP + -- MAYBE ADD A FUNCTION FROM PRESTO_DOM TO SCHEDULE + requestId <- Util.generateUUID + el ← EFn.runEffectFn3 Util.createMicroapp spec.fnObject requestId s + let node = DOMElement.toNode el + attrs ← EFn.runEffectFn1 (spec.buildAttributes spec.fnObject el) as1 + let onChild = EFn.mkEffectFn2 \ix child → do + res ← EFn.runEffectFn1 build child + EFn.runEffectFn6 Util.insertChildIx spec.fnObject "render" ix (extract res) node "" + pure res + children ← EFn.runEffectFn2 Util.forE (fromMaybe [] ch) onChild + let state = { build, node, service: s, attrs, requestId : requestId, payload : Nothing, children } + pure $ mkStep $ Step node state (patchMicroapp spec.fnObject) (haltMicroapp spec.fnObject) + +patchMicroapp ∷ ∀ a w. FnObject -> EFn.EffectFn2 (MicroAppState a w) (VDom a w) (VDomStep a w) +patchMicroapp fnObject = EFn.mkEffectFn2 \state vdom → do + let { build, node, attrs, service: value1, children : ch1} = state + case vdom of + Grafted g → + EFn.runEffectFn2 (patchMicroapp fnObject) state (runGraft g) + Microapp s2 value2 ch2 -- CHANGE IN PAYLOAD, NEEDS TO TERMINATE OLD / FIRE EVENT TO OTHER M_APP + | value1 == s2 → do + let + onThese = EFn.mkEffectFn4 \obj ix s v → do + res ← EFn.runEffectFn2 step s v + EFn.runEffectFn6 Util.insertChildIx obj "patch" ix (extract res) node "" + pure res + onThis = EFn.mkEffectFn3 \_ _ s → EFn.runEffectFn1 halt s + onThat = EFn.mkEffectFn3 \obj ix v → do + res ← EFn.runEffectFn1 build v + EFn.runEffectFn6 Util.insertChildIx obj "patch" ix (extract res) node "" + pure res + children2 ← EFn.runEffectFn6 Util.diffWithIxE fnObject ch1 (fromMaybe [] ch2) onThese onThis onThat + attrs2 ← EFn.runEffectFn2 step attrs value2 + pure $ mkStep $ Step node (state {attrs = attrs2, children= children2}) (patchMicroapp fnObject) (haltMicroapp fnObject) + | otherwise → do + -- NOT HANDLED THIS IS DUMMY CODE + -- CASE WHERE SERVICE CHANGES IS NOT ACCEPTABLE [FOR NOW] + -- DOING NOTHING + pure $ mkStep $ Step node state (patchMicroapp fnObject) (haltMicroapp fnObject) + _ → do + EFn.runEffectFn1 (haltMicroapp fnObject) state + EFn.runEffectFn1 build vdom + +haltMicroapp ∷ ∀ a w. FnObject -> EFn.EffectFn1 (MicroAppState a w) Unit +haltMicroapp fnObject = EFn.mkEffectFn1 \{ node, attrs, children } → do + parent ← EFn.runEffectFn1 Util.parentNode node + EFn.runEffectFn3 Util.removeChild fnObject node parent + EFn.runEffectFn2 Util.forEachE children halt + EFn.runEffectFn1 halt attrs type TextState a w = { build ∷ VDomMachine a w @@ -70,31 +139,31 @@ type TextState a w = buildText ∷ ∀ a w. VDomBuilder String a w buildText = EFn.mkEffectFn3 \(VDomSpec spec) build s → do - node ← EFn.runEffectFn2 Util.createTextNode s spec.document + node ← EFn.runEffectFn1 Util.createTextNode s let state = { build, node, value: s } - pure $ mkStep $ Step node state patchText haltText + pure $ mkStep $ Step node state (patchText spec.fnObject) (haltText spec.fnObject) -patchText ∷ ∀ a w. EFn.EffectFn2 (TextState a w) (VDom a w) (VDomStep a w) -patchText = EFn.mkEffectFn2 \state vdom → do +patchText ∷ ∀ a w. FnObject -> EFn.EffectFn2 (TextState a w) (VDom a w) (VDomStep a w) +patchText fnObject = EFn.mkEffectFn2 \state vdom → do let { build, node, value: value1 } = state case vdom of Grafted g → - EFn.runEffectFn2 patchText state (runGraft g) + EFn.runEffectFn2 (patchText fnObject) state (runGraft g) Text value2 | value1 == value2 → - pure $ mkStep $ Step node state patchText haltText + pure $ mkStep $ Step node state (patchText fnObject) (haltText fnObject) | otherwise → do let nextState = { build, node, value: value2 } EFn.runEffectFn2 Util.setTextContent value2 node - pure $ mkStep $ Step node nextState patchText haltText + pure $ mkStep $ Step node nextState (patchText fnObject) (haltText fnObject) _ → do - EFn.runEffectFn1 haltText state + EFn.runEffectFn1 (haltText fnObject) state EFn.runEffectFn1 build vdom -haltText ∷ ∀ a w. EFn.EffectFn1 (TextState a w) Unit -haltText = EFn.mkEffectFn1 \{ node } → do +haltText ∷ ∀ a w. FnObject -> EFn.EffectFn1 (TextState a w) Unit +haltText fnObject = EFn.mkEffectFn1 \{ node } → do parent ← EFn.runEffectFn1 Util.parentNode node - EFn.runEffectFn2 Util.removeChild node parent + EFn.runEffectFn3 Util.removeChild fnObject node parent type ElemState a w = { build ∷ VDomMachine a w @@ -107,15 +176,15 @@ type ElemState a w = buildElem ∷ ∀ a w. VDomBuilder4 (Maybe Namespace) ElemName a (Array (VDom a w)) a w buildElem = EFn.mkEffectFn6 \(VDomSpec spec) build ns1 name1 as1 ch1 → do - el ← EFn.runEffectFn3 Util.createElement (toNullable ns1) name1 spec.document + el ← EFn.runEffectFn4 Util.createElement spec.fnObject (toNullable ns1) name1 "elem" let node = DOMElement.toNode el onChild = EFn.mkEffectFn2 \ix child → do res ← EFn.runEffectFn1 build child - EFn.runEffectFn3 Util.insertChildIx ix (extract res) node + EFn.runEffectFn6 Util.insertChildIx spec.fnObject "render" ix (extract res) node "" pure res children ← EFn.runEffectFn2 Util.forE ch1 onChild - attrs ← EFn.runEffectFn1 (spec.buildAttributes el) as1 + attrs ← EFn.runEffectFn1 (spec.buildAttributes spec.fnObject el) as1 let state = { build @@ -125,14 +194,14 @@ buildElem = EFn.mkEffectFn6 \(VDomSpec spec) build ns1 name1 as1 ch1 → do , name: name1 , children } - pure $ mkStep $ Step node state patchElem haltElem + pure $ mkStep $ Step node state (patchElem spec.fnObject) (haltElem spec.fnObject) -patchElem ∷ ∀ a w. EFn.EffectFn2 (ElemState a w) (VDom a w) (VDomStep a w) -patchElem = EFn.mkEffectFn2 \state vdom → do +patchElem ∷ ∀ a w. FnObject -> EFn.EffectFn2 (ElemState a w) (VDom a w) (VDomStep a w) +patchElem fnObject = EFn.mkEffectFn2 \state vdom → do let { build, node, attrs, ns: ns1, name: name1, children: ch1 } = state case vdom of Grafted g → - EFn.runEffectFn2 patchElem state (runGraft g) + EFn.runEffectFn2 (patchElem fnObject) state (runGraft g) Elem ns2 name2 as2 ch2 | Fn.runFn4 eqElemSpec ns1 name1 ns2 name2 → do case Array.length ch1, Array.length ch2 of 0, 0 → do @@ -146,19 +215,19 @@ patchElem = EFn.mkEffectFn2 \state vdom → do , name: name2 , children: ch1 } - pure $ mkStep $ Step node nextState patchElem haltElem + pure $ mkStep $ Step node nextState (patchElem fnObject) (haltElem fnObject) _, _ → do let - onThese = EFn.mkEffectFn3 \ix s v → do + onThese = EFn.mkEffectFn4 \obj ix s v → do res ← EFn.runEffectFn2 step s v - EFn.runEffectFn3 Util.insertChildIx ix (extract res) node + EFn.runEffectFn6 Util.insertChildIx obj "patch" ix (extract res) node "" pure res - onThis = EFn.mkEffectFn2 \ix s → EFn.runEffectFn1 halt s - onThat = EFn.mkEffectFn2 \ix v → do + onThis = EFn.mkEffectFn3 \_ _ s → EFn.runEffectFn1 halt s + onThat = EFn.mkEffectFn3 \obj ix v → do res ← EFn.runEffectFn1 build v - EFn.runEffectFn3 Util.insertChildIx ix (extract res) node + EFn.runEffectFn6 Util.insertChildIx obj "patch" ix (extract res) node "" pure res - children2 ← EFn.runEffectFn5 Util.diffWithIxE ch1 ch2 onThese onThis onThat + children2 ← EFn.runEffectFn6 Util.diffWithIxE fnObject ch1 ch2 onThese onThis onThat attrs2 ← EFn.runEffectFn2 step attrs as2 let nextState = @@ -169,15 +238,15 @@ patchElem = EFn.mkEffectFn2 \state vdom → do , name: name2 , children: children2 } - pure $ mkStep $ Step node nextState patchElem haltElem + pure $ mkStep $ Step node nextState (patchElem fnObject) (haltElem fnObject) _ → do - EFn.runEffectFn1 haltElem state + EFn.runEffectFn1 (haltElem fnObject) state EFn.runEffectFn1 build vdom -haltElem ∷ ∀ a w. EFn.EffectFn1 (ElemState a w) Unit -haltElem = EFn.mkEffectFn1 \{ node, attrs, children } → do +haltElem ∷ ∀ a w. FnObject -> EFn.EffectFn1 (ElemState a w) Unit +haltElem fnObject = EFn.mkEffectFn1 \{ node, attrs, children } → do parent ← EFn.runEffectFn1 Util.parentNode node - EFn.runEffectFn2 Util.removeChild node parent + EFn.runEffectFn3 Util.removeChild fnObject node parent EFn.runEffectFn2 Util.forEachE children halt EFn.runEffectFn1 halt attrs @@ -193,15 +262,15 @@ type KeyedState a w = buildKeyed ∷ ∀ a w. VDomBuilder4 (Maybe Namespace) ElemName a (Array (Tuple String (VDom a w))) a w buildKeyed = EFn.mkEffectFn6 \(VDomSpec spec) build ns1 name1 as1 ch1 → do - el ← EFn.runEffectFn3 Util.createElement (toNullable ns1) name1 spec.document + el ← EFn.runEffectFn4 Util.createElement spec.fnObject (toNullable ns1) name1 "keyed" let node = DOMElement.toNode el - onChild = EFn.mkEffectFn3 \k ix (Tuple _ vdom) → do + onChild = EFn.mkEffectFn4 \k ix _ (Tuple _ vdom) → do res ← EFn.runEffectFn1 build vdom - EFn.runEffectFn3 Util.insertChildIx ix (extract res) node + EFn.runEffectFn6 Util.insertChildIx spec.fnObject "render" ix (extract res) node k pure res children ← EFn.runEffectFn3 Util.strMapWithIxE ch1 fst onChild - attrs ← EFn.runEffectFn1 (spec.buildAttributes el) as1 + attrs ← EFn.runEffectFn1 (spec.buildAttributes spec.fnObject el) as1 let state = { build @@ -212,14 +281,14 @@ buildKeyed = EFn.mkEffectFn6 \(VDomSpec spec) build ns1 name1 as1 ch1 → do , children , length: Array.length ch1 } - pure $ mkStep $ Step node state patchKeyed haltKeyed + pure $ mkStep $ Step node state (patchKeyed spec.fnObject) (haltKeyed spec.fnObject) -patchKeyed ∷ ∀ a w. EFn.EffectFn2 (KeyedState a w) (VDom a w) (VDomStep a w) -patchKeyed = EFn.mkEffectFn2 \state vdom → do +patchKeyed ∷ ∀ a w. FnObject -> EFn.EffectFn2 (KeyedState a w) (VDom a w) (VDomStep a w) +patchKeyed fnObject = EFn.mkEffectFn2 \state vdom → do let { build, node, attrs, ns: ns1, name: name1, children: ch1, length: len1 } = state case vdom of Grafted g → - EFn.runEffectFn2 patchKeyed state (runGraft g) + EFn.runEffectFn2 (patchKeyed fnObject) state (runGraft g) Keyed ns2 name2 as2 ch2 | Fn.runFn4 eqElemSpec ns1 name1 ns2 name2 → case len1, Array.length ch2 of 0, 0 → do @@ -234,19 +303,19 @@ patchKeyed = EFn.mkEffectFn2 \state vdom → do , children: ch1 , length: 0 } - pure $ mkStep $ Step node nextState patchKeyed haltKeyed + pure $ mkStep $ Step node nextState (patchKeyed fnObject) (haltKeyed fnObject) _, len2 → do let - onThese = EFn.mkEffectFn4 \_ ix' s (Tuple _ v) → do + onThese = EFn.mkEffectFn5 \obj k ix' s (Tuple _ v) → do res ← EFn.runEffectFn2 step s v - EFn.runEffectFn3 Util.insertChildIx ix' (extract res) node + EFn.runEffectFn6 Util.insertChildIx obj "patch" ix' (extract res) node k pure res - onThis = EFn.mkEffectFn2 \_ s → EFn.runEffectFn1 halt s - onThat = EFn.mkEffectFn3 \_ ix (Tuple _ v) → do + onThis = EFn.mkEffectFn3 \_ _ s → EFn.runEffectFn1 halt s + onThat = EFn.mkEffectFn4 \obj k ix (Tuple _ v) → do res ← EFn.runEffectFn1 build v - EFn.runEffectFn3 Util.insertChildIx ix (extract res) node + EFn.runEffectFn6 Util.insertChildIx obj "patch" ix (extract res) node k pure res - children2 ← EFn.runEffectFn6 Util.diffWithKeyAndIxE ch1 ch2 fst onThese onThis onThat + children2 ← EFn.runEffectFn7 Util.diffWithKeyAndIxE fnObject ch1 ch2 fst onThese onThis onThat attrs2 ← EFn.runEffectFn2 step attrs as2 let nextState = @@ -258,15 +327,15 @@ patchKeyed = EFn.mkEffectFn2 \state vdom → do , children: children2 , length: len2 } - pure $ mkStep $ Step node nextState patchKeyed haltKeyed + pure $ mkStep $ Step node nextState (patchKeyed fnObject) (haltKeyed fnObject) _ → do - EFn.runEffectFn1 haltKeyed state + EFn.runEffectFn1 (haltKeyed fnObject) state EFn.runEffectFn1 build vdom -haltKeyed ∷ ∀ a w. EFn.EffectFn1 (KeyedState a w) Unit -haltKeyed = EFn.mkEffectFn1 \{ node, attrs, children } → do +haltKeyed ∷ ∀ a w. FnObject -> EFn.EffectFn1 (KeyedState a w) Unit +haltKeyed fnObject = EFn.mkEffectFn1 \{ node, attrs, children } → do parent ← EFn.runEffectFn1 Util.parentNode node - EFn.runEffectFn2 Util.removeChild node parent + EFn.runEffectFn3 Util.removeChild fnObject node parent EFn.runEffectFn2 Util.forInE children (EFn.mkEffectFn2 \_ s → EFn.runEffectFn1 halt s) EFn.runEffectFn1 halt attrs @@ -279,7 +348,7 @@ buildWidget ∷ ∀ a w. VDomBuilder w a w buildWidget = EFn.mkEffectFn3 \(VDomSpec spec) build w → do res ← EFn.runEffectFn1 (spec.buildWidget (VDomSpec spec)) w let - res' = res # unStep \(Step n s k1 k2) → + res' = res # unStep \(Step n _ _ _) → mkStep $ Step n { build, widget: res } patchWidget haltWidget pure res' @@ -292,7 +361,7 @@ patchWidget = EFn.mkEffectFn2 \state vdom → do Widget w → do res ← EFn.runEffectFn2 step widget w let - res' = res # unStep \(Step n s k1 k2) → + res' = res # unStep \(Step n _ _ _) → mkStep $ Step n { build, widget: res } patchWidget haltWidget pure res' _ → do @@ -303,6 +372,94 @@ haltWidget ∷ forall a w. EFn.EffectFn1 (WidgetState a w) Unit haltWidget = EFn.mkEffectFn1 \{ widget } → do EFn.runEffectFn1 halt widget +type ChunkState a w = + { build ∷ VDomMachine a w + , node ∷ DOM.Node + , attrs ∷ Step a Unit + , ns ∷ Maybe Namespace + , name ∷ ElemName + , children ∷ Array ({ shimmer :: VDomStep a w, layout :: VDomStep a w }) + } + +buildChunk ∷ ∀ a w. VDomBuilder4 (Maybe Namespace) ElemName a (ShimmerHolder a w) a w +buildChunk = EFn.mkEffectFn6 \(VDomSpec spec) build ns1 name1 as1 ch1 → do + el ← EFn.runEffectFn3 Util.createChunkedElement spec.fnObject (toNullable ns1) name1 + let + node = DOMElement.toNode el + onChild = EFn.mkEffectFn2 \ix child → do + res1 ← EFn.runEffectFn1 build child.shimmer + res2 ← EFn.runEffectFn1 build child.actualLayout + let res = { shimmer: (extract res1), layout: (extract res2)} + EFn.runEffectFn5 Util.insertChunkIx spec.fnObject "render" ix res node + pure { shimmer: res1, layout: res2 } + children ← EFn.runEffectFn2 Util.forE ch1 onChild + attrs ← EFn.runEffectFn1 (spec.buildAttributes spec.fnObject el) as1 + let + state = + { build + , node + , attrs + , ns: ns1 + , name: name1 + , children + } + pure $ mkStep $ Step node state (patchChunk spec.fnObject) (haltChunk spec.fnObject) + +patchChunk ∷ ∀ a w. FnObject -> EFn.EffectFn2 (ChunkState a w) (VDom a w) (VDomStep a w) +patchChunk fnObject = EFn.mkEffectFn2 \state vdom → do + let { build, node, attrs, ns: ns1, name: name1, children: ch1 } = state + case vdom of + Grafted g → + EFn.runEffectFn2 (patchChunk fnObject) state (runGraft g) + Chunk ns2 name2 as2 ch2 | Fn.runFn4 eqElemSpec ns1 name1 ns2 name2 → do + case Array.length ch1, Array.length ch2 of + 0, 0 → do + attrs2 ← EFn.runEffectFn2 step attrs as2 + let + nextState = + { build + , node + , attrs: attrs2 + , ns: ns2 + , name: name2 + , children: ch1 + } + pure $ mkStep $ Step node nextState (patchChunk fnObject) (haltChunk fnObject) + _, _ → do + let + onThese = EFn.mkEffectFn4 \obj ix s v → do + -- res1 ← EFn.runEffectFn2 step s v.shimmer + res2 ← EFn.runEffectFn2 step s v.actualLayout + let res = { shimmer: (extract s), layout: (extract res2) } + EFn.runEffectFn5 Util.insertChunkIx obj "patch" ix res node + pure { shimmer: s, layout: res2 } + onThis = EFn.mkEffectFn3 \_ _ s → EFn.runEffectFn1 halt s + onThat = EFn.mkEffectFn3 \obj ix v → do + res1 ← EFn.runEffectFn1 build v.shimmer + res2 ← EFn.runEffectFn1 build v.actualLayout + let res = { shimmer: (extract res1), layout: (extract res2)} + EFn.runEffectFn5 Util.insertChunkIx obj "patch" ix res node + pure { shimmer: res1, layout: res2 } + children2 ← EFn.runEffectFn6 Util.diffChunkWithIxE fnObject ch1 ch2 onThese onThis onThat + attrs2 ← EFn.runEffectFn2 step attrs as2 + let + nextState = + { build + , node + , attrs: attrs2 + , ns: ns2 + , name: name2 + , children: children2 + } + pure $ mkStep $ Step node nextState (patchChunk fnObject) (haltChunk fnObject) + _ → do + EFn.runEffectFn1 (haltChunk fnObject) state + EFn.runEffectFn1 build vdom + +haltChunk ∷ ∀ a w. FnObject -> EFn.EffectFn1 (ChunkState a w) Unit +haltChunk _ = EFn.mkEffectFn1 \_ → do + pure unit + eqElemSpec ∷ Fn.Fn4 (Maybe Namespace) ElemName (Maybe Namespace) ElemName Boolean eqElemSpec = Fn.mkFn4 \ns1 (ElemName name1) ns2 (ElemName name2) → if name1 == name2 diff --git a/src/Halogen/VDom/DOM/Prop.js b/src/Halogen/VDom/DOM/Prop.js new file mode 100644 index 0000000..b19e38e --- /dev/null +++ b/src/Halogen/VDom/DOM/Prop.js @@ -0,0 +1,6 @@ + +export const eqPropValues = function (x) { + return function (y){ + return x == y; + }; +}; diff --git a/src/Halogen/VDom/DOM/Prop.purs b/src/Halogen/VDom/DOM/Prop.purs index 54c93e2..668a967 100644 --- a/src/Halogen/VDom/DOM/Prop.purs +++ b/src/Halogen/VDom/DOM/Prop.purs @@ -7,6 +7,7 @@ module Halogen.VDom.DOM.Prop , propFromInt , propFromNumber , buildProp + , propFromAny ) where import Prelude @@ -22,13 +23,14 @@ import Foreign (typeOf) import Foreign.Object as Object import Halogen.VDom as V import Halogen.VDom.Machine (Step'(..), mkStep) -import Halogen.VDom.Types (Namespace(..)) +import Halogen.VDom.Types (Namespace(..), FnObject) import Halogen.VDom.Util as Util import Unsafe.Coerce (unsafeCoerce) import Web.DOM.Element (Element) as DOM import Web.Event.Event (EventType(..), Event) as DOM import Web.Event.EventTarget (eventListener) as DOM + -- | Attributes, properties, event handlers, and element lifecycles. -- | Parameterized by the type of handlers outputs. data Prop a @@ -36,11 +38,15 @@ data Prop a | Property String PropValue | Handler DOM.EventType (DOM.Event → Maybe a) | Ref (ElemRef DOM.Element → Maybe a) + | BHandler String (Unit -> Maybe a) + | Payload String + | Nopatch String PropValue + | ListData (Array (Object.Object PropValue)) instance functorProp ∷ Functor Prop where map f (Handler ty g) = Handler ty (map f <$> g) map f (Ref g) = Ref (map f <$> g) - map f p = unsafeCoerce p + map _ p = unsafeCoerce p data ElemRef a = Created a @@ -52,6 +58,12 @@ instance functorElemRef ∷ Functor ElemRef where foreign import data PropValue ∷ Type +foreign import eqPropValues :: PropValue -> PropValue -> Boolean + +instance eqPropValue :: Eq PropValue where + eq p1 p2 = eqPropValues p1 p2 + + propFromString ∷ String → PropValue propFromString = unsafeCoerce @@ -64,38 +76,46 @@ propFromInt = unsafeCoerce propFromNumber ∷ Number → PropValue propFromNumber = unsafeCoerce +propFromAny :: forall a. a -> PropValue +propFromAny = unsafeCoerce + -- | A `Machine`` for applying attributes, properties, and event handlers. -- | An emitter effect must be provided to respond to events. For example, -- | to allow arbitrary effects in event handlers, one could use `id`. buildProp ∷ ∀ a . (a → Effect Unit) + → FnObject → DOM.Element → V.Machine (Array (Prop a)) Unit -buildProp emit el = renderProp +buildProp emit fnObject el = renderProp where renderProp = EFn.mkEffectFn1 \ps1 → do events ← Util.newMutMap - ps1' ← EFn.runEffectFn3 Util.strMapWithIxE ps1 propToStrKey (applyProp events) + _ ← Util.newMutMap + listData ← Util.newMutMap + ps1' ← EFn.runEffectFn3 Util.strMapWithIxE ps1 propToStrKey (applyProp "render" events) let state = { events: Util.unsafeFreeze events , props: ps1' + , listData : Util.unsafeFreeze listData } pure $ mkStep $ Step unit state patchProp haltProp patchProp = EFn.mkEffectFn2 \state ps2 → do events ← Util.newMutMap let - { events: prevEvents, props: ps1 } = state - onThese = Fn.runFn2 diffProp prevEvents events + { events: prevEvents, props: ps1, listData : prevListData } = state + onThese = Fn.runFn3 diffProp prevEvents events prevListData onThis = removeProp prevEvents - onThat = applyProp events - props ← EFn.runEffectFn6 Util.diffWithKeyAndIxE ps1 ps2 propToStrKey onThese onThis onThat + onThat = applyProp "patch" events + props ← EFn.runEffectFn8 Util.diffPropWithKeyAndIxE fnObject ps1 ps2 propToStrKey onThese onThis onThat el let nextState = { events: Util.unsafeFreeze events , props + , listData : prevListData } pure $ mkStep $ Step unit nextState patchProp haltProp @@ -109,13 +129,20 @@ buildProp emit el = renderProp Just a → emit a _ → pure unit - applyProp events = EFn.mkEffectFn3 \_ _ v → + applyProp pr events = EFn.mkEffectFn4 \_ _ props v → case v of Attribute ns attr val → do EFn.runEffectFn4 Util.setAttribute (toNullable ns) attr val el pure v Property prop val → do - EFn.runEffectFn3 setProperty prop val el + case pr of + "render" -> EFn.runEffectFn3 setProperty prop val el + _ -> EFn.runEffectFn3 Util.unsafeSetAny prop val props + pure v + Nopatch prop val → do + case pr of + "render" -> EFn.runEffectFn3 setProperty prop val el + _ -> EFn.runEffectFn3 Util.unsafeSetAny prop val props pure v Handler (DOM.EventType ty) f → do case Fn.runFn2 Util.unsafeGetAny ty events of @@ -128,13 +155,33 @@ buildProp emit el = renderProp f' ← Ref.read ref EFn.runEffectFn1 mbEmit (f' ev) EFn.runEffectFn3 Util.pokeMutMap ty (Tuple listener ref) events - EFn.runEffectFn3 Util.addEventListener ty listener el + EFn.runEffectFn5 Util.addEventListener fnObject pr ty listener el pure v Ref f → do EFn.runEffectFn1 mbEmit (f (Created el)) pure v + BHandler _ behavior → do + EFn.runEffectFn1 mbEmit (behavior unit) + pure v + Payload payload -> do + _ <- case pr of + "render" -> EFn.runEffectFn3 updateMicroAppPayload payload el false + _ -> EFn.runEffectFn3 updateMicroAppPayload payload el true + -- TODO ADD UPDATE_ORDER :: THIS LOOKS USELESS TO BE HONEST + pure v + ListData ld -> do + -- Create Run In UI for all props and add to some sort of state + -- _ <- case pr of + -- "render" -> + -- Call setProp with the final recieved value + -- _ -> + -- Call unsafeSetAny with the final recieved value + _ <- case pr of + "render" -> EFn.runEffectFn3 setProperty "listData" (unsafeCoerce ld) el + _ -> EFn.runEffectFn3 Util.unsafeSetAny "listData" (unsafeCoerce ld) props + pure v - diffProp = Fn.mkFn2 \prevEvents events → EFn.mkEffectFn4 \_ _ v1 v2 → + diffProp = Fn.mkFn3 \prevEvents events listState → EFn.mkEffectFn5 \_ _ props v1 v2 → case v1, v2 of Attribute _ _ val1, Attribute ns2 attr2 val2 → if val1 == val2 @@ -151,10 +198,17 @@ buildProp emit el = renderProp if Fn.runFn2 Util.refEq elVal val2 then pure v2 else do - EFn.runEffectFn3 setProperty prop2 val2 el + EFn.runEffectFn3 Util.unsafeSetAny prop2 val2 props pure v2 _, _ → do - EFn.runEffectFn3 setProperty prop2 val2 el + EFn.runEffectFn3 Util.unsafeSetAny prop2 val2 props + pure v2 + Payload val1, Payload val2 → + if Fn.runFn2 Util.refEq val1 val2 + then pure v2 + else do + EFn.runEffectFn3 Util.unsafeSetAny "payload" val2 props + EFn.runEffectFn3 updateMicroAppPayload val2 el true pure v2 Handler _ _, Handler (DOM.EventType ty) f → do let @@ -162,6 +216,13 @@ buildProp emit el = renderProp Ref.write f (snd handler) EFn.runEffectFn3 Util.pokeMutMap ty handler events pure v2 + ListData ld1, ListData ld2 -> do + -- diff; + -- Call parseParams for new props + -- Call removeProp for old props + -- Merge all runInUI props + EFn.runEffectFn6 Util.diffArrayOfObjects fnObject listState el ld1 ld2 props + pure v2 _, _ → pure v2 @@ -171,32 +232,47 @@ buildProp emit el = renderProp EFn.runEffectFn3 Util.removeAttribute (toNullable ns) attr el Property prop _ → EFn.runEffectFn2 removeProperty prop el + Nopatch prop _ → + EFn.runEffectFn2 removeProperty prop el + ListData _ → + EFn.runEffectFn2 removeProperty "listData" el Handler (DOM.EventType ty) _ → do let handler = Fn.runFn2 Util.unsafeLookup ty prevEvents EFn.runEffectFn3 Util.removeEventListener ty (fst handler) el Ref _ → pure unit + BHandler ty _ → do + _ <- EFn.runEffectFn1 fnObject.cancelBehavior ty + pure unit + Payload _ -> pure unit + + updateMicroAppPayload ∷ EFn.EffectFn3 String DOM.Element Boolean Unit + updateMicroAppPayload = fnObject.updateMicroAppPayload propToStrKey ∷ ∀ i. Prop i → String propToStrKey = case _ of Attribute (Just (Namespace ns)) attr _ → "attr/" <> ns <> ":" <> attr Attribute _ attr _ → "attr/:" <> attr Property prop _ → "prop/" <> prop + Nopatch prop _ → "prop/" <> prop Handler (DOM.EventType ty) _ → "handler/" <> ty Ref _ → "ref" + BHandler ty _ -> "bhandler/" <> ty + Payload _ -> "payload" + ListData _ → "prop/listData" setProperty ∷ EFn.EffectFn3 String PropValue DOM.Element Unit -setProperty = Util.unsafeSetAny +setProperty = Util.unsafeSetProp unsafeGetProperty ∷ Fn.Fn2 String DOM.Element PropValue -unsafeGetProperty = Util.unsafeGetAny +unsafeGetProperty = Util.unsafeGetProp removeProperty ∷ EFn.EffectFn2 String DOM.Element Unit removeProperty = EFn.mkEffectFn2 \key el → - case typeOf (Fn.runFn2 Util.unsafeGetAny key el) of - "string" → EFn.runEffectFn3 Util.unsafeSetAny key "" el + case typeOf (Fn.runFn2 Util.unsafeGetProp key el) of + "string" → EFn.runEffectFn3 Util.removeProperty key "" el _ → case key of "rowSpan" → EFn.runEffectFn3 Util.unsafeSetAny key 1 el "colSpan" → EFn.runEffectFn3 Util.unsafeSetAny key 1 el - _ → EFn.runEffectFn3 Util.unsafeSetAny key Util.jsUndefined el + _ → EFn.runEffectFn3 Util.removeProperty key Util.jsUndefined el diff --git a/src/Halogen/VDom/Thunk.purs b/src/Halogen/VDom/Thunk.purs index 56e2530..748d888 100644 --- a/src/Halogen/VDom/Thunk.purs +++ b/src/Halogen/VDom/Thunk.purs @@ -1,5 +1,7 @@ module Halogen.VDom.Thunk - ( Thunk + ( Thunk(..) + , ThunkArg + , ThunkId , buildThunk , runThunk , hoist @@ -7,6 +9,8 @@ module Halogen.VDom.Thunk , thunk1 , thunk2 , thunk3 + , unsafeEqThunk + , unsafeThunkId ) where import Prelude @@ -23,6 +27,7 @@ foreign import data ThunkArg ∷ Type foreign import data ThunkId ∷ Type +data Thunk :: forall k. (k -> Type) -> k -> Type data Thunk f i = Thunk ThunkId (Fn.Fn2 ThunkArg ThunkArg Boolean) (ThunkArg → f i) ThunkArg unsafeThunkId ∷ ∀ a. a → ThunkId @@ -82,6 +87,7 @@ unsafeEqThunk = Fn.mkFn2 \(Thunk a1 b1 _ d1) (Thunk a2 b2 _ d2) → Fn.runFn2 Util.refEq b1 b2 && Fn.runFn2 Util.refEq d1 d2 +type ThunkState :: forall k. (k -> Type) -> k -> Type -> Type -> Type type ThunkState f i a w = { thunk ∷ Thunk f i , vdom ∷ M.Step (V.VDom a w) Node diff --git a/src/Halogen/VDom/Types.purs b/src/Halogen/VDom/Types.purs index ac3c356..0c4c243 100644 --- a/src/Halogen/VDom/Types.purs +++ b/src/Halogen/VDom/Types.purs @@ -7,11 +7,16 @@ module Halogen.VDom.Types , runGraft , ElemName(..) , Namespace(..) + , FnObject(..) + , ShimmerHolder(..) + , ShimmerItem(..) ) where import Prelude +import Effect (Effect) +import Effect.Uncurried as EFn import Data.Bifunctor (class Bifunctor, bimap) -import Data.Maybe (Maybe) +import Data.Maybe (Maybe(..)) import Data.Newtype (class Newtype) import Data.Tuple (Tuple) import Unsafe.Coerce (unsafeCoerce) @@ -25,17 +30,19 @@ import Unsafe.Coerce (unsafeCoerce) data VDom a w = Text String | Elem (Maybe Namespace) ElemName a (Array (VDom a w)) + | Chunk (Maybe Namespace) ElemName a (ShimmerHolder a w) | Keyed (Maybe Namespace) ElemName a (Array (Tuple String (VDom a w))) | Widget w | Grafted (Graft a w) + | Microapp String a (Maybe (Array (VDom a w))) instance functorVDom ∷ Functor (VDom a) where - map g (Text a) = Text a + map _ (Text a) = Text a map g (Grafted a) = Grafted (map g a) map g a = Grafted (graft (Graft identity g a)) instance bifunctorVDom ∷ Bifunctor VDom where - bimap f g (Text a) = Text a + bimap _ _ (Text a) = Text a bimap f g (Grafted a) = Grafted (bimap f g a) bimap f g a = Grafted (graft (Graft f g a)) @@ -75,9 +82,20 @@ runGraft = go (Keyed ns n a ch) = Keyed ns n (fa a) (map (map go) ch) go (Widget w) = Widget (fw w) go (Grafted g) = Grafted (bimap fa fw g) + go (Microapp s a child) = Microapp s (fa a) $ + case child of + Just ch -> (Just $ map go ch) + _ -> Nothing + go (Chunk ns n a ch) = Chunk ns n (fa a) (chunkMap go ch) in go v +chunkMap :: forall a a' w w'. (VDom a w -> VDom a' w') -> ShimmerHolder a w -> ShimmerHolder a' w' +chunkMap go shimHolder = map (shimmerItemMap go) shimHolder + +shimmerItemMap :: forall a a' w w'. (VDom a w -> VDom a' w') -> ShimmerItem a w -> ShimmerItem a' w' +shimmerItemMap go item = { shimmer : (go item.shimmer) , actualLayout : (go item.actualLayout) } + newtype ElemName = ElemName String derive instance newtypeElemName ∷ Newtype ElemName _ @@ -89,3 +107,23 @@ newtype Namespace = Namespace String derive instance newtypeNamespace ∷ Newtype Namespace _ derive newtype instance eqNamespace ∷ Eq Namespace derive newtype instance ordNamespace ∷ Ord Namespace + +type ShimmerHolder a w = Array (ShimmerItem a w) + +type ShimmerItem a w = + { shimmer :: VDom a w + , actualLayout :: VDom a w + } + +type FnObject = + { replaceView :: forall a . EFn.EffectFn3 a String (Array String) Unit + , setManualEvents :: forall a b. a -> b -> Effect Unit + , updateChildren :: forall a. EFn.EffectFn1 a Unit + , removeChild :: forall a b. EFn.EffectFn3 a b Int Unit + , createPrestoElement:: forall a. Effect a + , cancelBehavior :: EFn.EffectFn1 String Unit + , manualEventsName :: Array String + , updateMicroAppPayload :: ∀ b. EFn.EffectFn3 String b Boolean Unit + , updateProperties :: forall a b. EFn.EffectFn2 a b Unit + , parseParams :: forall a b c d. EFn.EffectFn3 a b c d + } diff --git a/src/Halogen/VDom/Util.js b/src/Halogen/VDom/Util.js index cf540ab..4913154 100644 --- a/src/Halogen/VDom/Util.js +++ b/src/Halogen/VDom/Util.js @@ -1,22 +1,40 @@ -"use strict"; - -exports.unsafeGetAny = function (key, obj) { +export const unsafeGetAny = function (key, obj) { return obj[key]; }; -exports.unsafeHasAny = function (key, obj) { - return obj.hasOwnProperty(key); +export const unsafeGetProp = function (key, obj) { + if (obj.props) + return obj.props[key]; +}; + +export const unsafeHasAny = function (key, obj) { + return Object.prototype.hasOwnProperty.call(obj,key); }; -exports.unsafeSetAny = function (key, val, obj) { - obj[key] = val; +export const unsafeSetAny = function (key, val, obj) { + obj[key] = val; }; -exports.unsafeDeleteAny = function (key, obj) { - delete obj[key]; +export const unsafeSetProp = function (key, val, obj) { + if(key == "id2"){ + obj.__ref = {__id : val}; + obj.props.id = val; + delete obj.props.id2; + } else { + obj.props[key] = val; + } }; -exports.forE = function (a, f) { +export const removeProperty = function (key, val, obj) { + obj.props[key] = val; + delete obj.props[key]; +}; + +export const unsafeDeleteAny = function (key, obj) { + delete obj.props[key]; +}; + +export const forE = function (a, f) { var b = []; for (var i = 0; i < a.length; i++) { b.push(f(i, a[i])); @@ -24,13 +42,13 @@ exports.forE = function (a, f) { return b; }; -exports.forEachE = function (a, f) { +export const forEachE = function (a, f) { for (var i = 0; i < a.length; i++) { f(a[i]); } }; -exports.forInE = function (o, f) { +export const forInE = function (o, f) { var ks = Object.keys(o); for (var i = 0; i < ks.length; i++) { var k = ks[i]; @@ -38,123 +56,306 @@ exports.forInE = function (o, f) { } }; -exports.replicateE = function (n, f) { +export const replicateE = function (n, f) { for (var i = 0; i < n; i++) { f(); } }; -exports.diffWithIxE = function (a1, a2, f1, f2, f3) { +export const diffWithIxE = function (fnObject, a1, a2, f1, f2, f3) { + // console.log("This fails in chunking because:", fnObject, a1, a2, f1, f2, f3); + var actions = []; var a3 = []; var l1 = a1.length; var l2 = a2.length; var i = 0; + // eslint-disable-next-line no-constant-condition while (1) { if (i < l1) { if (i < l2) { - a3.push(f1(i, a1[i], a2[i])); + a3.push(f1(actions, i, a1[i], a2[i])); } else { - f2(i, a1[i]); + f2(actions, i, a1[i]); } } else if (i < l2) { - a3.push(f3(i, a2[i])); + a3.push(f3(actions, i, a2[i])); } else { break; } i++; } + if(actions.length > 0) { + fnObject.updateChildren(actions); + } return a3; }; -exports.strMapWithIxE = function (as, fk, f) { +export const strMapWithIxE = function (as, fk, f) { var o = {}; + var m = {}; for (var i = 0; i < as.length; i++) { var a = as[i]; var k = fk(a); - o[k] = f(k, i, a); + if(k=="prop/id2"){ + f(k,i,m,a); + continue; + } + o[k] = f(k, i, m, a); } return o; }; -exports.diffWithKeyAndIxE = function (o1, as, fk, f1, f2, f3) { +export const diffWithKeyAndIxE = function (fnObject, o1, as, fk, f1, f2, f3) { var o2 = {}; + var actions = []; for (var i = 0; i < as.length; i++) { var a = as[i]; - var k = fk(a); - if (o1.hasOwnProperty(k)) { - o2[k] = f1(k, i, o1[k], a); + let k = fk(a); + if (Object.prototype.hasOwnProperty.call(o1,k)) { + o2[k] = f1(actions, k, i, o1[k], a); + } else { + o2[k] = f3(actions, k, i, a); + } + } + for (var k in o1) { + if (k in o2) { + continue; + } + f2(actions, k, o1[k]); + } + if(actions.length > 0) { + fnObject.updateChildren(actions); + } + return o2; +}; + +export const diffPropWithKeyAndIxE = function (fnObject, o1, as, fk, f1, f2, f3, el) { + var removedProps = []; + var o2 = {}; + var updatedProps = {}; + var replace = false; + for (var i = 0; i < as.length; i++) { + var a = as[i]; + let k = fk(a); + if (Object.prototype.hasOwnProperty.call(o1,k)) { + o2[k] = f1(k, i, updatedProps, o1[k], a); } else { - o2[k] = f3(k, i, a); + o2[k] = f3(k, i, updatedProps, a); } } for (var k in o1) { if (k in o2) { continue; } + replace = true; f2(k, o1[k]); + removedProps.push(k); + } + if (replace) { + for(var key in updatedProps) { + el.props[key] = updatedProps[key]; + } + fnObject.replaceView(el, "", removedProps); + } else if(Object.keys(updatedProps).length > 0) { + fnObject.updateProperties(el, updatedProps); } return o2; }; -exports.refEq = function (a, b) { +export const diffArrayOfObjects = function (fnObject, listState, el, oldArray, newArray, updatedProps) { + // TODO :: Optimise with old Array + list State in the future; + var hasDiff = false; + if(oldArray.length != newArray.length) { + hasDiff = true; + } else { + for(var j = 0; j < newArray.length; ++j) { + for(var key in newArray[j]) { + hasDiff = newArray[j][key] != oldArray[j][key]; + if(hasDiff) + break; + } + if(hasDiff) + break; + } + } + if(hasDiff) { + updatedProps.listData = newArray; + } +}; + +export const refEq = function (a, b) { return a === b; }; -exports.createTextNode = function (s, doc) { - return doc.createTextNode(s); +export const createTextNode = function (s) { + return {type: "textView", children: [], props: {text: s}}; }; -exports.setTextContent = function (s, n) { +export const setTextContent = function (s, n) { n.textContent = s; }; -exports.createElement = function (ns, name, doc) { - if (ns != null) { - return doc.createElementNS(ns, name); +export const createElement = function (fnObject, ns, name, elemType) { + return {type: name, children: [], props: {}, __ref: fnObject.createPrestoElement(), elemType : elemType ? elemType : undefined}; +}; + +export const createChunkedElement = function(fnObject, ns, name) { + return {type: name, chunkedLayout: true, children: [], layouts: [], props: {}, __ref: fnObject.createPrestoElement()}; +}; + +export const createMicroapp = function (fnObject, requestId, service ) { + return {type: "microapp", children: [], props: {}, requestId : requestId, __ref: fnObject.createPrestoElement(), service : service}; +}; + +export const insertChildIx = function (obj, type, i, a, b, keyId) { + var n = (b.children[i]) || {__ref: {__id: "-1"}}; + if (!a) + // eslint-disable-next-line no-undef + console.warn("CUSTOM VDOM ERROR !! : ", "Trying to add undefined element to ", b); + + if (n === a) { + return; + } + if(keyId != ""){ + a.keyId = keyId; + } + if (type !== "patch") { + if((!window.parent.generateVdom) || a.elemType == "elem" || a.elemType == "keyed"){ + a.parentNode = b; + b.children.splice(i, 0, a); + } + + return; + } + + var index = b.children.indexOf(a); + if (index !== -1) { + b.children.splice(index, 1); + obj.push({action : "move", parent : b, elem : a, index : i}); } else { - return doc.createElement(name) + obj.push({action : "add", parent : b, elem : a, index : i}); } + b.children.splice(i, 0, a); + a.parentNode = b; }; -exports.insertChildIx = function (i, a, b) { - var n = b.childNodes.item(i) || null; - if (n !== a) { - b.insertBefore(a, n); +export const insertChunkIx = function(obj, opType, index, child, parentNode) { + var n = (parentNode.children[index]) || {__ref: {__id: "-1"}}; + if (!child) + // eslint-disable-next-line no-undef + console.warn("CUSTOM VDOM ERROR !! : ", "Trying to add undefined element to ", parentNode); + + if (n === child) { + return; + } + if (opType !== "patch") { + child.layout.parentNode = child.shimmer.parentNode = parentNode; + parentNode.children.splice(index, 0, child.shimmer); + parentNode.layouts.splice(index, 0, child.layout); + return; + } +}; + +export const diffChunkWithIxE = function(fnObject, a1, a2, f1, f2, f3) { + var actions = []; + var a3 = []; + var l1 = a1.length; + var l2 = a2.length; + var i = 0; + // eslint-disable-next-line no-constant-condition + while (1) { + if (i < l1) { + if (i < l2) { + a3.push(f1(actions, i, a1[i].layout, a2[i])); + } else { + f2(actions, i, a1[i].layout); + } + } else if (i < l2) { + a3.push(f3(actions, i, a2[i])); + } else { + break; + } + i++; } + if(actions.length > 0) { + fnObject.updateChildren(actions); + } + return a3; }; -exports.removeChild = function (a, b) { - if (b && a.parentNode === b) { - b.removeChild(a); +export const removeChild = function (fnObject, a, b) { + var childIndex = -1; + + if (b && a.parentNode.__ref.__id === b.__ref.__id) { + for (var i=0; i -1) { + fnObject.removeChild(a,b,childIndex); + a.props.__removed = true; + b.children.splice(childIndex, 1); } }; -exports.parentNode = function (a) { - return a.parentNode; +export const parentNode = function (a) { + if (a.parentNode.props.__removed) { + a.props.__removed = true; + return null; + } else { + return a.parentNode; + } }; -exports.setAttribute = function (ns, attr, val, el) { - if (ns != null) { +export const setAttribute = function (ns, attr, val, el) { + if (ns !== null) { el.setAttributeNS(ns, attr, val); } else { el.setAttribute(attr, val); } }; -exports.removeAttribute = function (ns, attr, el) { - if (ns != null) { +export const removeAttribute = function (ns, attr, el) { + if (ns !== null) { el.removeAttributeNS(ns, attr); } else { el.removeAttribute(attr); } }; -exports.addEventListener = function (ev, listener, el) { - el.addEventListener(ev, listener, false); +export const addEventListener = function (fnObject, pr, ev, listener, el) { + try{ + if((typeof fnObject.manualEventsName != "undefined") && + (Array.isArray(fnObject.manualEventsName)) && + (typeof fnObject.setManualEvents == "function") && + (fnObject.manualEventsName.indexOf(ev) != -1) + ){ + fnObject.setManualEvents(ev)(listener)(); + } + } catch(err){ + // eslint-disable-next-line no-undef + console.error("Error while checking for manualEvents \n",err); + } + el.props[ev] = listener; + if(pr == "patch") { + fnObject.replaceView(el, ev, []); + } }; -exports.removeEventListener = function (ev, listener, el) { - el.removeEventListener(ev, listener, false); +export const removeEventListener = function (ev, listener, el) { + // el.removeEventListener(ev, listener, false); + delete el.props[ev]; }; -exports.jsUndefined = void 0; +export const jsUndefined = void 0; + +export const generateUUID = function() { + function s4() { + return Math.floor((1 + Math.random()) * 0x10000).toString(16).substring(1); + } + return s4() + s4() + "-" + s4() + "-" + s4() + "-" + + s4() + "-" + s4() + s4() + s4(); +}; diff --git a/src/Halogen/VDom/Util.purs b/src/Halogen/VDom/Util.purs index 4441150..f7167dc 100644 --- a/src/Halogen/VDom/Util.purs +++ b/src/Halogen/VDom/Util.purs @@ -5,8 +5,10 @@ module Halogen.VDom.Util , unsafeFreeze , unsafeLookup , unsafeGetAny + , unsafeGetProp , unsafeHasAny , unsafeSetAny + , unsafeSetProp , unsafeDeleteAny , forE , forEachE @@ -14,20 +16,28 @@ module Halogen.VDom.Util , replicateE , diffWithIxE , diffWithKeyAndIxE + , diffPropWithKeyAndIxE + , diffArrayOfObjects , strMapWithIxE , refEq , createTextNode , setTextContent , createElement + , createMicroapp , insertChildIx + , insertChunkIx + , generateUUID , removeChild , parentNode , setAttribute , removeAttribute , addEventListener , removeEventListener + , removeProperty , JsUndefined , jsUndefined + , createChunkedElement + , diffChunkWithIxE ) where import Prelude @@ -40,9 +50,8 @@ import Foreign.Object (Object) import Foreign.Object as Object import Foreign.Object.ST (STObject) import Foreign.Object.ST as STObject -import Halogen.VDom.Types (Namespace, ElemName) +import Halogen.VDom.Types (ElemName, FnObject, Namespace) import Unsafe.Coerce (unsafeCoerce) -import Web.DOM.Document (Document) as DOM import Web.DOM.Element (Element) as DOM import Web.DOM.Node (Node) as DOM import Web.Event.EventTarget (EventListener) as DOM @@ -62,14 +71,23 @@ unsafeFreeze = unsafeCoerce unsafeLookup ∷ ∀ a. Fn.Fn2 String (Object a) a unsafeLookup = unsafeGetAny +foreign import unsafeGetProp + ∷ ∀ a b. Fn.Fn2 String a b + foreign import unsafeGetAny ∷ ∀ a b. Fn.Fn2 String a b foreign import unsafeHasAny ∷ ∀ a. Fn.Fn2 String a Boolean +foreign import unsafeSetProp + ∷ ∀ a b. EFn.EffectFn3 String a b Unit + foreign import unsafeSetAny ∷ ∀ a b. EFn.EffectFn3 String a b Unit +foreign import removeProperty + ∷ ∀ a b. EFn.EffectFn3 String a b Unit + foreign import unsafeDeleteAny ∷ ∀ a. EFn.EffectFn2 String a Unit @@ -102,51 +120,101 @@ foreign import replicateE Unit foreign import diffWithIxE - ∷ ∀ b c d - . EFn.EffectFn5 + ∷ ∀ a b c d + . EFn.EffectFn6 + FnObject (Array b) (Array c) - (EFn.EffectFn3 Int b c d) - (EFn.EffectFn2 Int b Unit) - (EFn.EffectFn2 Int c d) + (EFn.EffectFn4 a Int b c d) + (EFn.EffectFn3 a Int b Unit) + (EFn.EffectFn3 a Int c d) (Array d) foreign import diffWithKeyAndIxE - ∷ ∀ a b c d - . EFn.EffectFn6 + ∷ ∀ a b c d e + . EFn.EffectFn7 + FnObject (Object.Object a) (Array b) (b → String) - (EFn.EffectFn4 String Int a b c) + (EFn.EffectFn5 String e Int a b c) + (EFn.EffectFn3 String e a d) + (EFn.EffectFn4 String e Int b c) + (Object.Object c) + +foreign import diffPropWithKeyAndIxE + ∷ ∀ a b c d e el + . EFn.EffectFn8 + FnObject + (Object.Object a) + (Array b) + (b → String) + (EFn.EffectFn5 String Int e a b c) (EFn.EffectFn2 String a d) - (EFn.EffectFn3 String Int b c) + (EFn.EffectFn4 String Int e b c) + el (Object.Object c) +foreign import diffChunkWithIxE + ∷ ∀ a b c d e + . EFn.EffectFn6 + FnObject + (Array b) + (Array c) + (EFn.EffectFn4 a Int e c d) + (EFn.EffectFn3 a Int e Unit) + (EFn.EffectFn3 a Int c d) + (Array d) + + +foreign import diffArrayOfObjects + ∷ ∀ a b p el + . EFn.EffectFn6 + FnObject + (Object a) + el + (Array (Object p)) + (Array (Object p)) + b + Unit + -- (Array (Object p)) + foreign import strMapWithIxE - ∷ ∀ a b + ∷ ∀ a b c . EFn.EffectFn3 (Array a) (a → String) - (EFn.EffectFn3 String Int a b) + (EFn.EffectFn4 String Int a c b) (Object.Object b) foreign import refEq ∷ ∀ a b. Fn.Fn2 a b Boolean foreign import createTextNode - ∷ EFn.EffectFn2 String DOM.Document DOM.Node + ∷ EFn.EffectFn1 String DOM.Node foreign import setTextContent ∷ EFn.EffectFn2 String DOM.Node Unit foreign import createElement - ∷ EFn.EffectFn3 (Nullable Namespace) ElemName DOM.Document DOM.Element + ∷ EFn.EffectFn4 FnObject (Nullable Namespace) ElemName String DOM.Element + +foreign import createChunkedElement + ∷ EFn.EffectFn3 FnObject (Nullable Namespace) ElemName DOM.Element + +foreign import createMicroapp + ∷ EFn.EffectFn3 FnObject String String DOM.Element + +foreign import generateUUID :: Effect String foreign import insertChildIx - ∷ EFn.EffectFn3 Int DOM.Node DOM.Node Unit + ∷ forall a. EFn.EffectFn6 a String Int DOM.Node DOM.Node String Unit + +foreign import insertChunkIx + ∷ forall a. EFn.EffectFn5 a String Int ({ shimmer :: DOM.Node, layout :: DOM.Node }) DOM.Node Unit foreign import removeChild - ∷ EFn.EffectFn2 DOM.Node DOM.Node Unit + ∷ forall a. EFn.EffectFn3 a DOM.Node DOM.Node Unit foreign import parentNode ∷ EFn.EffectFn1 DOM.Node DOM.Node @@ -158,7 +226,7 @@ foreign import removeAttribute ∷ EFn.EffectFn3 (Nullable Namespace) String DOM.Element Unit foreign import addEventListener - ∷ EFn.EffectFn3 String DOM.EventListener DOM.Element Unit + ∷ EFn.EffectFn5 FnObject String String DOM.EventListener DOM.Element Unit foreign import removeEventListener ∷ EFn.EffectFn3 String DOM.EventListener DOM.Element Unit @@ -166,3 +234,4 @@ foreign import removeEventListener foreign import data JsUndefined ∷ Type foreign import jsUndefined ∷ JsUndefined + diff --git a/test/Main.js b/test/Main.js index 9a3b245..68951b5 100644 --- a/test/Main.js +++ b/test/Main.js @@ -1,16 +1,16 @@ -exports.getData = function () { +export const getData = function () { return ENV.generateData().toArray(); }; -exports.getTimeout = function () { +export const getTimeout = function () { return ENV.timeout }; -exports.pingRenderRate = function () { +export const pingRenderRate = function () { Monitoring.renderRate.ping(); }; -exports.requestAnimationFrame = function (f) { +export const requestAnimationFrame = function (f) { return function () { window.requestAnimationFrame(function () { f();