├── .gitignore
├── README.md
├── babel.config.js
├── package.json
├── packages.dhall
├── public
├── favicon.ico
└── index.html
├── spago.dhall
├── src
├── App.vue
├── assets
│ └── logo.png
├── components
│ ├── Board
│ │ ├── Board.purs
│ │ └── Board.vue
│ ├── Connected
│ │ └── Connected.vue
│ ├── Counter
│ │ ├── Counter.purs
│ │ └── Counter.vue
│ ├── Item
│ │ ├── Item.purs
│ │ └── Item.vue
│ ├── List
│ │ ├── List.purs
│ │ └── List.vue
│ └── Navigation
│ │ └── Navigation.vue
├── main.js
├── router.js
├── state
│ ├── App
│ │ ├── Action.purs
│ │ └── Reducer.purs
│ ├── Board
│ │ ├── Action.purs
│ │ ├── Item.purs
│ │ ├── List.purs
│ │ ├── Reducer.purs
│ │ └── State.purs
│ ├── Counter
│ │ ├── Action.purs
│ │ ├── Reducer.purs
│ │ └── State.purs
│ ├── Entity.purs
│ └── store.js
└── utils
│ └── utils.js
├── test
└── Main.purs
├── vue.config.js
└── yarn.lock
/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | node_modules
3 | /dist
4 |
5 | # local env files
6 | .env.local
7 | .env.*.local
8 |
9 | # Log files
10 | npm-debug.log*
11 | yarn-debug.log*
12 | yarn-error.log*
13 |
14 | # Editor directories and files
15 | .idea
16 | .vscode
17 | *.suo
18 | *.ntvs*
19 | *.njsproj
20 | *.sln
21 | *.sw*
22 |
23 | .spago
24 | output
25 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # vue-pure
2 |
3 | This is an example of using Purescript to handle business logic in a Vue application. It uses `purs-loader` to incorporate Purescript compilation into the webpack build.
4 |
5 | ## Advantages
6 | * All the benefits of static-typing, purity, and controlled effects
7 | * Redux debug tools
8 | * Vue ecosystem and tooling
9 |
10 | ## Setup
11 | The actions, state, and reducers are written in Purescript. They can be found in the `state` directory.
12 | Connecting this to Redux is done in `state/store.js`:
13 | * Each reducer is applied to `uncurryReducer`, which catches the initialization case (where `state` is undefined), by calling the `rootReducer` with an `initialAction`.
14 | * `standardizeAction` intercepts incoming actions to assign them a name (following "flux-standard-action")
15 |
16 | ## Dispatching
17 | Each smart component has a corresponding `.purs` file that will handle mapping the state and dispatch to props. An example is `components/Counter/Counter.purs`, where the state and actions are exposed.
18 | Action creators are placed in their corresponding `Action.purs` file and can be invoked inside `mapDispatchToProps`.
19 |
20 | ## Why not Vuex?
21 | Unfortunately, Vuex is not a good candidate for purescript interop because it depends on mutability. Purescript has no concept of mutability and this would require mapping immutable state updates to the Vuex state. Although not impossible, it does present a performance concern as well as hindering reducer composition.
22 |
23 | # Developing
24 |
25 | ## Project setup
26 | ```
27 | yarn install
28 | spago install
29 | ```
30 |
31 | ### Compiles and hot-reloads for development
32 | ```
33 | yarn run serve
34 | ```
35 |
36 | ### Compiles and minifies for production
37 | ```
38 | yarn run build
39 | ```
40 |
41 | ### Run your tests
42 | ```
43 | yarn run test
44 | ```
45 |
46 | ### Lints and fixes files
47 | ```
48 | yarn run lint
49 | ```
50 |
51 | ### Customize configuration
52 | See [Configuration Reference](https://cli.vuejs.org/config/).
53 |
--------------------------------------------------------------------------------
/babel.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | plugins: ['@babel/plugin-transform-modules-commonjs'],
3 | presets: [
4 | '@vue/app'
5 | ]
6 | }
7 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "vue-pure",
3 | "version": "0.1.0",
4 | "private": true,
5 | "scripts": {
6 | "serve": "vue-cli-service serve",
7 | "build": "vue-cli-service build",
8 | "lint": "vue-cli-service lint"
9 | },
10 | "dependencies": {
11 | "purs-loader": "^3.2.0",
12 | "redux": "^4.0.1",
13 | "vue": "^2.6.0",
14 | "vue-router": "^3.0.2",
15 | "vuejs-redux": "https://github.com/sliptype/vuejs-redux.git#master"
16 | },
17 | "devDependencies": {
18 | "@babel/plugin-transform-modules-commonjs": "^7.2.0",
19 | "@vue/cli-plugin-babel": "^3.3.0",
20 | "@vue/cli-plugin-eslint": "^3.3.0",
21 | "@vue/cli-service": "^3.3.0",
22 | "babel-eslint": "^10.0.1",
23 | "eslint": "^5.8.0",
24 | "eslint-plugin-vue": "^5.0.0",
25 | "node-sass": "^4.11.0",
26 | "sass-loader": "^7.1.0",
27 | "vue-template-compiler": "^2.6.7"
28 | },
29 | "eslintConfig": {
30 | "root": true,
31 | "env": {
32 | "node": true
33 | },
34 | "extends": [
35 | "plugin:vue/essential",
36 | "eslint:recommended"
37 | ],
38 | "rules": {},
39 | "parserOptions": {
40 | "parser": "babel-eslint"
41 | }
42 | },
43 | "postcss": {
44 | "plugins": {
45 | "autoprefixer": {}
46 | }
47 | },
48 | "browserslist": [
49 | "> 1%",
50 | "last 2 versions",
51 | "not ie <= 8"
52 | ]
53 | }
54 |
--------------------------------------------------------------------------------
/packages.dhall:
--------------------------------------------------------------------------------
1 | {-
2 | Welcome to Spacchetti local packages!
3 |
4 | Below are instructions for how to edit this file for most use
5 | cases, so that you don't need to know Dhall to use it.
6 |
7 | ## Warning: Don't Move This Top-Level Comment!
8 |
9 | Due to how `dhall format` currently works, this comment's
10 | instructions cannot appear near corresponding sections below
11 | because `dhall format` will delete the comment. However,
12 | it will not delete a top-level comment like this one.
13 |
14 | ## Use Cases
15 |
16 | Most will want to do one or both of these options:
17 | 1. Override/Patch a package's dependency
18 | 2. Add a package not already in the default package set
19 |
20 | This file will continue to work whether you use one or both options.
21 | Instructions for each option are explained below.
22 |
23 | ### Overriding/Patching a package
24 |
25 | Purpose:
26 | - Change a package's dependency to a newer/older release than the
27 | default package set's release
28 | - Use your own modified version of some dependency that may
29 | include new API, changed API, removed API by
30 | using your custom git repo of the library rather than
31 | the package set's repo
32 |
33 | Syntax:
34 | Replace the overrides' "{=}" (an empty record) with the following idea
35 | The "//" or "⫽" means "merge these two records and
36 | when they have the same value, use the one on the right:"
37 | -------------------------------
38 | let override =
39 | { packageName =
40 | upstream.packageName ⫽ { updateEntity1 = "new value", updateEntity2 = "new value" }
41 | , packageName =
42 | upstream.packageName ⫽ { version = "v4.0.0" }
43 | , packageName =
44 | upstream.packageName // { repo = "https://www.example.com/path/to/new/repo.git" }
45 | }
46 | -------------------------------
47 |
48 | Example:
49 | -------------------------------
50 | let overrides =
51 | { halogen =
52 | upstream.halogen ⫽ { version = "master" }
53 | , halogen-vdom =
54 | upstream.halogen-vdom ⫽ { version = "v4.0.0" }
55 | }
56 | -------------------------------
57 |
58 | ### Additions
59 |
60 | Purpose:
61 | - Add packages that aren't alread included in the default package set
62 |
63 | Syntax:
64 | Replace the additions' "{=}" (an empty record) with the following idea:
65 | -------------------------------
66 | let additions =
67 | { "package-name" =
68 | mkPackage
69 | [ "dependency1"
70 | , "dependency2"
71 | ]
72 | "https://example.com/path/to/git/repo.git"
73 | "tag ('v4.0.0') or branch ('master')"
74 | , "package-name" =
75 | mkPackage
76 | [ "dependency1"
77 | , "dependency2"
78 | ]
79 | "https://example.com/path/to/git/repo.git"
80 | "tag ('v4.0.0') or branch ('master')"
81 | , etc.
82 | }
83 | -------------------------------
84 |
85 | Example:
86 | -------------------------------
87 | let additions =
88 | { benchotron =
89 | mkPackage
90 | [ "arrays"
91 | , "exists"
92 | , "profunctor"
93 | , "strings"
94 | , "quickcheck"
95 | , "lcg"
96 | , "transformers"
97 | , "foldable-traversable"
98 | , "exceptions"
99 | , "node-fs"
100 | , "node-buffer"
101 | , "node-readline"
102 | , "datetime"
103 | , "now"
104 | ]
105 | "https://github.com/hdgarrood/purescript-benchotron.git"
106 | "v7.0.0"
107 | }
108 | -------------------------------
109 | -}
110 |
111 | let mkPackage =
112 | https://raw.githubusercontent.com/spacchetti/spacchetti/20181209/src/mkPackage.dhall sha256:8e1c6636f8a089f972b21cde0cef4b33fa36a2e503ad4c77928aabf92d2d4ec9
113 |
114 | let upstream =
115 | https://raw.githubusercontent.com/spacchetti/spacchetti/20181209/src/packages.dhall sha256:c63285af67ae74feb2f6eb67521712441928d2726ea10e2040774849ca765027
116 |
117 | let overrides = {=}
118 |
119 | let additions =
120 | { redux =
121 | mkPackage
122 | [ "prelude"
123 | ]
124 | "https://github.com/sliptype/purescript-redux.git"
125 | "master"
126 | }
127 |
128 | in upstream ⫽ overrides ⫽ additions
129 |
--------------------------------------------------------------------------------
/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sliptype/vue-pure/6b6f844076dedf7026b2ac1fa62aa88f81e46b81/public/favicon.ico
--------------------------------------------------------------------------------
/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 | vue-pure
9 |
10 |
11 |
12 | We're sorry but vue-pure doesn't work properly without JavaScript enabled. Please enable it to continue.
13 |
14 |
15 |
16 |
17 |
18 |
--------------------------------------------------------------------------------
/spago.dhall:
--------------------------------------------------------------------------------
1 | {-
2 | Welcome to a Spago project!
3 | You can edit this file as you like.
4 | -}
5 | { name =
6 | "my-project"
7 | , dependencies =
8 | [ "effect"
9 | , "console"
10 | , "maybe"
11 | , "foreign-object"
12 | , "redux"
13 | , "record-extra"
14 | , "typelevel-prelude"
15 | ]
16 | , packages =
17 | ./packages.dhall
18 | }
19 |
--------------------------------------------------------------------------------
/src/App.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
18 |
19 |
36 |
--------------------------------------------------------------------------------
/src/assets/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sliptype/vue-pure/6b6f844076dedf7026b2ac1fa62aa88f81e46b81/src/assets/logo.png
--------------------------------------------------------------------------------
/src/components/Board/Board.purs:
--------------------------------------------------------------------------------
1 | module Board.Connect where
2 |
3 | import Prelude (Unit, ($))
4 | import Data.Maybe (Maybe(..))
5 |
6 | import State.Action (Actions, Dispatch, Props, actions, props)
7 | import State.Entity (EntityId)
8 | import State.Board (State)
9 | import State.Board.Action (Action(..))
10 |
11 | type BoardProps = Props
12 | { listIds :: Array EntityId }
13 |
14 | type BoardActions = Actions
15 | { addList :: String -> Unit }
16 |
17 | mapStateToProps :: forall a b. { board :: State | a } -> b -> BoardProps
18 | mapStateToProps { board } _ = props
19 | { listIds: board.list.ids }
20 |
21 | mapDispatchToProps :: forall a. Dispatch -> a -> BoardActions
22 | mapDispatchToProps dispatch _ = actions
23 | { addList: \x -> dispatch $ Just $ AddList x }
24 |
--------------------------------------------------------------------------------
/src/components/Board/Board.vue:
--------------------------------------------------------------------------------
1 |
2 |
11 |
12 |
13 |
34 |
35 |
43 |
--------------------------------------------------------------------------------
/src/components/Connected/Connected.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
9 |
10 |
15 |
16 |
17 |
18 |
19 |
20 |
74 |
--------------------------------------------------------------------------------
/src/components/Counter/Counter.purs:
--------------------------------------------------------------------------------
1 | module Counter.Connect where
2 |
3 | import Prelude (Unit, ($))
4 | import Record.Extra (mapRecord)
5 |
6 | import State.Action (Actions, Dispatch, Props, actions, bindActionCreator, props)
7 | import State.Counter (State)
8 | import State.Counter.Action (Action(..))
9 |
10 | type CounterProps = Props
11 | { value :: State }
12 |
13 | type CounterActions = Actions
14 | { increment :: Int -> Unit
15 | , decrement :: Int -> Unit
16 | }
17 |
18 | -- mapStateToProps is a function that takes 2 arguments and returns Props
19 | -- The first agrument is a state that contains the counter state
20 | -- The second argument is of any type and contains the components own props (unused here)
21 | mapStateToProps :: forall a b. { counter :: State | a } -> b -> CounterProps
22 | mapStateToProps { counter } _ = props
23 | { value: counter }
24 |
25 | -- mapDispatchToProps is a function that takes 2 arguments and returns an Actions record
26 | -- The first argument is a Dispatch function
27 | -- The second argument is of any type and contains the components own props (unused here)
28 | mapDispatchToProps :: forall a. Dispatch -> a -> CounterActions
29 | mapDispatchToProps dispatch _ = actions $
30 | mapRecord (bindActionCreator dispatch)
31 | { increment: \x -> Increment x
32 | , decrement: \x -> Decrement x
33 | }
34 |
--------------------------------------------------------------------------------
/src/components/Counter/Counter.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
Counter: {{ props.value }}
4 | Increment
5 | Decrement
6 |
7 |
8 |
9 |
15 |
16 |
36 |
--------------------------------------------------------------------------------
/src/components/Item/Item.purs:
--------------------------------------------------------------------------------
1 | module Item.Connect where
2 |
3 | import Prelude (Unit, ($))
4 | import Data.Array
5 | import Data.Maybe (Maybe(..))
6 | import Foreign.Object (lookup)
7 |
8 | import State.Action (Actions, Dispatch, Props, actions, bindActionCreator, props)
9 | import State.Entity (EntityId)
10 | import State.Board (State)
11 | import State.Board.Action (Action(..))
12 | import State.Board.Item (item)
13 |
14 | type ItemProps = Props
15 | { name :: String }
16 |
17 | type OwnProps =
18 | { id :: EntityId }
19 |
20 | type ItemActions = Actions {}
21 |
22 | mapStateToProps :: forall a. { board:: State | a } -> OwnProps -> ItemProps
23 | mapStateToProps { board } { id } =
24 | let
25 | maybeItem = lookup id board.item.byId
26 | in
27 | case maybeItem of
28 | Nothing -> props $ item ""
29 | Just i -> props $ i
30 |
31 | mapDispatchToProps :: ((Maybe Action) -> Unit) -> OwnProps -> ItemActions
32 | mapDispatchToProps dispatch { id } = actions {}
33 |
--------------------------------------------------------------------------------
/src/components/Item/Item.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
20 |
21 |
29 |
--------------------------------------------------------------------------------
/src/components/List/List.purs:
--------------------------------------------------------------------------------
1 | module List.Connect where
2 |
3 | import Prelude (Unit, ($))
4 | import Data.Maybe (Maybe(..))
5 | import Foreign.Object (lookup)
6 |
7 | import State.Action (Actions, Dispatch, Props, actions, props)
8 | import State.Entity (EntityId)
9 | import State.Board (State)
10 | import State.Board.Action (Action(..))
11 | import State.Board.List (list)
12 |
13 | type ListProps = Props
14 | { name :: String
15 | , itemIds :: Array EntityId
16 | }
17 |
18 | type OwnProps =
19 | { id :: EntityId }
20 |
21 | type ListActions = Actions
22 | { addItem :: String -> Unit }
23 |
24 | mapStateToProps :: forall a. { board :: State | a } -> OwnProps -> ListProps
25 | mapStateToProps { board } { id } =
26 | let
27 | maybeList = lookup id board.list.byId
28 | in
29 | case maybeList of
30 | Nothing -> props $ list ""
31 | Just l -> props $ l
32 |
33 | mapDispatchToProps :: Dispatch -> OwnProps -> ListActions
34 | mapDispatchToProps dispatch { id } = actions
35 | { addItem: \x -> dispatch $ Just $ AddItemToList id x }
36 |
--------------------------------------------------------------------------------
/src/components/List/List.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
10 |
11 |
12 |
13 |
14 |
40 |
41 |
64 |
--------------------------------------------------------------------------------
/src/components/Navigation/Navigation.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
11 |
12 |
13 |
14 |
19 |
20 |
52 |
--------------------------------------------------------------------------------
/src/main.js:
--------------------------------------------------------------------------------
1 | import Vue from 'vue'
2 |
3 | import App from './App.vue'
4 | import createRouter from './router.js'
5 |
6 | new Vue({
7 | render: h => h(App),
8 | router: createRouter(),
9 | }).$mount('#app')
10 |
--------------------------------------------------------------------------------
/src/router.js:
--------------------------------------------------------------------------------
1 | import Vue from 'vue'
2 | import VueRouter from 'vue-router'
3 |
4 | import Board from './components/Board/Board.vue'
5 | import Connected from './components/Connected/Connected.vue'
6 | import Counter from './components/Counter/Counter.vue'
7 |
8 | const routes = [
9 | {
10 | path: '/',
11 | component: Connected,
12 | props: {
13 | component: Counter,
14 | },
15 | },
16 | {
17 | path: '/board',
18 | component: Connected,
19 | props: {
20 | component: Board,
21 | },
22 | },
23 | ];
24 |
25 | export default function createRouter() {
26 | Vue.use(VueRouter)
27 |
28 | return new VueRouter({
29 | routes,
30 | });
31 | }
32 |
--------------------------------------------------------------------------------
/src/state/App/Action.purs:
--------------------------------------------------------------------------------
1 | module State.Action where
2 |
3 | import Prelude (Unit, (<<<))
4 | import Data.Maybe (Maybe(..))
5 | import Type.Row.Homogeneous (class Homogeneous)
6 |
7 | type Dispatch = forall a. ((Maybe a) -> Unit)
8 |
9 | -- TODO: Bind higher arity action creators
10 | bindActionCreator :: forall a b. Dispatch -> (a -> b) -> (a -> Unit)
11 | bindActionCreator dispatch actionCreator = dispatch <<< Just <<< actionCreator
12 |
13 | -- TODO: Bind action creators
14 | -- bindActionCreators :: forall a ra rb. Homogeneous ra (a -> Action) => Homogeneous rb (a -> Unit) => Dispatch -> { | ra } -> { | rb }
15 | -- bindActionCreators dispatch actionCreators = mapRecord (bindActionCreator dispatch) actionCreators
16 |
17 | -- Actions is a type synonym for a record containing a "actions" field of type a
18 | type Actions a = { actions :: a }
19 |
20 | -- Props is a type synonym for a record containing a "props" field of type a
21 | type Props a = { props :: a }
22 |
23 | actions :: forall a. { | a } -> Actions { | a }
24 | actions a = { actions: a }
25 |
26 | props :: forall a. { | a } -> Props { | a }
27 | props a = { props: a }
28 |
--------------------------------------------------------------------------------
/src/state/App/Reducer.purs:
--------------------------------------------------------------------------------
1 | module State.Reducer
2 | ( Reducer
3 | , initialAction
4 | , withInitialState
5 | ) where
6 |
7 | import Data.Maybe
8 |
9 | -- Reducer is a function that:
10 | -- Takes:
11 | -- an action of type b
12 | -- a state of type a
13 | -- Returns:
14 | -- a state of type a
15 | type Reducer a b = b -> a -> a
16 |
17 | -- withInitialState is a function that wraps a reducer
18 | -- Returns a function that takes a (Maybe Action) and returns either the new state or initial state
19 | withInitialState :: forall a b. (Reducer a b) -> a -> (Maybe b) -> a -> a
20 | withInitialState _ initialState Nothing _ = initialState
21 | withInitialState reducer _ (Just action) state = reducer action state
22 |
23 | -- Re-exporting 'Nothing' as the initial action
24 | -- this allows all sliced reducers to handle their
25 | -- initial state when a root reducer is initialized
26 | initialAction :: forall a. Maybe a
27 | initialAction = Nothing
28 |
--------------------------------------------------------------------------------
/src/state/Board/Action.purs:
--------------------------------------------------------------------------------
1 | module State.Board.Action where
2 |
3 | import State.Entity (EntityId)
4 |
5 | data Action
6 | = AddList String
7 | | AddItemToList EntityId String
8 |
--------------------------------------------------------------------------------
/src/state/Board/Item.purs:
--------------------------------------------------------------------------------
1 | module State.Board.Item where
2 |
3 | import Prelude
4 |
5 | type Item =
6 | { name :: String }
7 |
8 | item :: String -> Item
9 | item name =
10 | { name }
11 |
--------------------------------------------------------------------------------
/src/state/Board/List.purs:
--------------------------------------------------------------------------------
1 | module State.Board.List where
2 |
3 | import Prelude
4 |
5 | import Data.Array ((:))
6 |
7 | import State.Entity (EntityId)
8 |
9 | type List =
10 | { name :: String
11 | , itemIds :: Array String
12 | }
13 |
14 | list :: String -> List
15 | list name =
16 | { name
17 | , itemIds: []
18 | }
19 |
20 | addItem :: EntityId -> List -> List
21 | addItem id l = l { itemIds = id : l.itemIds }
22 |
23 |
--------------------------------------------------------------------------------
/src/state/Board/Reducer.purs:
--------------------------------------------------------------------------------
1 | module State.Board.Reducer where
2 |
3 | import Prelude
4 | import Data.Maybe (Maybe)
5 |
6 | import State.Reducer (Reducer, withInitialState)
7 | import State.Entity (EntityId, addInstance, nextId, updateInstance)
8 | import State.Board (State, initialState)
9 | import State.Board.Action (Action(..))
10 | import State.Board.List (list, addItem)
11 | import State.Board.Item (item)
12 |
13 | boardReducer :: Reducer State Action
14 | boardReducer (AddList name) s = addList name s
15 | boardReducer (AddItemToList id name) s = addItemToList id name s
16 | boardReducer _ s = s
17 | -- TODO: Determine how to avoid having to specify default
18 |
19 | reducer :: (Maybe Action) -> State -> State
20 | reducer = withInitialState boardReducer initialState
21 |
22 | addList :: String -> State -> State
23 | addList name s =
24 | let
25 | newId = nextId s.list
26 | newList = list name
27 | in
28 | s { list = s.list # addInstance newId newList }
29 |
30 | addItemToList :: EntityId -> String -> State -> State
31 | addItemToList id name s =
32 | let
33 | newId = nextId s.item
34 | newItem = item name
35 | in
36 | s
37 | { item = s.item
38 | # addInstance newId newItem
39 | , list = s.list
40 | # updateInstance (addItem newId) id
41 | }
42 |
--------------------------------------------------------------------------------
/src/state/Board/State.purs:
--------------------------------------------------------------------------------
1 | module State.Board where
2 |
3 | import Prelude
4 |
5 | import State.Entity (Entity(..), addInstance, entity)
6 | import State.Board.List (List, list)
7 | import State.Board.Item (Item)
8 |
9 | type State =
10 | { list :: Entity List
11 | , item :: Entity Item
12 | }
13 |
14 | initialState :: State
15 | initialState =
16 | { list: entity
17 | # addInstance "2" (list "Done")
18 | # addInstance "1" (list "Doing")
19 | # addInstance "0" (list "Todo")
20 | , item: entity
21 | }
22 |
23 |
--------------------------------------------------------------------------------
/src/state/Counter/Action.purs:
--------------------------------------------------------------------------------
1 | module State.Counter.Action where
2 |
3 | -- Action is a tagged union
4 | -- with a constructor for each type of action
5 | data Action
6 | = Increment Int
7 | | Decrement Int
8 |
--------------------------------------------------------------------------------
/src/state/Counter/Reducer.purs:
--------------------------------------------------------------------------------
1 | module State.Counter.Reducer where
2 |
3 | import Prelude
4 | import Data.Maybe (Maybe)
5 |
6 | import State.Reducer (withInitialState)
7 | import State.Counter (State, initialState)
8 | import State.Counter.Action (Action(..))
9 |
10 | -- counterReducer is a function taking an Action, a State, and returning a State
11 | -- The action is destructured via pattern matching
12 | -- The bottom case handles any action not explicitly handled
13 | counterReducer :: Action -> State -> State
14 | counterReducer (Increment x) state = state + x
15 | counterReducer (Decrement x) state = state - x
16 | counterReducer _ state = state
17 |
18 | -- reducer wraps counterReducer to provide an initial state
19 | reducer :: (Maybe Action) -> State -> State
20 | reducer = withInitialState counterReducer initialState
21 |
--------------------------------------------------------------------------------
/src/state/Counter/State.purs:
--------------------------------------------------------------------------------
1 | module State.Counter where
2 |
3 | -- State is a type synonym of Int
4 | type State = Int
5 |
6 | -- initialState is a function taking zero arguments
7 | -- and returning a value of type State
8 | initialState :: State
9 | initialState = 0
10 |
--------------------------------------------------------------------------------
/src/state/Entity.purs:
--------------------------------------------------------------------------------
1 | module State.Entity where
2 |
3 | import Prelude
4 |
5 | import Data.Array ((:), length)
6 | import Data.Maybe (Maybe(..))
7 | import Foreign.Object (Object(..), empty, insert, lookup)
8 |
9 | type EntityId = String
10 |
11 | type Entity a =
12 | { ids :: Array EntityId
13 | , byId :: Object a
14 | }
15 |
16 | entity :: forall a. Entity a
17 | entity =
18 | { ids: []
19 | , byId: empty
20 | }
21 |
22 | nextId :: forall a. Entity a -> EntityId
23 | nextId e = show $ length e.ids
24 |
25 | addInstance :: forall a. EntityId -> a -> Entity a -> Entity a
26 | addInstance id i e =
27 | e { ids = id : e.ids
28 | , byId = insert id i e.byId
29 | }
30 |
31 | updateInstance :: forall a. (a -> a) -> EntityId -> Entity a -> Entity a
32 | updateInstance f id e =
33 | let i = lookup id e.byId in
34 | case i of
35 | Nothing -> e
36 | Just ins ->
37 | e { byId = insert id (f ins) e.byId }
38 |
39 |
40 |
--------------------------------------------------------------------------------
/src/state/store.js:
--------------------------------------------------------------------------------
1 | import { applyMiddleware, combineReducers, compose, createStore } from 'redux'
2 | import { mapObjectValues } from '../utils/utils.js'
3 |
4 | import { reducer as counter } from './Counter/Reducer.purs'
5 | import { reducer as board } from './Board/Reducer.purs'
6 | import { initialAction } from './App/Reducer.purs'
7 |
8 | /**
9 | * Pass data into the purescript reducer
10 | * @param { Object } state
11 | * @param { Object } action
12 | */
13 | const uncurryReducer = (curriedReducer) => (state, action) => (
14 | state === undefined
15 | ? curriedReducer (initialAction) ({})
16 | : curriedReducer (action.data) (state)
17 | )
18 |
19 | /**
20 | * Redux middleware to transform purescript action into a standard action
21 | * @param { Function } next
22 | * @param { Object } action
23 | */
24 | const standardizeAction = () => next => action => (
25 | next({
26 | type: action.value0.constructor.name,
27 | data: action
28 | })
29 | )
30 |
31 | /**
32 | * Combine reducers into root reducer
33 | * TODO: Do this in purescript
34 | */
35 | const rootReducer = combineReducers(mapObjectValues({
36 | counter,
37 | board,
38 | }, uncurryReducer));
39 |
40 | const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose
41 |
42 | const store = createStore(rootReducer, composeEnhancers(applyMiddleware(
43 | standardizeAction,
44 | )))
45 |
46 | /**
47 | * A Vue mixin that exposes the store
48 | * @returns { Mixin }
49 | */
50 | export default {
51 | data() {
52 | return {
53 | store,
54 | };
55 | },
56 | }
57 |
--------------------------------------------------------------------------------
/src/utils/utils.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Applies a function to all values in an object
3 | * @param { Object } obj The source object
4 | * @param { Function } f The transformation function
5 | */
6 | export const mapObjectValues = (obj, f) =>
7 | Object
8 | .keys(obj)
9 | .reduce((result, current) => ({
10 | ...result,
11 | [current]: f(obj[current], current)
12 | }), {})
13 |
14 |
--------------------------------------------------------------------------------
/test/Main.purs:
--------------------------------------------------------------------------------
1 | module Test.Main where
2 |
3 | import Prelude
4 |
5 | import Effect (Effect)
6 | import Effect.Console (log)
7 |
8 | import Main as Main
9 |
10 | main :: Effect Unit
11 | main = do
12 | Main.main
13 | log "You should add some tests."
14 |
--------------------------------------------------------------------------------
/vue.config.js:
--------------------------------------------------------------------------------
1 | const path = require('path');
2 |
3 | // vue.config.js
4 | module.exports = {
5 | chainWebpack: config => {
6 |
7 | // Purescript Loader
8 | config.module
9 | .rule('purescript')
10 | .test(/\.purs$/)
11 | .use('purs-loader')
12 | .loader('purs-loader')
13 | .tap(options => ({
14 | src: [
15 | path.join('src', '**', '*.purs'),
16 | path.join('.spago', '**', 'src', '**', '*.purs'),
17 | ]
18 | }))
19 | }
20 | }
21 |
--------------------------------------------------------------------------------