├── .gitignore
├── .babelrc
├── examples
├── admin
│ ├── src
│ │ ├── api
│ │ │ ├── index.js
│ │ │ ├── Result.js
│ │ │ ├── api.js
│ │ │ └── users.js
│ │ ├── UpdateResult.js
│ │ ├── RequestStatus.js
│ │ ├── main.js
│ │ ├── Login.js
│ │ ├── UserForm.js
│ │ ├── App.js
│ │ └── UserList.js
│ ├── index.html
│ └── style.css
├── login
│ ├── src
│ │ ├── api
│ │ │ ├── index.js
│ │ │ ├── Result.js
│ │ │ ├── api.js
│ │ │ └── users.js
│ │ ├── UpdateResult.js
│ │ ├── RequestStatus.js
│ │ ├── main.js
│ │ └── Login.js
│ ├── index.html
│ └── style.css
├── counterList
│ ├── style.css
│ ├── src
│ │ ├── UpdateResult.js
│ │ ├── Counter.js
│ │ ├── main.js
│ │ └── CounterList.js
│ └── index.html
└── counter
│ ├── src
│ ├── UpdateResult.js
│ ├── main.js
│ └── Counter.js
│ ├── style.css
│ └── index.html
├── .eslintrc
├── README.md
├── LICENSE
└── package.json
/.gitignore:
--------------------------------------------------------------------------------
1 | *.log
2 | *.pid
3 | *.seed
4 | node_modules
5 |
--------------------------------------------------------------------------------
/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "presets": ["es2015", "stage-2", "react"]
3 | }
4 |
--------------------------------------------------------------------------------
/examples/admin/src/api/index.js:
--------------------------------------------------------------------------------
1 | import api from './api';
2 |
3 | export default api;
4 |
--------------------------------------------------------------------------------
/examples/login/src/api/index.js:
--------------------------------------------------------------------------------
1 | import api from './api';
2 |
3 | export default api;
4 |
--------------------------------------------------------------------------------
/examples/login/src/api/Result.js:
--------------------------------------------------------------------------------
1 | import Type from 'union-type';
2 |
3 | const Result = Type({
4 | Ok: [String],
5 | Error: [String]
6 | });
7 |
8 | export default Result;
9 |
--------------------------------------------------------------------------------
/examples/counterList/style.css:
--------------------------------------------------------------------------------
1 | * {
2 | box-sizing: border-box;
3 | }
4 |
5 | div, button {
6 | text-align: center;
7 | padding: 5px;
8 | font-weight: bold;
9 | }
10 |
--------------------------------------------------------------------------------
/examples/admin/src/api/Result.js:
--------------------------------------------------------------------------------
1 | import Type from 'union-type';
2 |
3 | const T = () => true;
4 | const Result = Type({
5 | Ok: [T],
6 | Error: [T]
7 | });
8 |
9 | export default Result;
10 |
--------------------------------------------------------------------------------
/.eslintrc:
--------------------------------------------------------------------------------
1 | {
2 | "env": {
3 | "browser": true,
4 | "node": true
5 | },
6 |
7 | "parser": "babel-eslint",
8 | "extends": "eslint:recommended",
9 | "plugins": [ "react" ],
10 | "rules": {
11 | "react/jsx-no-undef": 1,
12 | "react/jsx-uses-vars": 1,
13 | "react/jsx-uses-react": 1
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/examples/counter/src/UpdateResult.js:
--------------------------------------------------------------------------------
1 | import Type from 'union-type';
2 |
3 | const T = () => true;
4 |
5 | export const UpdateResult = Type({
6 | Pure: [T],
7 | WithEffects: [T, T]
8 | });
9 |
10 | export const pure = v => UpdateResult.Pure(v);
11 | export const withEffects = (v,ef) => UpdateResult.WithEffects(v, ef);
12 |
--------------------------------------------------------------------------------
/examples/login/src/UpdateResult.js:
--------------------------------------------------------------------------------
1 | import Type from 'union-type';
2 |
3 | const T = () => true;
4 |
5 | export const UpdateResult = Type({
6 | Pure: [T],
7 | WithEffects: [T, T]
8 | });
9 |
10 | export const pure = v => UpdateResult.Pure(v);
11 | export const withEffects = (v,ef) => UpdateResult.WithEffects(v, ef);
12 |
--------------------------------------------------------------------------------
/examples/counterList/src/UpdateResult.js:
--------------------------------------------------------------------------------
1 | import Type from 'union-type';
2 |
3 | const T = () => true;
4 |
5 | export const UpdateResult = Type({
6 | Pure: [T],
7 | WithEffects: [T, T]
8 | });
9 |
10 | export const pure = v => UpdateResult.Pure(v);
11 | export const withEffects = (v,ef) => UpdateResult.WithEffects(v, ef);
12 |
--------------------------------------------------------------------------------
/examples/admin/src/UpdateResult.js:
--------------------------------------------------------------------------------
1 | import Type from 'union-type';
2 |
3 | const T = () => true;
4 |
5 | export const UpdateResult = Type({
6 | Pure: [T],
7 | WithEffects: [T, T]
8 | });
9 |
10 | export const pure = v => UpdateResult.Pure(v);
11 | export const withEffects = (v, effect) => UpdateResult.WithEffects(v, effect);
12 |
--------------------------------------------------------------------------------
/examples/counter/style.css:
--------------------------------------------------------------------------------
1 | * {
2 | box-sizing: border-box;
3 | }
4 |
5 | body {
6 | display: flex;
7 | align-items: center;
8 | justify-content: center;
9 | font-size: 14px;
10 | height: 100vh;
11 | }
12 |
13 | div, button {
14 | width: 120px;
15 | text-align: center;
16 | padding: 5px;
17 | font-weight: bold;
18 | }
19 |
--------------------------------------------------------------------------------
/examples/login/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | Login
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
--------------------------------------------------------------------------------
/examples/admin/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | User admin
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
--------------------------------------------------------------------------------
/examples/counter/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | Counter
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
--------------------------------------------------------------------------------
/examples/counterList/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | Counter
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
--------------------------------------------------------------------------------
/examples/admin/src/api/api.js:
--------------------------------------------------------------------------------
1 |
2 | import * as users from './users';
3 | import Result from './Result';
4 |
5 | const makeApiCall = method =>
6 | (...args) => {
7 | return new Promise((res, rej) => {
8 | setTimeout(() => {
9 | Result.case({
10 | Ok: res,
11 | Error: rej
12 | }, method(...args));
13 | }, 200);
14 | });
15 | }
16 |
17 | const obj = {};
18 | for (var key in users) {
19 | obj[key] = makeApiCall(users[key]);
20 | }
21 |
22 | export default obj;
23 |
--------------------------------------------------------------------------------
/examples/login/src/api/api.js:
--------------------------------------------------------------------------------
1 |
2 | import * as users from './users';
3 | import Result from './Result';
4 |
5 | const makeApiCall = method =>
6 | (...args) => {
7 | return new Promise((res, rej) => {
8 | setTimeout(() => {
9 | Result.case({
10 | Ok: res,
11 | Error: rej
12 | }, method(...args));
13 | }, 200);
14 | });
15 | }
16 |
17 | const obj = {};
18 | for (var key in users) {
19 | obj[key] = makeApiCall(users[key]);
20 | }
21 |
22 | export default obj;
23 |
--------------------------------------------------------------------------------
/examples/admin/src/RequestStatus.js:
--------------------------------------------------------------------------------
1 | import Type from 'union-type';
2 |
3 | const T = () => true;
4 |
5 | const Status = Type({
6 | Empty : [],
7 | Pending : [],
8 | Success : [T],
9 | Error : [T]
10 | });
11 |
12 | Status.isPending = Status.case({
13 | Pending: () => true,
14 | _: () => false
15 | });
16 |
17 | Status.isSuccess = Status.case({
18 | Success: () => true,
19 | _: () => false
20 | });
21 |
22 | Status.isError = Status.case({
23 | Error: () => true,
24 | _: () => false
25 | });
26 |
27 | export default Status;
28 |
--------------------------------------------------------------------------------
/examples/login/src/RequestStatus.js:
--------------------------------------------------------------------------------
1 | import Type from 'union-type';
2 |
3 | const T = () => true;
4 |
5 | const Status = Type({
6 | Empty : [],
7 | Pending : [],
8 | Success : [T],
9 | Error : [T]
10 | });
11 |
12 | Status.isPending = Status.case({
13 | Pending: () => true,
14 | _: () => false
15 | });
16 |
17 | Status.isSuccess = Status.case({
18 | Success: () => true,
19 | _: () => false
20 | });
21 |
22 | Status.isError = Status.case({
23 | Error: () => true,
24 | _: () => false
25 | });
26 |
27 | export default Status;
28 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | Elm architecture examples with JavaScript/JSX using
2 |
3 | - Snabbdom : A small and modular Virtual DOM library
4 | - Snabbdom-jsx: a tiny library for writing Snabbdom Virtual DOM using JSX syntax
5 | - union-type: to represent Component Actions
6 |
7 | ## Setup
8 |
9 | - Clone the repository then run `npm install`.
10 | - `npm run {example}` to run the specified example
11 |
12 | ## Examples
13 |
14 | The repository contains some examples
15 |
16 | - counter: a simple increment/decrement counter
17 | - counterList: List of the precedent counter example
18 | - login: Demonstrates Asynchronous Actions in a single component
19 | - admin: Demonstrates Asynchronous Actions in nested component hierarchies (need fix)
20 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2015 Yassine Elouafi
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
23 |
--------------------------------------------------------------------------------
/examples/counterList/src/Counter.js:
--------------------------------------------------------------------------------
1 | /** @jsx html */
2 |
3 | import { html } from 'snabbdom-jsx';
4 | import Type from 'union-type';
5 | import { pure, withEffects } from './UpdateResult';
6 |
7 | const Action = Type({
8 | Increment : [],
9 | Decrement : [],
10 | IncrementLater : []
11 | })
12 |
13 | const Effect = Type({
14 | IncrementAsync : []
15 | });
16 |
17 | const view = ({state, dispatch}) =>
18 |
19 |
20 |
{state}
21 |
22 |
23 |
;
24 |
25 | const init = () => pure(0);
26 |
27 | const update = (state, action) => Action.case({
28 | Increment : () => pure(state + 1),
29 | Decrement : () => pure(state - 1),
30 | IncrementLater : () => withEffects(state, Effect.IncrementAsync())
31 | }, action);
32 |
33 | const execute = (state, effect, dispatch) => Effect.case({
34 | IncrementAsync: () => setTimeout(() => dispatch(Action.Increment()), 1000)
35 | }, effect);
36 |
37 | export default { init, update, execute, view, Action, Effect };
38 |
--------------------------------------------------------------------------------
/examples/counter/src/main.js:
--------------------------------------------------------------------------------
1 | /** @jsx html */
2 |
3 | import { html } from 'snabbdom-jsx';
4 | import snabbdom from 'snabbdom';
5 | import { UpdateResult } from './updateResult';
6 | import App from './Counter';
7 |
8 | const patch = snabbdom.init([
9 | require('snabbdom/modules/class'),
10 | require('snabbdom/modules/props'),
11 | require('snabbdom/modules/style'),
12 | require('snabbdom/modules/eventListeners')
13 | ]);
14 |
15 |
16 | var state,
17 | vnode = document.getElementById('placeholder');
18 |
19 | function updateUI() {
20 | const newVnode = ;
21 | vnode = patch(vnode, newVnode);
22 | }
23 |
24 | function updateStatePure(newState) {
25 | state = newState;
26 | updateUI();
27 | }
28 |
29 | function updateStateWithEffect(newState, effect) {
30 | updateStatePure(newState);
31 | App.execute(state, effect, dispatch);
32 | }
33 |
34 | function handleUpdateResult(updateResult) {
35 | UpdateResult.case({
36 | Pure : updateStatePure,
37 | WithEffects : updateStateWithEffect
38 | }, updateResult);
39 | }
40 |
41 | export function dispatch(action) {
42 | const updateResult = App.update(state, action);
43 | handleUpdateResult(updateResult);
44 | }
45 |
46 | handleUpdateResult(App.init());
47 |
--------------------------------------------------------------------------------
/examples/login/src/main.js:
--------------------------------------------------------------------------------
1 | /** @jsx html */
2 |
3 | import { html } from 'snabbdom-jsx';
4 | import snabbdom from 'snabbdom';
5 | import { UpdateResult } from './UpdateResult';
6 | import App from './Login';
7 |
8 | const patch = snabbdom.init([
9 | require('snabbdom/modules/class'),
10 | require('snabbdom/modules/props'),
11 | require('snabbdom/modules/style'),
12 | require('snabbdom/modules/eventListeners')
13 | ]);
14 |
15 |
16 | var state,
17 | vnode = document.getElementById('placeholder');
18 |
19 | function updateUI() {
20 | const newVnode = ;
21 | vnode = patch(vnode, newVnode);
22 | }
23 |
24 | function updateStatePure(newState) {
25 | state = newState;
26 | updateUI();
27 | }
28 |
29 | function updateStateWithEffect(newState, effect) {
30 | updateStatePure(newState);
31 | App.execute(state, effect, dispatch);
32 | }
33 |
34 | function handleUpdateResult(updateResult) {
35 | UpdateResult.case({
36 | Pure : updateStatePure,
37 | WithEffects : updateStateWithEffect
38 | }, updateResult);
39 | }
40 |
41 | export function dispatch(action) {
42 | const updateResult = App.update(state, action);
43 | handleUpdateResult(updateResult);
44 | }
45 |
46 | handleUpdateResult(App.init());
47 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "elm-arch-with-snabbdom",
3 | "version": "1.0.0",
4 | "description": "Elm architecture examples with JavaScript/JSX using Snabbdom",
5 | "scripts": {
6 | "counter": "budo examples/counter/src/main.js:build.js --dir examples/counter --verbose --live -- -t babelify",
7 | "counterList": "budo examples/counterList/src/main.js:build.js --dir examples/counterList --verbose --live -- -t babelify",
8 | "login": "budo examples/login/src/main.js:build.js --dir examples/login --verbose --live -- -t babelify",
9 | "admin": "budo examples/admin/src/main.js:build.js --dir examples/admin --verbose --live -- -t babelify"
10 | },
11 | "keywords": [
12 | "elm",
13 | "architecture",
14 | "snabbdom",
15 | "JSX",
16 | "virtual",
17 | "dom"
18 | ],
19 | "author": "Yassine Elouafi ",
20 | "license": "MIT",
21 | "dependencies": {
22 | "snabbdom": "^0.2.8",
23 | "snabbdom-jsx": "^0.3.0",
24 | "union-type": "^0.1.6"
25 | },
26 | "devDependencies": {
27 | "babel-preset-es2015": "^6.3.13",
28 | "babel-preset-react": "^6.3.13",
29 | "babel-preset-stage-2": "^6.3.13",
30 | "babelify": "^7.2.0",
31 | "browserify": "^13.0.0",
32 | "budo": "^7.1.0"
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/examples/admin/src/main.js:
--------------------------------------------------------------------------------
1 | /** @jsx html */
2 |
3 | import { html } from 'snabbdom-jsx';
4 | import snabbdom from 'snabbdom';
5 | import { UpdateResult } from './UpdateResult';
6 | import App from './App';
7 |
8 | const patch = snabbdom.init([
9 | require('snabbdom/modules/class'),
10 | require('snabbdom/modules/props'),
11 | require('snabbdom/modules/style'),
12 | require('snabbdom/modules/eventListeners')
13 | ]);
14 |
15 |
16 | var state,
17 | vnode = document.getElementById('placeholder');
18 |
19 | function updateUI() {
20 | const newVnode = ;
21 | vnode = patch(vnode, newVnode);
22 | }
23 |
24 | function updateStatePure(newState) {
25 | //console.log(newState)
26 | state = newState;
27 | updateUI();
28 | }
29 |
30 | function updateStateWithEffect(newState, effect) {
31 | updateStatePure(newState);
32 | App.execute(state, effect, dispatch);
33 | }
34 |
35 | function handleUpdateResult(updateResult) {
36 | UpdateResult.case({
37 | Pure : updateStatePure,
38 | WithEffects : updateStateWithEffect
39 | }, updateResult);
40 | }
41 |
42 | export function dispatch(action) {
43 | const updateResult = App.update(state, action);
44 | handleUpdateResult(updateResult);
45 | }
46 |
47 | handleUpdateResult(App.init());
48 |
--------------------------------------------------------------------------------
/examples/counter/src/Counter.js:
--------------------------------------------------------------------------------
1 | /** @jsx html */
2 |
3 | import { html } from 'snabbdom-jsx';
4 | import Type from 'union-type';
5 | import { pure, withEffects } from './updateResult';
6 |
7 | const Action = Type({
8 | Increment : [],
9 | Decrement : [],
10 | IncrementLater : []
11 | })
12 |
13 | const Effect = Type({
14 | IncrementAsync : []
15 | });
16 |
17 | const view = ({state, dispatch}) =>
18 |
19 |
20 |
{state}
21 |
22 |
23 |
;
24 |
25 | const init = () => pure(0);
26 |
27 | const update = (state, action) => Action.case({
28 | Increment : () => pure(state + 1),
29 | Decrement : () => pure(state - 1),
30 | IncrementLater : () => withEffects(state, Effect.IncrementAsync())
31 | }, action);
32 |
33 | function incrementAsync(dispatch) {
34 | setTimeout(() => dispatch(Action.Increment()), 1000)
35 | }
36 |
37 | const execute = (state, effect, dispatch) => Effect.case({
38 | IncrementAsync: () => incrementAsync(dispatch)
39 | }, effect);
40 |
41 | export default { view, init, update, execute, Action, Effect };
42 |
--------------------------------------------------------------------------------
/examples/counterList/src/main.js:
--------------------------------------------------------------------------------
1 | /** @jsx html */
2 |
3 | import { html } from 'snabbdom-jsx';
4 | import snabbdom from 'snabbdom';
5 | import { UpdateResult } from './UpdateResult';
6 | import App from './CounterList';
7 |
8 | const patch = snabbdom.init([
9 | require('snabbdom/modules/class'),
10 | require('snabbdom/modules/props'),
11 | require('snabbdom/modules/style'),
12 | require('snabbdom/modules/eventListeners')
13 | ]);
14 |
15 |
16 | var state,
17 | vnode = document.getElementById('placeholder');
18 |
19 | function updateUI() {
20 | const newVnode = ;
21 | vnode = patch(vnode, newVnode);
22 | }
23 |
24 | function updateStatePure(newState) {
25 | state = newState;
26 | updateUI();
27 | }
28 |
29 | function updateStateWithEffect(newState, effect) {
30 | updateStatePure(newState);
31 | App.execute(state, effect, dispatch);
32 | }
33 |
34 | function handleUpdateResult(updateResult) {
35 | UpdateResult.case({
36 | Pure : updateStatePure,
37 | WithEffects : updateStateWithEffect
38 | }, updateResult);
39 | }
40 |
41 | function dispatch(action) {
42 | const updateResult = App.update(state, action);
43 | handleUpdateResult(updateResult);
44 | }
45 |
46 | function mapDispatcher(context) {
47 | const newDisp = action => this(context(action));
48 | newDisp.map = mapDispatcher;
49 | return newDisp;
50 | }
51 |
52 | dispatch.map = mapDispatcher;
53 |
54 | handleUpdateResult(App.init());
55 |
--------------------------------------------------------------------------------
/examples/login/style.css:
--------------------------------------------------------------------------------
1 | * {
2 | box-sizing: border-box;
3 | }
4 |
5 | body {
6 | display: flex;
7 | align-items: center;
8 | justify-content: center;
9 | height: 100vh;
10 | font-size: 14px;
11 | }
12 |
13 | .login {
14 | width: 300px;
15 | padding: 1em;
16 | background: #fff;
17 | border-radius: 0.5em;
18 | border: 1px solid #46b8da;
19 | border-top-width: 0.5em;
20 | }
21 |
22 | .login h1 {
23 | text-align: center;
24 | font-size: 1.5em;
25 | text-transform: uppercase;
26 | margin-top: 0;
27 | margin-bottom: 1em;
28 | }
29 |
30 | .login input {
31 | width: 100%;
32 | height: 3em;
33 | border-radius: 5px;
34 | border: 1px solid #ccc;
35 | margin-bottom: 1em;
36 | padding: 0 0.5em;
37 | outline: none;
38 | }
39 |
40 | .login input:active, .login input:focus {
41 | border-color: #66afe9;
42 | outline: 0;
43 | box-shadow: inset 0 1px 1px rgba(0,0,0,.075),0 0 8px rgba(102,175,233,.6);
44 | }
45 |
46 | .login button {
47 | width: 100%;
48 | height: 40px;
49 | border-radius: 5px;
50 | border: 1px solid #46b8da;
51 | color: #ffffff;
52 | background-color: #3097d1;
53 | border-color: #2a88bd;
54 | font-weight: bold;
55 | text-transform: uppercase;
56 | cursor: pointer;
57 | }
58 |
59 | .login button:hover {
60 | color: #ffffff;
61 | background-color: #2579a9;
62 | border-color: #1f648b;
63 | }
64 |
65 | .error {
66 | margin-bottom: 1em;
67 | color: #c9302c;
68 | }
69 |
70 | .success {
71 | margin-bottom: 1em;
72 | color: #449d44;
73 | }
74 |
--------------------------------------------------------------------------------
/examples/login/src/api/users.js:
--------------------------------------------------------------------------------
1 | import Result from './Result';
2 |
3 | const users = [
4 | { name: 'admin', password: 'admin', admin: true},
5 | { name: 'guest', password: 'guest'}
6 | ];
7 |
8 | function isDuplicate(name, exceptIdx) {
9 | return users.some( (user, idx) => user.name === name && idx !== exceptIdx );
10 | }
11 |
12 | export function getUsers() {
13 | return users.slice(1);
14 | }
15 |
16 | export function login(name, password) {
17 | const user = users.find(user => user.name === name && user.password === password);
18 | return user ?
19 | Result.Ok('')
20 | : Result.Error('Invalid username/password');
21 | }
22 |
23 | export function addUser(name, password, admin) {
24 | if(!isDuplicate(name)) {
25 | users.push({name, password, admin});
26 | return Result.Ok(`${users.length}`);
27 | } else {
28 | return Result.Error('Duplicate user');
29 | }
30 | }
31 |
32 | export function updateUser(user) {
33 | const idx = users.findIndex( u => u.name === user.name );
34 | if(idx < 0)
35 | return Result.Error('Invalid user id');
36 | else if(isDuplicate(user.name, idx))
37 | return Result.Error('Duplicate user name');
38 | else {
39 | users[idx] = user;
40 | return Result.Ok('');
41 | }
42 | }
43 |
44 | export function removeUser(user) {
45 | const idx = users.findIndex( u => u.name === user.name );
46 | if(idx < 0)
47 | return Result.Error('Unkown user!');
48 |
49 | if(idx === 0)
50 | return Result.Error('Can not remove this one!');
51 |
52 | else {
53 | users.splice(idx, 1);
54 | return Result.Ok();
55 | }
56 | }
57 |
--------------------------------------------------------------------------------
/examples/admin/src/api/users.js:
--------------------------------------------------------------------------------
1 | import Result from './Result';
2 |
3 | const users = [
4 | { name: 'admin', password: 'admin', admin: true},
5 | { name: 'guest', password: 'guest'}
6 | ];
7 |
8 | function isDuplicate(name, exceptIdx) {
9 | return users.some( (user, idx) => user.name === name && idx !== exceptIdx );
10 | }
11 |
12 | export function getUsers() {
13 | return Result.Ok(users.slice(1));
14 | }
15 |
16 | export function login(name, password) {
17 | const user = users.find(user => user.name === name && user.password === password);
18 | return user ?
19 | Result.Ok('')
20 | : Result.Error('Invalid username/password');
21 | }
22 |
23 | export function addUser(name, password, admin) {
24 | if(!isDuplicate(name)) {
25 | users.push({name, password, admin});
26 | return Result.Ok(`${users.length}`);
27 | } else {
28 | return Result.Error('Duplicate user');
29 | }
30 | }
31 |
32 | export function updateUser(user) {
33 | const idx = users.findIndex( u => u.name === user.name );
34 | if(idx < 0)
35 | return Result.Error('Invalid user id');
36 | else if(isDuplicate(user.name, idx))
37 | return Result.Error('Duplicate user name');
38 | else {
39 | users[idx] = user;
40 | return Result.Ok('');
41 | }
42 | }
43 |
44 | export function removeUser(user) {
45 | const idx = users.findIndex( u => u.name === user.name );
46 | if(idx < 0)
47 | return Result.Error('Unkown user!');
48 |
49 | if(idx === 0)
50 | return Result.Error('Can not remove this one!');
51 |
52 | else {
53 | users.splice(idx, 1);
54 | return Result.Ok();
55 | }
56 | }
57 |
--------------------------------------------------------------------------------
/examples/admin/style.css:
--------------------------------------------------------------------------------
1 | * {
2 | box-sizing: border-box;
3 | }
4 |
5 | body {
6 | font-size: 14px;
7 | }
8 |
9 | input {
10 | height: 3em;
11 | border-radius: 0.2em;
12 | border: 1px solid #ccc;
13 | padding: 0 0.5em;
14 | outline: none;
15 | }
16 |
17 | input:active, input:focus {
18 | border-color: #66afe9;
19 | outline: 0;
20 | box-shadow: inset 0 1px 1px rgba(0,0,0,.075),0 0 8px rgba(102,175,233,.6);
21 | }
22 |
23 | button {
24 | height: 3em;
25 | border-radius: 0.2em;
26 | border: 1px solid #46b8da;
27 | color: #2a88bd;
28 | background-color: transparent;
29 | border-color: #2a88bd;
30 | outline: none;
31 | cursor: pointer;
32 | }
33 |
34 | button:hover {
35 | color: #ffffff;
36 | background-color: #2a88bd;
37 | box-shadow: none;
38 | }
39 |
40 | h1 {
41 | font-size: 1.5em;
42 | text-transform: uppercase;
43 | }
44 |
45 | .status {
46 | font-size: 1.5em;
47 | font-weight: bold;
48 | }
49 |
50 | .error {
51 | color: #c9302c;
52 | }
53 |
54 | .success {
55 | color: #449d44;
56 | }
57 |
58 | .login {
59 | display: flex;
60 | align-items: center;
61 | justify-content: center;
62 | height: 100vh;
63 | }
64 |
65 | .login form {
66 | width: 300px;
67 | padding: 1em;
68 | background: #fff;
69 | border-radius: 0.5em;
70 | border: 1px solid #46b8da;
71 | border-top-width: 0.5em;
72 | }
73 |
74 | .login h1 {
75 | text-align: center;
76 | margin-top: 0;
77 | margin-bottom: 1em;
78 | }
79 |
80 | .login input {
81 | width: 100%;
82 | margin-bottom: 1em;
83 | }
84 |
85 | .login button {
86 | width: 100%;
87 | font-weight: bold;
88 | text-transform: uppercase;
89 | }
90 |
91 | .login .status {
92 | margin-bottom: 1em;
93 | }
94 |
95 | .admin {
96 | margin: 2em;
97 | }
98 |
99 | .admin .item {
100 | padding: 0.5em 1em;
101 | }
102 |
103 | .admin input {
104 | width: 15em;
105 | margin-right: 1em;
106 | }
107 |
108 | .admin button {
109 | width: 12em;
110 | margin-right: 1em;
111 | }
112 |
--------------------------------------------------------------------------------
/examples/counterList/src/CounterList.js:
--------------------------------------------------------------------------------
1 | /** @jsx html */
2 |
3 | import { html } from 'snabbdom-jsx';
4 | import Counter from './Counter';
5 | import Type from 'union-type';
6 | import { UpdateResult, pure, withEffects } from './UpdateResult';
7 |
8 | const Action = Type({
9 | Add : [],
10 | Update : [Number, Counter.Action]
11 | });
12 |
13 | const Effect = Type({
14 | Counter : [Number, Counter.Effect]
15 | });
16 |
17 |
18 | const view = ({state, dispatch}) =>
19 |
20 |
21 |
22 |
{
23 | state.map((item, idx) =>
24 | )
25 | }
26 |
;
27 |
28 | const init = () => pure([]);
29 |
30 | const addPure = (state, v) => [...state, v];
31 | const updatePure = (state, v, idx) => state.map((item,i) => i === idx ? v : item);
32 |
33 | function addCounter(state) {
34 | return UpdateResult.case({
35 | Pure : v => pure(addPure(state, v)),
36 | WithEffects : (v, eff) => withEffects(
37 | addPure(state, v),
38 | Effect.Counter(state.length, eff))
39 | }, Counter.init());
40 | }
41 |
42 | function updateCounter(state, idx, action) {
43 | return UpdateResult.case({
44 | Pure : v => pure(updatePure(state, v, idx)),
45 | WithEffects : (v, eff) => withEffects(
46 | updatePure(state, v, idx),
47 | Effect.Counter(idx, eff))
48 | }, Counter.update(state[idx],action));
49 | }
50 |
51 | const update = (state, action) => Action.case({
52 | Add : () => addCounter(state),
53 | Update : (idx, action) => updateCounter(state, idx, action)
54 | }, action);
55 |
56 | const execute = (state, effect, dispatch) => Effect.case({
57 | Counter: (idx, counterEffect) =>
58 | Counter.execute(
59 | state[idx],
60 | counterEffect, dispatch.map(Action.Update(idx)) )
61 | }, effect);
62 |
63 | export default { view, init, update, execute, Action, Effect }
64 |
--------------------------------------------------------------------------------
/examples/admin/src/Login.js:
--------------------------------------------------------------------------------
1 | /** @jsx html */
2 |
3 | import { html } from 'snabbdom-jsx';
4 | import Type from 'union-type';
5 | import Status from './RequestStatus';
6 | import { pure, withEffects } from './UpdateResult';
7 | import api from './api';
8 |
9 | /*
10 | state: {
11 | name : String, current username input
12 | password : String, current paswword input
13 | status : Status, status of the last request
14 | }
15 | */
16 |
17 | const Action = Type({
18 | Name : [String],
19 | Password : [String],
20 | Login : [],
21 | LoginError : [String]
22 | });
23 |
24 | const Effect = Type({
25 | Login : []
26 | });
27 |
28 | function onInput(dispatch, action) {
29 | return e => dispatch(action(e.target.value));
30 | }
31 |
32 | function onSubmit(dispatch) {
33 | return e => {
34 | e.preventDefault();
35 | dispatch(Action.Login());
36 | return false;
37 | }
38 | }
39 |
40 | const view = ({
41 | state: {name, password, status},
42 | dispatch
43 | }) =>
44 |
68 |
69 | const statusMsg = Status.case({
70 | Empty : () => '',
71 | Pending : () => 'Logging in ...',
72 | Success : () => 'Login Successfull',
73 | Error : error => error
74 | });
75 |
76 |
77 | function init() {
78 | return pure({ name: '', password: '', status: Status.Empty() });
79 | }
80 |
81 |
82 | function login(state, dispatch) {
83 | api.login(state.name, state.password)
84 | .then(() => window.location.hash = '/admin')
85 | .catch( err => dispatch(Action.LoginError(err)));
86 | }
87 |
88 | function update(state, action) {
89 | return Action.case({
90 | // Input actions
91 | Name : name => pure({ ...state, name }),
92 | Password : password => pure({ ...state, password }),
93 |
94 | // Request actions
95 | Login : () => withEffects(
96 | { ...state, status: Status.Pending()},
97 | Effect.Login()
98 | ),
99 | LoginError : (error) => pure({ ...state, status: Status.Error(error) })
100 | }, action);
101 | }
102 |
103 | function execute(state, effect, dispatch) {
104 | Effect.case({
105 | Login : () => login(state, dispatch)
106 | }, effect)
107 | }
108 |
109 | export default { view, init, update, Action, execute, Effect };
110 |
--------------------------------------------------------------------------------
/examples/login/src/Login.js:
--------------------------------------------------------------------------------
1 | /** @jsx html */
2 |
3 | import { html } from 'snabbdom-jsx';
4 | import Type from 'union-type';
5 | import Status from './RequestStatus';
6 | import { pure, withEffects } from './UpdateResult';
7 | import api from './api';
8 |
9 | /*
10 | state: {
11 | name : String, current username input
12 | password : String, current paswword input
13 | status : Status, status of the last request
14 | }
15 | */
16 |
17 | const Action = Type({
18 | Name : [String],
19 | Password : [String],
20 | Login : [],
21 | LoginSuccess: [String],
22 | LoginError : [String]
23 | });
24 |
25 | const Effect = Type({
26 | Login : []
27 | });
28 |
29 | function onInput(dispatch, action) {
30 | return e => dispatch(action(e.target.value));
31 | }
32 |
33 | function onSubmit(dispatch) {
34 | return e => {
35 | e.preventDefault();
36 | dispatch(Action.Login());
37 | return false;
38 | }
39 | }
40 |
41 | const view = ({
42 | state: {name, password, status},
43 | dispatch
44 | }) =>
45 |
46 | ;
68 |
69 | const statusMsg = Status.case({
70 | Empty : () => '',
71 | Pending : () => 'Logging in ...',
72 | Success : () => 'Login Successfull',
73 | Error : error => error
74 | });
75 |
76 |
77 | function init() {
78 | return pure({ name: '', password: '', status: Status.Empty() });
79 | }
80 |
81 |
82 | function login(state, dispatch) {
83 | api.login(state.name, state.password)
84 | .then(Action.LoginSuccess, Action.LoginError)
85 | .then(dispatch);
86 | }
87 |
88 | function update(state, action) {
89 | return Action.case({
90 | // Input actions
91 | Name : name => pure({ ...state, name }),
92 | Password : password => pure({ ...state, password }),
93 |
94 | // Request actions
95 | Login : () => withEffects(
96 | { ...state, status: Status.Pending()},
97 | Effect.Login()
98 | ),
99 | LoginSuccess : () => pure({ ...state, status: Status.Success('') }),
100 | LoginError : (error) => pure({ ...state, status: Status.Error(error) })
101 | }, action);
102 | }
103 |
104 | function execute(state, effect, dispatch) {
105 | Effect.case({
106 | Login : () => login(state, dispatch)
107 | }, effect)
108 | }
109 |
110 | export default { view, init, update, Action, execute, Effect };
111 |
--------------------------------------------------------------------------------
/examples/admin/src/UserForm.js:
--------------------------------------------------------------------------------
1 | /** @jsx html */
2 |
3 | import { html } from 'snabbdom-jsx';
4 | import Type from 'union-type';
5 | import Status from './RequestStatus';
6 | import { pure, withEffects } from './UpdateResult';
7 | import api from './api';
8 | /*
9 | state: {
10 | id : Number, stored id
11 | name : String, current username input
12 | password : String, current paswword input
13 | status : Status, status of the last request
14 | }
15 | */
16 |
17 | const Action = Type({
18 | Name : [String],
19 | Password : [String],
20 | Save : [],
21 | SaveSuccess : [Object], // {id}
22 | SaveError : [Object] // {error}
23 | });
24 |
25 | const Effect = Type({
26 | Save: []
27 | });
28 |
29 | function onInput(dispatch, action) {
30 | return e => dispatch(action(e.target.value));
31 | }
32 |
33 | function onSubmit(dispatch) {
34 | return e => {
35 | e.preventDefault();
36 | dispatch(Action.Save());
37 | return false;
38 | }
39 | }
40 |
41 |
42 | const view = ({
43 | state: { id, name, password, status },
44 | dispatch
45 | }) =>
46 |
47 | ;
72 |
73 | const statusMsg = Status.case({
74 | Empty : () => '',
75 | Pending : () => 'Saving user...',
76 | Success : id => `User ${id} saved with success`,
77 | Error : error => `Error! ${error}`
78 | });
79 |
80 |
81 | function init(user={name: '', password: ''}) {
82 | return pure({ ...user, status: Status.Empty() });
83 | }
84 |
85 |
86 | function save(state, dispatch) {
87 | const save = state.id ? api.addUser : api.updateUser;
88 | const data = {id: state.id, name: state.name, password: state.password};
89 | return save(data)
90 | .then(Action.SaveSuccess, Action.SaveError)
91 | .then(dispatch);
92 |
93 | }
94 |
95 | function update(state, action) {
96 | return Action.case({
97 | // Input Actions
98 | Name : name => pure({ ...state, name }),
99 | Password : password => pure({ ...state, password }),
100 | // Save Request Actions
101 | Save : () => withEffects(
102 | { ...state, status: Status.Pending()},
103 | Effect.Save()
104 | ),
105 | SaveSuccess : id => pure({ ...state, id, status: Status.Success(id) }),
106 | SaveError : error => pure({ ...state, status: Status.Error(error) })
107 | }, action);
108 | }
109 |
110 | function execute(state, effect, dispatch) {
111 | Effect.case({
112 | Save: () => save(state, dispatch)
113 | }, effect)
114 | }
115 |
116 | export default { view, init, update, Action, execute, Effect };
117 |
--------------------------------------------------------------------------------
/examples/admin/src/App.js:
--------------------------------------------------------------------------------
1 | /** @jsx html */
2 |
3 | import { html } from 'snabbdom-jsx';
4 | import Type from 'union-type';
5 | import { UpdateResult, pure, withEffects } from './UpdateResult';
6 | import Login from './Login';
7 | import UserList from './UserList';
8 |
9 | /*
10 | state: {
11 | { route1: component1, ...}
12 | currentRoute: String,
13 | currentState: Object
14 | }
15 | */
16 |
17 | const DEFAULT_ROUTE = '/login';
18 | const routes = {
19 | '/login': Login,
20 | '/admin': UserList
21 | };
22 | const T = () => true;
23 |
24 | const Action = Type({
25 | Navigate: [String],
26 | Update : [T]
27 | });
28 |
29 | const Effect = Type({
30 | Init: [T],
31 | Child: [T]
32 | });
33 |
34 | function componentDispatcher(dispatch) {
35 | return action => dispatch(Action.Update(action));
36 | }
37 |
38 | const view = ({
39 | state: {routes, currentRoute, currentState},
40 | dispatch
41 | }) =>
42 |
43 | {
44 | routes[currentRoute].view({
45 | state: currentState,
46 | dispatch: componentDispatcher(dispatch)
47 | })
48 | }
49 |
50 | function initPure(currentState) {
51 | return {
52 | routes,
53 | currentRoute: DEFAULT_ROUTE,
54 | currentState
55 | };
56 | }
57 |
58 | function init() {
59 | const component = routes[DEFAULT_ROUTE],
60 | result = component.init();
61 |
62 | return UpdateResult.case({
63 | Pure: currentState =>
64 | withEffects(initPure(currentState), Effect.Init(null)),
65 | WithEffects: (currentState, eff) =>
66 | withEffects(initPure(currentState), Effect.Init(eff))
67 | }, result);
68 | }
69 |
70 | function navigatePure(state, currentRoute, currentState) {
71 | return {...state,
72 | currentRoute,
73 | currentState
74 | };
75 | }
76 |
77 | function navigate(state, currentRoute) {
78 | const component = state.routes[currentRoute] || DEFAULT_ROUTE,
79 | result = component.init();
80 |
81 | return UpdateResult.case({
82 | Pure: currentState =>
83 | pure(navigatePure(state, currentRoute, currentState)),
84 | WithEffects: (currentState, eff) =>
85 | withEffects(
86 | navigatePure(state, currentRoute, currentState),
87 | Effect.Child(eff))
88 | }, result);
89 | }
90 |
91 | function updateComponentPure(state, currentState) {
92 | return {...state, currentState };
93 | }
94 |
95 | function updateComponent(state, action) {
96 | const component = state.routes[state.currentRoute],
97 | result = component.update(state.currentState, action);
98 |
99 | return UpdateResult.case({
100 | Pure: currentState =>
101 | pure(updateComponentPure(state, currentState)),
102 | WithEffects: (currentState, eff) =>
103 | withEffects(
104 | updateComponentPure(state, currentState),
105 | Effect.Child(eff))
106 | }, result);
107 |
108 | }
109 |
110 |
111 | function update(state, action) {
112 | return Action.case({
113 | Navigate: route => navigate(state, route),
114 | Update : componentAction => updateComponent(state, componentAction)
115 | }, action);
116 | }
117 |
118 | function executeInit(state, childEff, dispatch) {
119 | window.addEventListener('hashchange', () =>
120 | dispatch(Action.Navigate(window.location.hash.substr(1) || DEFAULT_ROUTE))
121 | );
122 |
123 | if(childEff) {
124 | const component = state.routes[state.currentRoute];
125 | component.execute(state.currentState, childEff, componentDispatcher(dispatch))
126 | }
127 | }
128 |
129 | function execute(state, effect, dispatch) {
130 | Effect.case({
131 | Init: childEff => executeInit(state, childEff, dispatch),
132 | Child: eff => {
133 | const component = state.routes[state.currentRoute];
134 | component.execute(state.currentState, eff, componentDispatcher(dispatch))
135 | }
136 | }, effect);
137 | }
138 |
139 | export default { view, init, update, Action, execute, Effect };
140 |
--------------------------------------------------------------------------------
/examples/admin/src/UserList.js:
--------------------------------------------------------------------------------
1 | /** @jsx html */
2 |
3 | import { html } from 'snabbdom-jsx';
4 | import Type from 'union-type';
5 | import UserForm from './UserForm';
6 | import Status from './RequestStatus';
7 | import { UpdateResult, pure, withEffects } from './UpdateResult';
8 | import api from './api';
9 |
10 | /*
11 | state: {
12 | items : [{id: Number, user: UserForm}],
13 | nextId : Number,
14 | status : Status
15 | }
16 | */
17 |
18 | const Action = Type({
19 | Add : [],
20 | Update : [Number, UserForm.Action],
21 | GetUsers : [],
22 | GetUsersSuccess : [Array],
23 | GetUsersError : [Object]
24 | });
25 |
26 | const Effect = Type({
27 | GetUsers: [],
28 | UserForm: [Number, UserForm.Effect]
29 | });
30 |
31 | function userDispatcher(id, dispatch) {
32 | return action => dispatch(Action.Update(id, action));
33 | }
34 |
35 | const view = ({state, dispatch}) =>
36 |
37 |
38 |
{statusMsg(state.status)}
39 |
40 |
{
41 | state.items.map( item => )
42 | }
43 |
;
44 |
45 | const UserItem = ({item, dispatch}) =>
46 |
47 |
48 |
49 |
50 | const statusMsg = Status.case({
51 | Pending : () => 'Getting user list...',
52 | Error : error => `Error! ${error}`,
53 | _ : () => ''
54 | });
55 |
56 | function receiveUsers(state, users) {
57 | const items = users.map( (user, idx) => ({ id: idx + 1, user: UserForm.init(user)[0] }) );
58 | return pure({
59 | items,
60 | nextId: items.length + 1,
61 | status: Status.Success('')
62 | });
63 | }
64 |
65 | function getUsers(dispatch) {
66 | api.getUsers()
67 | .then(Action.GetUsersSuccess, Action.GetUsersError)
68 | .then(dispatch);
69 | }
70 |
71 | function addUserPure(state, user) {
72 | return {...state,
73 | users: [...state.users, {id: state.nextId, user}],
74 | nextId: state.nextId + 1
75 | };
76 | }
77 |
78 | function addUser(state, userData) {
79 | const result = UserForm.init(userData);
80 | return UpdateResult.case({
81 | Pure : user => pure(addUserPure(state, user)),
82 | WithEffects : (user, eff) => {
83 | const state = addUserPure(state, user);
84 | return withEffects(state, Effect.UserForm(state.nextId-1, eff));
85 | }
86 | }, result);
87 | }
88 |
89 | function updateUserPure(state, user, id) {
90 | return {...state,
91 | items: state.items.map(it => it.id !== id ? it : { id: it.id, user })
92 | };
93 | }
94 |
95 | function updateUser(state, id, userAction) {
96 | const item = state.items.find(it => it.id === id);
97 | if(item) {
98 | const result = UserForm.update(item.user, userAction);
99 | return UpdateResult.case({
100 | Pure : user => pure(updateUserPure(state, user, id)),
101 | WithEffects : (user, eff) => {
102 | const state = updateUserPure(state, user, id);
103 | return withEffects(state, Effect.UserForm(id, eff));
104 | }
105 | }, result);
106 | }
107 | return pure(state);
108 | }
109 |
110 | const init = () =>
111 | withEffects(
112 | { nextId: 1, items: [], status: Status.Pending() },
113 | Effect.GetUsers()
114 | );
115 |
116 | function update(state, action) {
117 | return Action.case({
118 | Add : () => addUser(state),
119 | Update : (id, userAction) => updateUser(state, id, userAction),
120 | // GetUsers Request Actions
121 | GetUsers : () => withEffects(
122 | {...state, status: Status.Pending()},
123 | Effect.GetUsers()
124 | ),
125 | GetUsersSuccess : users => receiveUsers(state, users),
126 | GetUsersError : error => pure({...state, status: Status.Error(error) })
127 | }, action);
128 | }
129 |
130 | function execute(state, effect, dispatch) {
131 | Effect.case({
132 | GetUsers: () => getUsers(dispatch),
133 | UserForm: (id, eff) => {
134 | const item = state.items.find(it => it.id === id);
135 | if(item)
136 | UserForm.execute(item.user, eff, userDispatcher(dispatch, id))
137 | }
138 | }, effect);
139 | }
140 |
141 |
142 |
143 | export default { init, view, update, Action, execute, Effect };
144 |
--------------------------------------------------------------------------------