Skip to content

Commit 8499962

Browse files
committed
First commit
0 parents  commit 8499962

28 files changed

+8234
-0
lines changed

.babelrc

+10
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
{
2+
"presets": ["@babel/preset-react", "@babel/preset-typescript", [
3+
"@babel/preset-env", {
4+
"targets": {
5+
"node": "current"
6+
}
7+
}
8+
]],
9+
"plugins": ["@babel/plugin-proposal-class-properties"]
10+
}

.gitignore

+4
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
dist
2+
.DS_Store
3+
node_modules
4+
.idea

README.md

+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
# Universal Model Vue Todo App
2+
3+

index.html

+2
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
<div id="app"></div>
2+
<script src="/dist/build.js"></script>

package-lock.json

+7,906
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

+46
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
{
2+
"name": "universal-model-react-todo-app",
3+
"version": "0.1.0",
4+
"private": true,
5+
"scripts": {
6+
"dev": "webpack-dev-server",
7+
"eslint": "eslint --ext .js,.vue src",
8+
"build": "webpack"
9+
},
10+
"dependencies": {
11+
"react": "^16.12.0",
12+
"react-dom": "^16.12.0",
13+
"universal-model-react": "^0.5.1",
14+
"vue": "^3.0.0-alpha.1"
15+
},
16+
"devDependencies": {
17+
"@babel/core": "^7.8.3",
18+
"@babel/plugin-proposal-class-properties": "^7.8.3",
19+
"@babel/preset-env": "^7.8.3",
20+
"@babel/preset-react": "^7.8.3",
21+
"@babel/preset-typescript": "^7.8.3",
22+
"@types/react": "^16.9.17",
23+
"@types/react-dom": "^16.9.4",
24+
"@typescript-eslint/eslint-plugin": "^2.15.0",
25+
"@typescript-eslint/parser": "^2.15.0",
26+
"babel-loader": "^8.0.6",
27+
"eslint": "^6.8.0",
28+
"prettier": "^1.19.1",
29+
"typescript": "^3.7.4",
30+
"webpack": "^4.41.4",
31+
"webpack-cli": "^3.3.10",
32+
"webpack-dev-server": "^3.10.1"
33+
},
34+
"browserslist": [
35+
"last 2 version",
36+
"> 0.2%",
37+
"Firefox ESR",
38+
"not dead"
39+
],
40+
"prettier": {
41+
"arrowParens": "always",
42+
"printWidth": 110,
43+
"tabWidth": 2,
44+
"singleQuote": true
45+
}
46+
}

src/App.tsx

+12
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
import * as React from 'react';
2+
import HeaderView from './header/HeaderView';
3+
import TodoListView from './todolist/TodoListView';
4+
5+
const App = () => (
6+
<div>
7+
<HeaderView />
8+
<TodoListView />
9+
</div>
10+
);
11+
12+
export default App;

src/Constants.ts

+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
export default class Constants {
2+
static FAKE_SERVICE_LATENCY_IN_MILLIS = 1000;
3+
}

src/header/HeaderView.tsx

+18
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import * as React from 'react';
2+
import store from '../store/store';
3+
import changeUserName from './model/actions/changeUserName';
4+
5+
const HeaderView = () => {
6+
const { headerState } = store.getState();
7+
store.useState([headerState]);
8+
9+
return (
10+
<div>
11+
<h1>{headerState.userName}</h1>
12+
<label>User name:</label>
13+
<input onChange={({ target: { value } }) => changeUserName(value)} />
14+
</div>
15+
);
16+
};
17+
18+
export default HeaderView;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
import store from '../../../store/store';
2+
3+
export default function changeUserName(newUserName: string): void {
4+
const { headerState } = store.getState();
5+
headerState.userName = newUserName;
6+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
export default {
2+
userName: 'John'
3+
};

src/main.tsx

+9
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import * as React from 'react';
2+
import { render } from 'react-dom';
3+
import App from './App';
4+
5+
const rootElement = document.getElementById('app');
6+
7+
if (rootElement) {
8+
render(<App />, rootElement);
9+
}

src/store/store.ts

+17
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
import { createStore } from 'universal-model-react';
2+
import initialHeaderState from '../header/model/state/initialHeaderState';
3+
import initialTodoListState from '../todolist/model/state/initialTodoListState';
4+
import createTodoListStateSelectors from '../todolist/model/state/createTodoListStateSelectors';
5+
6+
const initialState = {
7+
headerState: initialHeaderState,
8+
todosState: initialTodoListState
9+
};
10+
11+
export type State = typeof initialState;
12+
13+
const selectors = {
14+
...createTodoListStateSelectors<State>()
15+
};
16+
17+
export default createStore(initialState, selectors);

src/todolist/TodoListView.tsx

+55
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
import * as React from 'react';
2+
import { useEffect, useMemo } from 'react';
3+
import store from '../store/store';
4+
import { Todo } from './model/state/initialTodoListState';
5+
import removeTodo from './model/actions/removeTodo';
6+
import toggleShouldShowOnlyDoneTodos from './model/actions/toggleShouldShowOnlyDoneTodos';
7+
import fetchTodos from './model/actions/fetchTodos';
8+
import todoListController from './controller/todoListController';
9+
import toggleIsDoneTodo from './model/actions/toggleIsDoneTodo';
10+
11+
const TodoListView = () => {
12+
const { todosState } = store.getState();
13+
store.useState([todosState]);
14+
const { shownTodos } = store.getSelectors();
15+
store.useSelectors([shownTodos]);
16+
17+
useEffect(() => {
18+
// noinspection JSIgnoredPromiseFromCall
19+
fetchTodos();
20+
document.addEventListener('keypress', todoListController.handleKeyPress);
21+
return () => document.removeEventListener('keypress', todoListController.handleKeyPress);
22+
}, []);
23+
24+
const todoItems = useMemo(
25+
() =>
26+
shownTodos.value.map((todo: Todo, index: number) => (
27+
<li key={index}>
28+
<input
29+
id={todo.name}
30+
type="checkbox"
31+
defaultChecked={todo.isDone}
32+
onChange={() => toggleIsDoneTodo(todo)}
33+
/>
34+
<label>{todo.name}</label>
35+
<button onClick={() => removeTodo(todo)}>Remove</button>
36+
</li>
37+
)),
38+
[shownTodos.value]
39+
);
40+
41+
return (
42+
<div>
43+
<input
44+
id="shouldShowOnlyDoneTodos"
45+
type="checkbox"
46+
defaultChecked={todosState.shouldShowOnlyDoneTodos}
47+
onChange={toggleShouldShowOnlyDoneTodos}
48+
/>
49+
<label>Show only done todos</label>
50+
<ul>{todoItems}</ul>
51+
</div>
52+
);
53+
};
54+
55+
export default TodoListView;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
import addTodo from '../model/actions/addTodo';
2+
import removeAllTodos from '../model/actions/removeAllTodos';
3+
4+
export default {
5+
handleKeyPress(keyboardEvent: KeyboardEvent): void {
6+
if (keyboardEvent.code === 'KeyA') {
7+
addTodo();
8+
} else if (keyboardEvent.code === 'KeyR') {
9+
removeAllTodos();
10+
}
11+
}
12+
};

src/todolist/model/actions/addTodo.ts

+6
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
import store from '../../../store/store';
2+
3+
export default function addTodo(): void {
4+
const { todosState } = store.getState();
5+
todosState.todos.push({ name: 'new todo', isDone: false });
6+
}
+10
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
import store from '../../../store/store';
2+
import todoService from '../services/todoService';
3+
4+
export default async function fetchTodos(): Promise<void> {
5+
const { todosState } = store.getState();
6+
7+
todosState.isFetchingTodos = false;
8+
todosState.todos = await todoService.fetchTodos();
9+
todosState.isFetchingTodos = true;
10+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
import store from '../../../store/store';
2+
3+
export default function removeAllTodos(): void {
4+
const { todosState } = store.getState();
5+
todosState.todos = [];
6+
}
+7
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
import { Todo } from '../state/initialTodoListState';
2+
import store from '../../../store/store';
3+
4+
export default function removeTodo(todoToRemove: Todo): void {
5+
const { todosState } = store.getState();
6+
todosState.todos = todosState.todos.filter((todo: Todo) => todo !== todoToRemove);
7+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
import { Todo } from '../state/initialTodoListState';
2+
3+
export default function toggleIsDoneTodo(todo: Todo): void {
4+
todo.isDone = !todo.isDone;
5+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
import store from '../../../store/store';
2+
3+
export default function toggleShouldShowOnlyDoneTodos(): void {
4+
const { todosState } = store.getState();
5+
todosState.shouldShowOnlyDoneTodos = !todosState.shouldShowOnlyDoneTodos;
6+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import { ITodoService } from './ITodoService';
2+
import { Todo } from '../state/initialTodoListState';
3+
import Constants from '../../../Constants';
4+
5+
export default class FakeTodoService implements ITodoService {
6+
fetchTodos(): Promise<Todo[]> {
7+
return new Promise<Todo[]>((resolve: (todo: Todo[]) => void) => {
8+
setTimeout(
9+
() =>
10+
resolve([
11+
{ name: 'first todo', isDone: true },
12+
{ name: 'second todo', isDone: false }
13+
]),
14+
Constants.FAKE_SERVICE_LATENCY_IN_MILLIS
15+
);
16+
});
17+
}
18+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
import { Todo } from '../state/initialTodoListState';
2+
3+
export interface ITodoService {
4+
fetchTodos(): Promise<Todo[]>;
5+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
import FakeTodoService from './FakeTodoService';
2+
3+
export default new FakeTodoService();
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
import { State } from '../../../store/store';
2+
import { Todo } from './initialTodoListState';
3+
4+
const createTodoListStateSelectors = <T extends State>() => ({
5+
shownTodos: (state: T) =>
6+
state.todosState.todos.filter(
7+
(todo: Todo) =>
8+
(state.todosState.shouldShowOnlyDoneTodos && todo.isDone) || !state.todosState.shouldShowOnlyDoneTodos
9+
)
10+
});
11+
12+
export default createTodoListStateSelectors;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
export interface Todo {
2+
name: string;
3+
isDone: boolean;
4+
}
5+
6+
export default {
7+
todos: [] as Todo[],
8+
shouldShowOnlyDoneTodos: false,
9+
isFetchingTodos: false
10+
};

tsconfig.json

+13
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
{
2+
"compilerOptions": {
3+
"target": "esnext",
4+
"moduleResolution": "node",
5+
"allowJs": true,
6+
"noEmit": true,
7+
"strict": true,
8+
"esModuleInterop": true,
9+
"jsx": "react"
10+
},
11+
"include": ["src/**/*"],
12+
"exclude": ["node_modules"]
13+
}

webpack.config.js

+27
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
const path = require("path");
2+
3+
module.exports = (env = {}) => ({
4+
entry: path.resolve(__dirname, "./src/main.tsx"),
5+
output: {
6+
path: path.resolve(__dirname, "./dist"),
7+
publicPath: "/dist/",
8+
filename: "build.js"
9+
},
10+
resolve: {
11+
extensions: ['.webpack.js', '.web.js', '.ts', '.tsx', '.js', 'jsx']
12+
},
13+
module: {
14+
rules: [
15+
{
16+
test: /\.tsx?$/,
17+
loader: "babel-loader"
18+
}
19+
]
20+
},
21+
devServer: {
22+
inline: true,
23+
hot: true,
24+
stats: "minimal",
25+
contentBase: __dirname
26+
}
27+
});

0 commit comments

Comments
 (0)