Skip to content

Commit

Permalink
Merge pull request #12 from ericpoon/develop
Browse files Browse the repository at this point in the history
Merge develop into master
  • Loading branch information
zhiqipan authored May 18, 2018
2 parents bf139ce + a7dacb6 commit 9a9780e
Show file tree
Hide file tree
Showing 33 changed files with 1,923 additions and 70 deletions.
2 changes: 1 addition & 1 deletion .babelrc
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
{
"presets": ["env"],
"presets": ["env", "react"],
"plugins": ["transform-decorators-legacy", "transform-class-properties"]
}
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
/node_modules
/.idea
/dist
/coverage
*.log
examples/public/bundle.js
37 changes: 30 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,10 +1,18 @@
# Observable and Computed
# MobX

The minimal working code for observing primitive values is available at
commit [`c11313d`](https://github.com/ericpoon/demo-mobx/commit/c11313d04cbd91afb36c2a43bbaca9770b8b2b4c).
If you want to have a very basic idea regarding how `@observable` and `autorun()` work,
it should be a good start point.
> It's easy! Don't bother starting with the source code of official MobX.
Implemented based on [MobX](https://github.com/mobxjs/mobx), for fun and learning purpose.

This project supports basic features of MobX including `@observable` and `autorun()`,
but also implements `@observer` in [mobx-react](https://github.com/mobxjs/mobx-react).

Comprehensive unit tests and integration tests with real react components are included.

---

The minimal working code for observable primitive values (number, string and boolean) is available at
commit `c11313d`. If you want to have a very basic idea regarding how `@observable` and `autorun()` work,
it should be a good start point. Checkout to have a look!
> It's relatively easy to understand! You may not want to start with the source code of official MobX.
### How `@observable` and `autorun()` work?

Expand All @@ -25,6 +33,21 @@ Or in other words, here's the flow:

---

As of commit [`51315bd`](https://github.com/ericpoon/demo-mobx/commit/51315bd), `@observable` and `autorun()` should work well enough to support **primitive
As of commit `51315bd`, `@observable` and `autorun()` should work well enough to support **primitive
values, arrays and objects**. Also `@observable` automatically converts array items or object properties into observable properties in a recursive manner.
This behaviour is the same as what official MobX library does.

---

As of commit `1ce8a9b`, we have successfully integrated this MobX with React (without `@observer`), and several examples are provided to demo how to use it.

### How to see the demo?

1. `yarn install`
2. `yarn start:demo`

---

As of commit `1c3d12f`, `@observer` annotation has been added to support the integration with React.

In my opinion, `@observer` is a syntax sugar that simplifies data binding with React.
11 changes: 11 additions & 0 deletions examples/Counter/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Counter</title>
</head>
<body>
<div id="counter-app"></div>
<script type="text/javascript" src="../public/bundle.js"></script>
</body>
</html>
28 changes: 28 additions & 0 deletions examples/Counter/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import React, { Component } from 'react';
import ReactDom from 'react-dom';
import { observable, observer } from '../../src/core/index';

@observer
class Main extends Component {
render() {
const { counter } = this.props;

return (
<p>Count: {counter.count}</p>
);
}
}

class Counter {
@observable
count = 0;
}

const counter = new Counter();

setInterval(() => {
counter.count += 1;
}, 300);

const app = document.getElementById('counter-app');
if (app) ReactDom.render(<Main counter={counter} />, app);
7 changes: 7 additions & 0 deletions examples/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
# Demo

#### How to start

1) Go back to the project root directory
2) `yarn install`
3) `yarn start:demo`
11 changes: 11 additions & 0 deletions examples/TodoList/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Todo List</title>
</head>
<body>
<div id="todo-list-app"></div>
<script type="text/javascript" src="../public/bundle.js"></script>
</body>
</html>
52 changes: 52 additions & 0 deletions examples/TodoList/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import React, { Component } from 'react';
import ReactDom from 'react-dom';
import { observer } from '../../src/core';
import Task from './models/Task';
import TaskList from './models/TaskList';

@observer
class Main extends Component {
taskItem = (task) => {
const { title, done } = task;
const onTaskClick = () => task.done = !task.done;
return (
<div>
<label style={{ color: done ? 'green' : 'red', fontSize: 20, lineHeight: 2 }}>
<input type={'checkbox'} checked={done} onClick={onTaskClick} />
{title}
</label>
</div>
);
};

render() {
const { tasks } = this.props;
const { finished, unfinished } = tasks;

return (
<div>
<h2>Task List</h2>
<p>Summary: {finished.length} completed and {unfinished.length} to be done.</p>
{tasks.list.map(task => {
return <div key={task.title}>{this.taskItem(task)}</div>;
})}
</div>
);
}
}

const tasks = new TaskList();
const taskA = new Task('Pick up laundry', false);
const taskB = new Task('Go to supermarket', false);
const taskC = new Task('Take medicine', false);
const taskD = new Task('Housekeeping', false);
const taskE = new Task('Appointment with clients', false);

tasks.list.push(taskA);
tasks.list.push(taskB);
tasks.list.push(taskC);
tasks.list.push(taskD);
tasks.list.push(taskE);

const app = document.getElementById('todo-list-app');
if (app) ReactDom.render(<Main tasks={tasks} />, app);
10 changes: 10 additions & 0 deletions examples/TodoList/models/Task.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { observable } from '../../../src/core';

export default class Task {
constructor(t, d) {
this.title = t;
this.done = d;
}

@observable done;
}
11 changes: 11 additions & 0 deletions examples/TodoList/models/TaskList.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
export default class TaskList {
list = [];

get finished() {
return this.list.filter(i => i.done);
}

get unfinished() {
return this.list.filter(i => !i.done);
}
}
24 changes: 24 additions & 0 deletions examples/TodoListAdvanced/components/TaskInput.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import React, { Component } from 'react';
import { observer } from '../../../src/core';

@observer
class TaskInput extends Component {
render() {
const { label, buttonText, task, onSubmit } = this.props;
return (
<div>
{label}
<input
type={'text'}
value={task.title}
onChange={event => {
task.title = event.target.value;
}}
/>
<button onClick={onSubmit}>{buttonText}</button>
</div>
);
}
}

export default TaskInput;
22 changes: 22 additions & 0 deletions examples/TodoListAdvanced/components/TaskItem.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import React from 'react';

const TaskItem = ({ task, onEditClick, onDeleteClick }) => {
const { title, done } = task;
const onTaskClick = () => {
task.done = !task.done;
};

return (
<div>
<label style={{ color: done ? 'green' : 'red', fontSize: 20, lineHeight: 2 }}>
<input type={'checkbox'} checked={done} onChange={onTaskClick} />
{title}
</label>
<button onClick={onEditClick}>Edit</button>
<button onClick={onDeleteClick}>Delete</button>
<hr />
</div>
);
};

export default TaskItem;
90 changes: 90 additions & 0 deletions examples/TodoListAdvanced/components/TaskList.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
import React, { Component } from 'react';
import { observer } from '../../../src/core';
import TaskItemComponent from './TaskItem';
import TaskInput from './TaskInput';
import Task from '../models/Task';

@observer
class TaskList extends Component {
state = {
editingIdx: -1,
};

observableTask = new Task();

onTaskEditClick = (task, idx) => {
this.observableTask.title = task.title;
this.setState({ editingIdx: idx });
};

onTaskDeleteClick = (idx) => {
if (idx === this.state.editingIdx) {
this.setState({ editingIdx: -1 });
this.observableTask.title = '';
}
this.props.tasks.list.remove(idx);
};

renderInputComponent = () => {
const { editingIdx } = this.state;
const { list } = this.props.tasks;
const isEditing = editingIdx >= 0;
let label, buttonText, onSubmit;

if (isEditing) {
label = 'Edit task: ';
buttonText = 'Save';
onSubmit = () => {
list[editingIdx].title = this.observableTask.title;
this.setState({ editingIdx: -1 });
this.observableTask.title = '';
};
} else {
label = 'New task: ';
buttonText = 'Add';
onSubmit = () => {
list.push(new Task(this.observableTask.title, false));
this.observableTask.title = '';
};
}

return (
<TaskInput
label={label}
buttonText={buttonText}
task={this.observableTask}
onSubmit={onSubmit}
/>
);
};

renderTaskItems = () => {
const list = [...this.props.tasks.list]; // safe copy to avoid index issue due to deletion
return list.map((task, idx) => {
return (
<div key={task.title}>
<TaskItemComponent
task={task}
onEditClick={() => this.onTaskEditClick(task, idx)}
onDeleteClick={() => this.onTaskDeleteClick(idx)}
/>
</div>
);
});
};

render() {
const { finished, unfinished } = this.props.tasks;

return (
<div>
<h2>Task List (Advanced)</h2>
<p>Summary: {finished.length} completed and {unfinished.length} to be done.</p>
{this.renderTaskItems()}
{this.renderInputComponent()}
</div>
);
}
}

export default TaskList;
11 changes: 11 additions & 0 deletions examples/TodoListAdvanced/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Todo List (Advanced)</title>
</head>
<body>
<div id="todo-list-advanced-app"></div>
<script type="text/javascript" src="../public/bundle.js"></script>
</body>
</html>
21 changes: 21 additions & 0 deletions examples/TodoListAdvanced/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import React from 'react';
import ReactDom from 'react-dom';
import Task from './models/Task';
import TaskList from './models/TaskList';
import TaskListComponent from './components/TaskList';

const tasks = new TaskList();
const taskA = new Task('Pick up laundry', true);
const taskB = new Task('Go to supermarket', true);
const taskC = new Task('Take medicine', false);
const taskD = new Task('Housekeeping', false);
const taskE = new Task('Appointment with clients', false);

tasks.list.push(taskA);
tasks.list.push(taskB);
tasks.list.push(taskC);
tasks.list.push(taskD);
tasks.list.push(taskE);

const app = document.getElementById('todo-list-advanced-app');
if (app) ReactDom.render(<TaskListComponent tasks={tasks} />, app);
11 changes: 11 additions & 0 deletions examples/TodoListAdvanced/models/Task.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { observable } from '../../../src/core';

export default class Task {
constructor(t, d) {
this.title = t;
this.done = d;
}

@observable title;
@observable done;
}
13 changes: 13 additions & 0 deletions examples/TodoListAdvanced/models/TaskList.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { observable } from '../../../src/core';

export default class TaskList {
@observable list = [];

get finished() {
return this.list.filter(i => i.done);
}

get unfinished() {
return this.list.filter(i => !i.done);
}
}
Loading

0 comments on commit 9a9780e

Please sign in to comment.