├── .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 | 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 | 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 | 12 | 13 | 34 | 35 | 43 | -------------------------------------------------------------------------------- /src/components/Connected/Connected.vue: -------------------------------------------------------------------------------- 1 | 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 | 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 | 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 | 13 | 14 | 40 | 41 | 64 | -------------------------------------------------------------------------------- /src/components/Navigation/Navigation.vue: -------------------------------------------------------------------------------- 1 | 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 | --------------------------------------------------------------------------------