Skip to content
This repository has been archived by the owner on Jun 22, 2021. It is now read-only.

Commit

Permalink
fix: Fixes todo editing.
Browse files Browse the repository at this point in the history
  • Loading branch information
ryasmi committed Feb 17, 2018
1 parent 590c57d commit 1b5517e
Show file tree
Hide file tree
Showing 8 changed files with 206 additions and 170 deletions.
63 changes: 33 additions & 30 deletions src/presenter/Footer.tsx
Original file line number Diff line number Diff line change
@@ -1,40 +1,43 @@
import * as React from 'react';
import Service from '../service/Facade';
import asyncRender from './utils/asyncRender';
import connect from './utils/connect';
import AsyncConnect from './utils/AsyncConnect';

export interface Props {
readonly service: Service;
}

export default connect(asyncRender(async ({ service }: Props) => {
const route = service.getRoute();
const completeCount = await service.getCompleteCount();
const incompleteCount = await service.getIncompleteCount();
export default ({ service }: Props) => {
return (
<AsyncConnect service={service} render={async () => {
const route = service.getRoute();
const completeCount = await service.getCompleteCount();
const incompleteCount = await service.getIncompleteCount();

const renderLink = (text: string, link: string) => {
const className = route === link ? 'selected' : '';
const href = `#${link}`;
return <li><a href={href} className={className}>{text}</a></li>;
};
const renderLink = (text: string, link: string) => {
const className = route === link ? 'selected' : '';
const href = `#${link}`;
return <li><a href={href} className={className}>{text}</a></li>;
};

return (
<footer className="footer">
<span className="todo-count">
<strong>{incompleteCount}</strong> items left
</span>
<ul className="filters">
{renderLink('All', '')}
{renderLink('Active', 'active')}
{renderLink('Completed', 'completed')}
</ul>
{completeCount > 0 ? (
<button
className="clear-completed"
onClick={async () => { await service.deleteCompleted(); }}>
Clear completed
</button>
) : <noscript />}
</footer>
return (
<footer className="footer">
<span className="todo-count">
<strong>{incompleteCount}</strong> items left
</span>
<ul className="filters">
{renderLink('All', '')}
{renderLink('Active', 'active')}
{renderLink('Completed', 'completed')}
</ul>
{completeCount > 0 ? (
<button
className="clear-completed"
onClick={async () => { await service.deleteCompleted(); }}>
Clear completed
</button>
) : <noscript />}
</footer>
);
}} />
);
}));
};
42 changes: 23 additions & 19 deletions src/presenter/NewTodo.tsx
Original file line number Diff line number Diff line change
@@ -1,30 +1,34 @@
import * as React from 'react';
import Service from '../service/Facade';
import connect from './utils/connect';
import SyncConnect from './utils/SyncConnect';

const enterKey = 13;

export interface Props {
readonly service: Service;
}

export default connect(({ service }: Props) => {
const newTodoTitle = service.getNewTodoTitle();

export default ({ service }: Props) => {
return (
<input
className="new-todo"
placeholder="What needs to be done?"
value={newTodoTitle}
onChange={(event) => {
service.changeNewTodoTitle((event.target as any).value);
}}
onKeyDown={async (event) => {
if (event.keyCode === enterKey) {
await service.createNewTodo();
service.changeNewTodoTitle('');
}
}}
/>
<SyncConnect service={service} render={() => {
const newTodoTitle = service.getNewTodoTitle();

return (
<input
className="new-todo"
placeholder="What needs to be done?"
value={newTodoTitle}
onChange={(event) => {
service.changeNewTodoTitle((event.target as any).value);
}}
onKeyDown={async (event) => {
if (event.keyCode === enterKey) {
await service.createNewTodo();
service.changeNewTodoTitle('');
}
}}
/>
);
}} />
);
});
};
109 changes: 57 additions & 52 deletions src/presenter/TodoItem.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import * as React from 'react';
import Service from '../service/Facade';
import TodoEntity from '../utils/TodoEntity';
import connect from './utils/connect';
import SyncConnect from './utils/SyncConnect';

const enterKey = 13;
const escapeKey = 27;
Expand All @@ -12,57 +12,62 @@ export interface Props {
readonly service: Service;
}

export default connect(({ todo, service }: Props) => {
const isEditing = service.getIsEditing(todo.id);
const editedTitle = service.getEditedTitle(todo.id);

const handleSave = async () => {
const title = editedTitle.trim();
if (title.length === 0) {
await service.removeTodo(todo.id);
return;
}
service.setEditedTitle(todo.id, title);
service.setIsEditing(todo.id, false);
await service.setTodoTitle(todo.id, title);
};

export default ({ todo, service }: Props) => {
return (
<li className={`${todo.completed ? 'completed' : ''} ${isEditing ? 'editing' : ''}`}>
<div className="view">
<input
className="toggle"
type="checkbox"
checked={todo.completed}
onChange={async () => {
await service.setTodoCompletion(todo.id, !todo.completed);
}}
/>
<label onDoubleClick={async () => {
service.setIsEditing(todo.id, true);
}}>
{todo.title}
</label>
<button className="destroy" onClick={async () => {
<SyncConnect service={service} render={() => {
const isEditing = service.getIsEditing(todo.id);
const editedTitle = service.getEditedTitle(todo.id);

const handleSave = async () => {
const title = editedTitle.trim();
if (title.length === 0) {
await service.removeTodo(todo.id);
}} />
</div>
<input
className="edit"
value={editedTitle}
onBlur={handleSave}
onChange={async (event) => {
service.setEditedTitle(todo.id, (event.target as any).value);
}}
onKeyDown={async (event) => {
if (event.keyCode === escapeKey) {
service.setEditedTitle(todo.id, todo.title);
service.setIsEditing(todo.id, false);
} else if (event.keyCode === enterKey) {
await handleSave();
}
}}
/>
</li>
return;
}
service.setEditedTitle(todo.id, title);
service.setIsEditing(todo.id, false);
await service.setTodoTitle(todo.id, title);
};

return (
<li className={`${todo.completed ? 'completed' : ''} ${isEditing ? 'editing' : ''}`}>
<div className="view">
<input
className="toggle"
type="checkbox"
checked={todo.completed}
onChange={async () => {
await service.setTodoCompletion(todo.id, !todo.completed);
}}
/>
<label onDoubleClick={async () => {
service.setEditedTitle(todo.id, todo.title);
service.setIsEditing(todo.id, true);
}}>
{todo.title}
</label>
<button className="destroy" onClick={async () => {
await service.removeTodo(todo.id);
}} />
</div>
<input
className="edit"
value={editedTitle}
onBlur={handleSave}
onChange={async (event) => {
service.setEditedTitle(todo.id, (event.target as any).value);
}}
onKeyDown={async (event) => {
if (event.keyCode === escapeKey) {
service.setEditedTitle(todo.id, todo.title);
service.setIsEditing(todo.id, false);
} else if (event.keyCode === enterKey) {
await handleSave();
}
}}
/>
</li>
);
}} />
);
});
};
29 changes: 16 additions & 13 deletions src/presenter/TodoItems.tsx
Original file line number Diff line number Diff line change
@@ -1,23 +1,26 @@
import * as React from 'react';
import Service from '../service/Facade';
import TodoItem from './TodoItem';
import asyncRender from './utils/asyncRender';
import connect from './utils/connect';
import AsyncConnect from './utils/AsyncConnect';

export interface Props {
readonly service: Service;
}

export default connect(asyncRender(async ({ service }: Props) => {
const todos = await service.getRouteTodos();

export default ({ service }: Props) => {
return (
<div>
<ul className="todo-list">
{todos.map((todo) => {
return <TodoItem key={todo.id} todo={todo} service={service} />;
})}
</ul>
</div>
<AsyncConnect service={service} render={async () => {
const todos = await service.getRouteTodos();

return (
<div>
<ul className="todo-list">
{todos.map((todo) => {
return <TodoItem key={todo.id} todo={todo} service={service} />;
})}
</ul>
</div>
);
}} />
);
}));
};
46 changes: 46 additions & 0 deletions src/presenter/utils/AsyncConnect.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import * as React from 'react';
import Service from '../../service/Facade';
import observer from '../../utils/observer';

const loader = () => {
return <span>Loading</span>;
};

export interface Props {
readonly service: Service;
readonly render: () => Promise<JSX.Element>;
}

export interface State {
readonly renderedComponent: JSX.Element;
}

// tslint:disable:no-class no-this
export default class AsyncConnect extends React.Component<Props, State> {
constructor(p: Props, s: State) {
super(p, s);
const renderedComponent = loader();
this.state = { renderedComponent };
}

public componentDidMount() {
observer.addListener('change', this.update.bind(this));
this.update().catch((err) => {
// tslint:disable-next-line:no-console
console.error(err);
});
}

public componentWillUnmount() {
observer.removeListener('change', this.update.bind(this));
}

private async update() {
const renderedComponent = await this.props.render();
this.setState({ renderedComponent });
}

public render() {
return this.state.renderedComponent;
}
}
31 changes: 31 additions & 0 deletions src/presenter/utils/SyncConnect.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import * as React from 'react';
import Service from '../../service/Facade';
import observer from '../../utils/observer';

export interface Props {
readonly service: Service;
readonly render: () => JSX.Element;
}

export interface State {
readonly change: Date;
}

// tslint:disable:no-class no-this
export default class Connect extends React.Component<Props, State> {
public componentDidMount() {
observer.addListener('change', this.update.bind(this));
}

public componentWillUnmount() {
observer.removeListener('change', this.update.bind(this));
}

private async update() {
this.setState({ change: new Date() });
}

public render() {
return this.props.render();
}
}
29 changes: 0 additions & 29 deletions src/presenter/utils/asyncRender.tsx

This file was deleted.

0 comments on commit 1b5517e

Please sign in to comment.