GoF, рдкреНрдпреЛрд░ рдЖрд░реНрдХрд┐рдЯреЗрдХреНрдЪрд░, рдкрд░рдлреЗрдХреНрдЯ рдХреЛрдб - рдЯреНрд░реВ рдкреНрд░реЛрдЧреНрд░рд╛рдорд░ рдХреА рд╣реИрдВрдбрдмреБрдХред рд▓реЗрдХрд┐рди рдлреНрд░рдВрдЯ-рдПрдВрдб рдХреА рджреБрдирд┐рдпрд╛ рдореЗрдВ, рдЗрди рдкреБрд╕реНрддрдХреЛрдВ рдХреЗ рдХрдИ рд╡рд┐рдЪрд╛рд░ рдЙрдкрд▓рдмреНрдз рдирд╣реАрдВ рд╣реИрдВред рд╡рд╛рд╕реНрддрд╡рд┐рдХ рджреБрдирд┐рдпрд╛ рдореЗрдВ рдХрдо рд╕реЗ рдХрдо рд╕рдорд╛рдирддрд╛ рдмрд╣реБрдд рдореБрд╢реНрдХрд┐рд▓ рд╣реИред рд╢рд╛рдпрдж рдЖрдзреБрдирд┐рдХ рд╕реАрдорд╛рдВрдд рд╕рдордп рд╕реЗ рдкрд╣рд▓реЗ рд╣реИ? рд╣реЛ рд╕рдХрддрд╛ рд╣реИ рдХрд┐ "рдХрд╛рд░реНрдпрд╛рддреНрдордХ" рдкреНрд░реЛрдЧреНрд░рд╛рдорд┐рдВрдЧ рдФрд░ рд░рд┐рдПрдХреНрдЯ рдкрд╣рд▓реЗ рд╣реА рдУрдУрдкреА рдкрд░ рдЕрдкрдиреА рд╢реНрд░реЗрд╖реНрдарддрд╛ рд╕рд╛рдмрд┐рдд рдХрд░ рдЪреБрдХреЗ рд╣реЛрдВ? рдЗрд╕ рд▓реЗрдЦ рдореЗрдВ, рдореИрдВ рдПрдХ рдЯреВрдбреВ-рд╕реВрдЪреА рдПрдкреНрд▓рд┐рдХреЗрд╢рди рдХрд╛ рдПрдХ рдЙрджрд╛рд╣рд░рдг рджреЗрдирд╛ рдЪрд╛рд╣рддрд╛ рд╣реВрдВ рдЬрд┐рд╕реЗ рдореИрдВрдиреЗ рдХреНрд▓рд╛рд╕рд┐рдХ рдХрд┐рддрд╛рдмреЛрдВ рдореЗрдВ рд╡рд░реНрдгрд┐рдд рд╕рд┐рджреНрдзрд╛рдВрддреЛрдВ рдФрд░ рджреГрд╖реНрдЯрд┐рдХреЛрдгреЛрдВ рдХреЗ рдЕрдиреБрд╕рд╛рд░ рд▓рд╛рдЧреВ рдХрд░рдиреЗ рдХреА рдХреЛрд╢рд┐рд╢ рдХреА рдереАредрдврд╛рдВрдЪрд╛ рдирд┐рд░реНрднрд░рддрд╛
рд░реВрдкрд░реЗрдЦрд╛ рдЖрдзреБрдирд┐рдХ рдореЛрд░реНрдЪреЗ рдХреА рдЖрдзрд╛рд░рд╢рд┐рд▓рд╛ рд╣реИред Hh.ru рдкрд░ рд░рд┐рдХреНрддрд┐рдпрд╛рдВ рд░рд┐рдПрдХреНрдЯ рдмрдирд╛рдо рдПрдВрдЧреБрд▓рд░ рдмрдирд╛рдо рд╡реА рдбреЗрд╡рд▓рдкрд░реНрд╕ рд╣реИрдВред рдореИрдВрдиреЗ рдЗрдирдореЗрдВ рд╕реЗ рдкреНрд░рддреНрдпреЗрдХ рдЪреМрдЦрдЯреЗ рдХреЗ рд╕рд╛рде рдХрд╛рдо рдХрд┐рдпрд╛, рдФрд░ рдмрд╣реБрдд рд▓рдВрдмреЗ рд╕рдордп рддрдХ рдореИрдВ рдпрд╣ рдирд╣реАрдВ рд╕рдордЭ рд╕рдХрд╛ рдХрд┐ рдореБрдЭреЗ 3 рд╕рд╛рд▓ рд╕реЗ Vue рдХреЗ рд╕рд╛рде рдХрд╛рдо рдХрд░рдирд╛ рдкрдбрд╝рд╛ рдерд╛ рдПрдХ рдмрдЯрди рдХреЛ рд▓рд╛рд▓ рд╕реЗ рдмреИрдВрдЧрдиреА рд░рдВрдЧ рддрдХ? рдореБрдЭреЗ рдпрд╣ рдЬрд╛рдирдиреЗ рдХреА рдЖрд╡рд╢реНрдпрдХрддрд╛ рд╣реИ рдХрд┐ рдПрдХ рд╣реА рдмрдЯрди рдХреЛ рдмрд╛рдПрдВ рд╕реЗ рджрд╛рдПрдВ рд╕реНрдерд╛рдирд╛рдВрддрд░рд┐рдд рдХрд░рдиреЗ рдХреЗ рд▓рд┐рдП рдкреНрд░реЛрдЯреЛрдЯрд╛рдЗрдк, рдпрд╛ рдЗрд╡реЗрдВрдЯ рд▓реВрдк рдХреЗ рд╕рд┐рджреНрдзрд╛рдВрдд рдкрд░ рдХреИрд╕реЗ рд╡рд╛рд░ рдХрд┐рдпрд╛ рдЬрд╛рдП? рдЗрд╕рдХрд╛ рдЙрддреНрддрд░ рд╕рд░рд▓ рд╣реИ - рд╣рдо рд▓рд╛рдЗрдмреНрд░реЗрд░реА-рдмрд╛рдЙрдВрдб рдПрдкреНрд▓рд┐рдХреЗрд╢рди рд▓рд┐рдЦрддреЗ рд╣реИрдВредрдХреНрдпреЛрдВ рдкреНрд░рддрд┐рдХреНрд░рд┐рдпрд╛ рдХреЗ рд╕рд╛рде рдХрд╛рдо рдХрд░рдиреЗ рд╡рд╛рд▓реЗ рд▓рдВрдмреЗ рдЕрдиреБрднрд╡ рд╡рд╛рд▓реА рдХрдВрдкрдирд┐рдпрд╛рдВ рд╣реИрдВ? рд╣рд╛рдВ, рдХреНрдпреЛрдВрдХрд┐ рдПрдкреНрд▓рд┐рдХреЗрд╢рди рдЗрд╕ рд░рд┐рдПрдХреНрдЯ рдХреА рд╡рд┐рд╢реЗрд╖рддрд╛рдУрдВ рдкрд░ рдЕрддреНрдпрдзрд┐рдХ рдирд┐рд░реНрднрд░ рд╣реИ, рдФрд░ рдмрдЯрди рдХреЛ рджреЛрд╣рд░рд╛рддреЗ рд╕рдордп рдХреБрдЫ рднреА рдирд╣реАрдВ рддреЛрдбрд╝рдиреЗ рдХреЗ рд▓рд┐рдП, рдЖрдкрдХреЛ рдЕрдкрдиреЗ рд╕рд┐рд░ рдХреЛ рддреЛрдбрд╝рдирд╛ рдЪрд╛рд╣рд┐рдП рдХрд┐ рдХреИрд╕реЗ рдкрд░рд┐рд╡рд░реНрддрди рдХрд╛ рдкрддрд╛ рд▓рдЧрд╛рдиреЗ рдХреЗ рд▓рд┐рдП, рдШрдЯрдХ рдкреЗрдбрд╝ рдХреЗ рд░рд┐рдПрдХреНрд╢рди рдХреЗ рдЕрдВрджрд░ рдХрд╛рдо рдХрд░рддрд╛ рд╣реИ, рдФрд░ рдпрд╣ рдмрдЯрди рдХреЛ рдлрд┐рд░ рд╕реЗ рджрдмрд╛рдиреЗ рдХреЗ рдХрд╛рд░реНрдп рд╕реЗ рдХреИрд╕реЗ рд╕рдВрдмрдВрдзрд┐рдд рд╣реИред (рдореИрдВ рдорд╛рдирддрд╛ рд╣реВрдВ, рдпреЗ рд╕рднреА рд╡рд┐рд╢реЗрд╖ рдорд╛рдорд▓реЗ рд╣реИрдВ ... рдФрд░ рдЖрдкрдХреА рдХрдВрдкрдиреА рдореЗрдВ, рдХреНрдпрд╛ рдЖрдк рдврд╛рдВрдЪреЗ рдХреЗ рд╕рд╛рде рдХрд╛рдо рдХрд░рдиреЗ рдХреЗ рдЕрдиреБрднрд╡ рдХреЗ рдмрд┐рдирд╛ рд╡рд┐рд╢реЗрд╖рдЬреНрдЮ рд▓реЗрдиреЗ рдХреЗ рд▓рд┐рдП рддреИрдпрд╛рд░ рд╣реИрдВ?)рднрд╛рд╖рд╛ рдХрд╛ рдЙрдкрдпреЛрдЧ рдХрд░рддреЗ рд╣реБрдП рдХрд╛рд░реНрдпрдХреНрд░рдо, рднрд╛рд╖рд╛ рдирд╣реАрдВред (рдореИрдХрдХреЛрдиреЗрд▓) рдПрдХ
рдврд╛рдВрдЪрд╛ рдПрдХ рдЙрдкрдХрд░рдг рд╣реИ, рдЬреАрд╡рди рдХрд╛ рдПрдХ рддрд░реАрдХрд╛ рдирд╣реАрдВ рд╣реИред (рдорд╛рд░реНрдЯрд┐рди)
рдореЛрд░реНрдЪреЗ рдХреА рджреБрдирд┐рдпрд╛ рдХреЗ рд▓рд┐рдП, рдпреЗ рд╢реЛрдз рд╕рдмрд╕реЗ рдЕрдЪреНрдЫрд╛ рдЦрд╛рд▓реА рд╡рд╛рдХреНрдпрд╛рдВрд╢ рд╣реИрдВ, рдФрд░ рд╡рд┐рдкрд░реАрдд рд╕рд╛рдмрд┐рдд рдХрд░рдиреЗ рдХреЗ рд▓рд┐рдП рд╕рдмрд╕реЗ рдЦрд░рд╛рдм рдЪреБрдиреМрддреА рд╣реИред рдЖрдЗрдП рдЖрдзрд┐рдХрд╛рд░рд┐рдХ рдкреНрд░рддрд┐рдХреНрд░рд┐рдпрд╛ рджрд╕реНрддрд╛рд╡реЗрдЬ рджреЗрдЦреЗрдВ, рдФрд░ рдПрдХ рд╕рд░рд▓ рдЯреВрдбреВ-рд╕реВрдЪреА рдПрдкреНрд▓рд┐рдХреЗрд╢рди рдХрд╛ рдПрдХ рдЙрджрд╛рд╣рд░рдг рджреЗрдЦреЗрдВредрдЖрдзрд┐рдХрд╛рд░рд┐рдХ рдкреНрд░рддрд┐рдХреНрд░рд┐рдпрд╛ рд╡реЗрдмрд╕рд╛рдЗрдЯ рд╕реЗ рдЯреВрдбреВ-рд╕реВрдЪреА рдХрд╛ рдЙрджрд╛рд╣рд░рдгclass TodoApp extends React.Component {
constructor(props) {
super(props);
this.state = { items: [], text: '' };
this.handleChange = this.handleChange.bind(this);
this.handleSubmit = this.handleSubmit.bind(this);
}
render() {
return (
<div>
<h3> </h3>
<TodoList items={this.state.items} />
<form onSubmit={this.handleSubmit}>
<label htmlFor="new-todo">
?
</label>
<input
id="new-todo"
onChange={this.handleChange}
value={this.state.text}
/>
<button>
#{this.state.items.length + 1}
</button>
</form>
</div>
);
}
handleChange(e) {
this.setState({ text: e.target.value });
}
handleSubmit(e) {
e.preventDefault();
if (!this.state.text.length) {
return;
}
const newItem = {
text: this.state.text,
id: Date.now()
};
this.setState(state => ({
items: state.items.concat(newItem),
text: ''
}));
}
}
class TodoList extends React.Component {
render() {
return (
<ul>
{this.props.items.map(item => (
<li key={item.id}>{item.text}</li>
))}
</ul>
);
}
}
тАЬ рд╕рд╣рд╛рд░рд╛ рдФрд░ рд░рд╛рдЬреНрдп рдХрд╛ рдЙрдкрдпреЛрдЧ рдХрд░рдХреЗ , рдЖрдк рдПрдХ рдЫреЛрдЯреА рд╕реА рдЯреВ-рдбреВ рд╕реВрдЪреА рдПрдкреНрд▓рд┐рдХреЗрд╢рди рдмрдирд╛ рд╕рдХрддреЗ рд╣реИрдВред рдЗрд╕ рдЙрджрд╛рд╣рд░рдг рдореЗрдВ, рддрддреНрд╡реЛрдВ рдХреА рд╡рд░реНрддрдорд╛рди рд╕реВрдЪреА рдХреЛ рдЯреНрд░реИрдХ рдХрд░рдиреЗ рдХреЗ рд▓рд┐рдП рд░рд╛рдЬреНрдп рдХрд╛ рдЙрдкрдпреЛрдЧ рдХрд┐рдпрд╛ рдЬрд╛рддрд╛ рд╣реИ ... "рдиреМрд╕рд┐рдЦрд┐рдП рдкреНрд░реЛрдЧреНрд░рд╛рдорд░ рдХреЗ рд▓рд┐рдП (рдЕрд░реНрдерд╛рдд, рдореБрдЭреЗ рдХреБрдЫ рд╕рд╛рд▓ рдкрд╣рд▓реЗ), рдпрд╣ рд╡рд╛рдХреНрдпрд╛рдВрд╢ рд╕реНрд╡рдЪрд╛рд▓рд┐рдд рд░реВрдк рд╕реЗ рдЖрдЙрдЯрдкреБрдЯ рдЙрддреНрдкрдиреНрди рдХрд░рддрд╛ рд╣реИ:" рдпрд╣рд╛рдВ рдПрдХ рдЯреВрдбреВ-рд╕реВрдЪреА рдПрдкреНрд▓рд┐рдХреЗрд╢рди рдХрд╛ рдПрдХ рдЖрджрд░реНрд╢ рдЙрджрд╛рд╣рд░рдг рд╣реИ "ред рд▓реЗрдХрд┐рди рдШрдЯрдХ рдореЗрдВ рд░рд╛рдЬреНрдп рдХреЛ рдХреМрди рд╕рдВрдЧреНрд░рд╣реАрдд рдХрд░рддрд╛ рд╣реИ? рдЗрд╕рдХреЗ рд▓рд┐рдП рдПрдХ рд░рд╛рдЬреНрдп рдкреНрд░рдмрдВрдзрди рдкреБрд╕реНрддрдХрд╛рд▓рдп рд╣реИредRedux рдкреНрд░рд▓реЗрдЦрди рд╕реЗ рдПрдХ рдЯреВрдбреВ-рд╕реВрдЪреА рдХрд╛ рдПрдХ рдЙрджрд╛рд╣рд░рдгрд╣рд╛рдВ, рдпрд╣ рд╣реИ рдХрд┐ рдХреИрд╕реЗ рдЖрд╡реЗрджрди рдЕрдзрд┐рдХ рд╕рдордЭ рдореЗрдВ рдЖ рдЧрдпрд╛ рд╣реИ рдФрд░ рд╕рд░рд▓ (рдирд╣реАрдВ) рд╣реИ ред рдХреНрдпрд╛ рд╣рдо рдирд┐рд░реНрднрд░рддрд╛ рдХреЛ рд╕рд╣реА рджрд┐рд╢рд╛ рдореЗрдВ рдЦреАрдВрдЪрдиреЗ рдХреА рдХреЛрд╢рд┐рд╢ рдХрд░ рд╕рдХрддреЗ рд╣реИрдВ?рд╕реНрд╡рддрдВрддреНрд░ рдирд┐рд░реНрдгрдп
рдЖрдЗрдП, рдЯреВрдбреВ-рд▓рд┐рд╕реНрдЯ рдХреА рд╕рдорд╕реНрдпрд╛ рдХреЛ рдлреНрд░рдВрдЯ-рдПрдВрдб рдХреЗ рд░реВрдк рдореЗрдВ рдирд╣реАрдВ рджреЗрдЦреЗрдВ, рдЕрд░реНрдерд╛рдд, рдпрд╣ рднреВрд▓ рдЬрд╛рдПрдВ рдХрд┐ рд╣рдореЗрдВ рдПрдЪрдЯреАрдПрдордПрд▓ ("рд╡реЗрдм рдПрдХ рд╡рд┐рд╡рд░рдг рд╣реИ") рдЖрдХрд░реНрд╖рд┐рдд рдХрд░рдирд╛ рд╣реЛрдЧрд╛ред рд╣рдо рдЕрдкрдиреА рдЖрдВрдЦреЛрдВ рд╕реЗ рдкрд░рд┐рдгрд╛рдо рдХреА рдЬрд╛рдВрдЪ рдирд╣реАрдВ рдХрд░ рдкрд╛рдПрдВрдЧреЗ, рдЗрд╕рд▓рд┐рдП рд╣рдореЗрдВ рдкрд░реАрдХреНрд╖рдг рд▓рд┐рдЦрдирд╛ рд╣реЛрдЧрд╛ (рдЬреИрд╕рд╛ рдХрд┐ рдЕрдВрдХрд▓ рдмреЙрдм рдХрд╣рддреЗ рд╣реИрдВ, "рдЖрдк TDD рдХрд╛ рдЙрдкрдпреЛрдЧ рдХрд░ рд╕рдХрддреЗ рд╣реИрдВ")ред рдФрд░ рдХрд╛рд░реНрдп рдХреНрдпрд╛ рд╣реИ? рдЯреВрдбреВ-рд╕реВрдЪреА рдХреНрдпрд╛ рд╣реИ? рд╣рдо рд▓рд┐рдЦрдиреЗ рдХреА рдХреЛрд╢рд┐рд╢ рдХрд░ рд░рд╣реЗ рд╣реИрдВредTodo.spec.tsimport { Todo } from './Todo';
describe('Todo', () => {
let todo: Todo;
beforeEach(() => {
todo = new Todo('description');
});
it('+getItems() should returns Todo[]', () => {
expect(todo.getTitle()).toBe('description');
});
it('+isCompleted() should returns completion flag', () => {
expect(todo.isCompleted()).toBe(false);
});
it('+toggleCompletion() should invert completion flag', () => {
todo.toggleCompletion();
expect(todo.isCompleted()).toBe(true);
});
});
TodoList.spec.tsimport { TodoList } from './TodoList';
describe('TodoList', () => {
let todoList: TodoList;
beforeEach(() => {
todoList = new TodoList();
});
it('+getItems() should returns Todo[]', () => {
expect(todoList.getItems()).toEqual([]);
});
it('+add() should create item and add to collection', () => {
todoList.add('Write tests');
expect(todoList.getItems()).toHaveLength(1);
});
it('+add() should create item with the description', () => {
const description = 'Write tests';
todoList.add(description);
const [item] = todoList.getItems();
expect(item.getTitle()).toBe(description);
});
it('+getCompletedItems() should not returns uncompleted Todo[]', () => {
const description = 'Write tests';
todoList.add(description);
expect(todoList.getCompletedItems()).toEqual([]);
});
it('+getCompletedItems() should returns completed Todo[]', () => {
const description = 'Write tests';
todoList.add(description);
const [item] = todoList.getItems();
item.toggleCompletion();
expect(todoList.getCompletedItems()).toEqual([item]);
});
it('+getUncompletedItems() should returns uncompleted Todo[]', () => {
const description = 'Write tests';
todoList.add(description);
const [item] = todoList.getItems();
expect(todoList.getUncompletedItems()).toEqual([item]);
});
it('+getUncompletedItems() should not returns completed Todo[]', () => {
const description = 'Write tests';
todoList.add(description);
const [item] = todoList.getItems();
item.toggleCompletion();
expect(todoList.getUncompletedItems()).toEqual([]);
});
});
export class Todo {
private completed: boolean = false;
constructor(private description: string) {}
getTitle(): string {
return this.description;
}
isCompleted(): boolean {
return this.completed;
}
toggleCompletion(): void {
this.completed = !this.completed;
}
}
import { Todo } from './Todo';
export class TodoList {
private items: Todo[] = [];
getItems(): Todo[] {
return this.items;
}
getCompletedItems(): Todo[] {
return this.items.filter((todo) => todo.isCompleted());
}
getUncompletedItems(): Todo[] {
return this.items.filter((todo) => !todo.isCompleted());
}
add(description: string): void {
this.items.push(new Todo(description));
}
}
рд╣рдореЗрдВ рд╕реВрдЪрдирд╛рддреНрдордХ рдЗрдВрдЯрд░рдлреЗрд╕ рдХреЗ рд╕рд╛рде рджреЛ рд╕рд░рд▓ рдХрдХреНрд╖рд╛рдПрдВ рдорд┐рд▓рддреА рд╣реИрдВред рдпрд╣реА рдмрд╛рдд рд╣реИ рди? рдЯреЗрд╕реНрдЯ рдкрд╛рд╕ рдХрд░рддреЗ рд╣реИрдВред рдЕрдм рд░рд┐рдПрдХреНрдЯ рдЙрдард╛рдПрдВредimport React from 'react';
import { TodoList } from './core/TodoList';
export class App extends React.Component {
todoList: TodoList = this.createTodoList();
render(): any {
return (
<React.Fragment>
<header>
<h1>Todo List App</h1>
</header>
<main>
<TodoListCmp todoList={this.todoList}></TodoListCmp>
<AddTodoCmp todoList={this.todoList}></AddTodoCmp>
</main>
</React.Fragment>
);
}
private createTodoList(): TodoList {
const todoList = new TodoList();
todoList.add('Initial created Todo');
return todoList;
}
}
export const TodoListCmp: React.FC<{ todoList: TodoList }> = ({ todoList }) => {
return (
<div>
<h2>What to do?</h2>
<ul>
{todoList.getItems().map((todo) => (
<li key={todo.getTitle()}>{todo.getTitle()}</li>
))}
</ul>
</div>
);
};
export const AddTodoCmp: React.FC<{ todoList: TodoList }> = ({ todoList }) => {
return <button onClick={() => todoList.add(`Todo ${todoList.getItems().length}`)}>Add</button>;
};
рдФрд░ рд╕реБрдирд┐рд╢реНрдЪрд┐рдд рдХрд░реЗрдВ рдХрд┐ ... рдХреЛрдИ рдЖрдЗрдЯрдо рдЬреЛрдбрд╝рдирд╛ рдХрд╛рдо рдирд╣реАрдВ рдХрд░рддрд╛ рд╣реИред рд╣рдореНрдо ... рдЕрдм рдпрд╣ рд╕реНрдкрд╖реНрдЯ рд╣реИ рдХрд┐ рд░рд╛рдЬреНрдп рдореЗрдВ рд╕рдм рдХреБрдЫ рдХреНрдпреЛрдВ рд▓рд┐рдЦрд╛ рдЬрд╛рдирд╛ рдЪрд╛рд╣рд┐рдП - рддрд╛рдХрд┐ рдкрд░рд┐рд╡рд░реНрддрдиреЛрдВ рдХреЗ рдмрд╛рд░реЗ рдореЗрдВ рдЬрд╛рдирдиреЗ рдХреЗ рдмрд╛рдж рд░рд┐рдПрдХреНрдЯ рдШрдЯрдХ рдлрд┐рд░ рд╕реЗ рддреИрдпрд╛рд░ рд╣реЛ рдЬрд╛рдПред рд▓реЗрдХрд┐рди рдХреНрдпрд╛ рдпрд╣ рд╕рднреА рд╕рдВрднрд╛рд╡рд┐рдд рд╕рд┐рджреНрдзрд╛рдВрддреЛрдВ рдХрд╛ рдЙрд▓реНрд▓рдВрдШрди рдХрд░рдиреЗ рдФрд░ рджреГрд╢реНрдп рдШрдЯрдХ рдореЗрдВ рддрд░реНрдХ рд░рдЦрдиреЗ рдХрд╛ рдПрдХ рдХрд╛рд░рдг рд╣реИ? рдереЛрдбрд╝рд╛ рдзреИрд░реНрдп рдФрд░ рд╕рд╛рд╣рд╕ред рд╕рдорд╕реНрдпрд╛ рдХреЛ рд╣рд▓ рдХрд░рдиреЗ рдХреЗ рд▓рд┐рдП, рдПрдХ рдЕрдирдВрдд рд▓реВрдк рдпрд╛ рдСрдмреНрдЬрд░реНрд╡рд░ рдкреИрдЯрд░реНрди рдореЗрдВ рдлреЛрд░реНрд╕рдЕрдкрдбреЗрдЯ () рдХреЙрд▓рд┐рдВрдЧ рдПрдХрджрдо рд╕рд╣реА рд╣реИ редрдореБрдЭреЗ рдЖрд░рдПрдХреНрд╕рдЬреЗ рд▓рд╛рдЗрдмреНрд░реЗрд░реА рдкрд╕рдВрдж рд╣реИ, рд▓реЗрдХрд┐рди рдореИрдВ рдЗрд╕реЗ рдХрдиреЗрдХреНрдЯ рдирд╣реАрдВ рдХрд░реВрдВрдЧрд╛, рд▓реЗрдХрд┐рди рд╣рдорд╛рд░реЗ рдХрд╛рдо рдХреЗ рд▓рд┐рдП рдЬрд░реВрд░реА рдЗрд╕рдХреА рдПрдкреАрдЖрдИ рдХреЙрдкреА рдХрд░реЗрдВредObservable.spec.tsimport { Observable, Subject } from './Observable';
describe('Observable', () => {
let subject: Subject<any>;
let observable: Observable<any>;
beforeEach(() => {
subject = new Subject();
observable = subject.asObservable();
});
it('should call callback on next value', async () => {
const spy = jasmine.createSpy();
observable.subscribe(spy);
subject.next({});
await delay();
expect(spy).toHaveBeenCalled();
});
it('should not call callback on next value if unsubscribed', async () => {
const spy = jasmine.createSpy();
const subscription = observable.subscribe(spy);
subscription.unsubscribe();
subject.next({});
await delay();
expect(spy).not.toHaveBeenCalled();
});
it('should send to callback subject.next value', async () => {
const spy = jasmine.createSpy();
observable.subscribe(spy);
const sendingValue = {};
subject.next(sendingValue);
await delay();
expect(spy.calls.first().args[0]).toBe(sendingValue);
});
});
function delay(timeoutInMs?: number): Promise<void> {
return new Promise((resolve) => setTimeout(resolve, timeoutInMs));
}
Observable.tsexport interface Observable<T = unknown> {
subscribe(onNext: (value: T) => void): Subscription;
}
export interface Subscription {
unsubscribe(): void;
}
export class Subject<T = unknown> implements Observable<T> {
protected callbackSet: Set<(value: T) => void> = new Set();
asObservable(): Observable<T> {
return this;
}
subscribe(onNext: (value: T) => void): Subscription {
this.callbackSet.add(onNext);
return { unsubscribe: () => this.callbackSet.delete(onNext) };
}
next(value: T): void {
Promise.resolve().then(() => this.callbackSet.forEach((onNext) => onNext(value)));
}
}
рдореЗрд░реА рд░рд╛рдп рдореЗрдВ, рдХреБрдЫ рднреА рдЬрдЯрд┐рд▓ рдирд╣реАрдВ рд╣реИред рдПрдХ рдкрд░реАрдХреНрд╖рдг рдЬреЛрдбрд╝реЗрдВ (рдкрд░рд┐рд╡рд░реНрддрдиреЛрдВ рдХреА рд╕реВрдЪрдирд╛ - рд╡рд╣ рддрд░реНрдХ рд╣реИ)ред it('+TodoList.prototype.add() should emit changes', async () => {
const spy = jasmine.createSpy();
todoList.changes.subscribe(spy);
todoList.add('description');
await delay();
expect(spy).toHaveBeenCalled();
});
рдЖрдЗрдП рдПрдХ рдкрд▓ рдХреЗ рд▓рд┐рдП рд╕реЛрдЪреЗрдВ, рд▓реЗрдХрд┐рди рдХреНрдпрд╛ рдПрдХ рдЯреЛрдбреЛ рддрддреНрд╡ рдореЗрдВ рдмрджрд▓рд╛рд╡ рд╕реЗ рдЯреЛрдбреЛрд▓рд┐рд╕реНрдЯ рдХреА рд╕реНрдерд┐рддрд┐ рдкреНрд░рднрд╛рд╡рд┐рдд рд╣реЛрддреА рд╣реИ? рдкреНрд░рднрд╛рд╡рд┐рдд рдХрд░рддрд╛ рд╣реИ - getCompletedItems / getUncompletedItems рд╡рд┐рдзрд┐рдпреЛрдВ рдХреЛ рддрддреНрд╡реЛрдВ рдХрд╛ рдПрдХ рдЕрд▓рдЧ рд╕реЗрдЯ рд╡рд╛рдкрд╕ рдХрд░рдирд╛ рд╣реЛрдЧрд╛ред рд╣реЛ рд╕рдХрддрд╛ рд╣реИ рдХрд┐ рдпрд╣ рдЯреЙрдбрд▓рд┐рд╕реНрдЯ рд╡рд░реНрдЧ рдХреЛ рдЯреЙрдЧрд▓ рдХреЙрдордкреНрд▓реЗрдХреНрд╢рди рдореЗрдВ рд▓реЗ рдЬрд╛рдиреЗ рд▓рд╛рдпрдХ рд╣реЛ? рдпрд╣ рдПрдХ рдмреБрд░рд╛ рд╡рд┐рдЪрд╛рд░ рд╣реИ - рдЗрд╕ рджреГрд╖реНрдЯрд┐рдХреЛрдг рдХреЗ рд╕рд╛рде, рд╣рдореЗрдВ рдПрдХ рдирдпрд╛ рдЯреЛрдбреЛ-рддрддреНрд╡ рдХреЗ рдмрд╛рд░реЗ рдореЗрдВ рд╣рд░ рд╕реБрд╡рд┐рдзрд╛ рдХреЗ рд▓рд┐рдП рдЯреЛрдбрд▓рд┐рд╕реНрдЯ рдХреЛ рдмрдврд╝рд╛рдирд╛ рд╣реЛрдЧрд╛ (рд╣рдо рдЗрд╕ рдкрд░ рдмрд╛рдж рдореЗрдВ рд▓реМрдЯреЗрдВрдЧреЗ)ред рд▓реЗрдХрд┐рди рдкрд░рд┐рд╡рд░реНрддрдиреЛрдВ рдХреЗ рдмрд╛рд░реЗ рдореЗрдВ рдХреИрд╕реЗ рд╕реАрдЦрдирд╛ рд╣реИ, рдлрд┐рд░ рд╕реЗ рдкреНрд░реЗрдХреНрд╖рдХ? рдЪреАрдЬреЛрдВ рдХреЛ рд╕рд░рд▓ рдмрдирд╛рдиреЗ рдХреЗ рд▓рд┐рдП, рдЯреЛрдбреЛ-рддрддреНрд╡ рдХреЛ рдХреЙрд▓рдмреИрдХ рдХреЗ рдорд╛рдзреНрдпрдо рд╕реЗ рдкрд░рд┐рд╡рд░реНрддрдиреЛрдВ рдХрд╛ рд╕рдВрдЪрд╛рд░ рдХрд░рдиреЗ рджреЗрдВредрдХрд╛рд░реНрдпрдХреНрд░рдо рдХрд╛ рдкреВрд░реНрдг рд╕рдВрд╕реНрдХрд░рдг рдЗрд╕ рддрд░рд╣ рджрд┐рдЦрддрд╛ рд╣реИредimport React from 'react';
import { Observable, Subject } from 'src/utils/Observable';
import { generateId } from 'src/utils/generateId';
export class Todo {
private completed: boolean = false;
id: string = generateId();
constructor(private description: string, private onCompletionToggle?: (todo: Todo) => void) {}
getTitle(): string {
return this.description;
}
isCompleted(): boolean {
return this.completed;
}
toggleCompletion(): void {
this.completed = !this.completed;
this.onCompletionToggle?.(this);
}
}
export class TodoList {
private items: Todo[] = [];
private changesSubject = new Subject();
readonly changes: Observable = this.changesSubject.asObservable();
getItems(): Todo[] {
return this.items;
}
getCompletedItems(): Todo[] {
return this.items.filter((todo) => todo.isCompleted());
}
getUncompletedItems(): Todo[] {
return this.items.filter((todo) => !todo.isCompleted());
}
add(description: string): void {
this.items.push(new Todo(description, () => this.changesSubject.next({})));
this.changesSubject.next({});
}
}
export class App extends React.Component {
todoList: TodoList = this.createTodoList();
render(): any {
return (
<React.Fragment>
<header>
<h1>Todo List App</h1>
</header>
<main>
<TodoListCmp todoList={this.todoList}></TodoListCmp>
<AddTodoCmp todoList={this.todoList}></AddTodoCmp>
</main>
</React.Fragment>
);
}
componentDidMount(): void {
this.todoList.changes.subscribe(() => this.forceUpdate());
}
private createTodoList(): TodoList {
const todoList = new TodoList();
todoList.add('Initial created Todo');
return todoList;
}
}
export const TodoListCmp: React.FC<{ todoList: TodoList }> = ({ todoList }) => {
return (
<div>
<h2>What to do?</h2>
<ul>
{todoList.getUncompletedItems().map((todo) => (
<TodoCmp key={todo.id} todo={todo}></TodoCmp>
))}
{todoList.getCompletedItems().map((todo) => (
<TodoCmp key={todo.id} todo={todo}></TodoCmp>
))}
</ul>
</div>
);
};
export const TodoCmp: React.FC<{ todo: Todo }> = ({ todo }) => (
<li
style={{ textDecoration: todo.isCompleted() ? 'line-through' : '' }}
onClick={() => todo.toggleCompletion()}
>
{todo.getTitle()}
</li>
);
export const AddTodoCmp: React.FC<{ todoList: TodoList }> = ({ todoList }) => {
return <button onClick={() => todoList.add(`Todo ${todoList.getItems().length}`)}>Add</button>;
};
рдРрд╕рд╛ рдкреНрд░рддреАрдд рд╣реЛрддрд╛ рд╣реИ рдХрд┐ рдлреНрд░реЗрдорд╡рд░реНрдХ рд╕реЗ рд╕реНрд╡рддрдВрддреНрд░ рдЯреВрдбреВ-рд╕реВрдЪреА рдПрдкреНрд▓рд┐рдХреЗрд╢рди рдХреЛ рдХреНрдпрд╛ рджреЗрдЦрдирд╛ рдЪрд╛рд╣рд┐рдПред рдХреЗрд╡рд▓ рд╕реАрдорд╛ рдкреАрдПрд▓ рд╣реИред рдЖрдк рдХрдВрд╕реЛрд▓ рдХреЗ рд▓рд┐рдП рдкреНрд░рджрд░реНрд╢рди рдХреЛ рд▓рд╛рдЧреВ рдХрд░ рд╕рдХрддреЗ рд╣реИрдВ, рдпрд╛ рдХреЛрдгреАрдп рдХрд╛ рдЙрдкрдпреЛрдЧ рдХрд░ рд╕рдХрддреЗ рд╣реИрдВредрд╢рд╛рдпрдж рдЕрдиреБрдкреНрд░рдпреЛрдЧ рдХрд╛ рд╡рд░реНрддрдорд╛рди рд╕рдВрд╕реНрдХрд░рдг рдЗрддрдирд╛ рдЬрдЯрд┐рд▓ рдирд╣реАрдВ рд╣реИ рдХрд┐ рдпрд╣ рд╕реБрдирд┐рд╢реНрдЪрд┐рдд рдХрд░ рд╕рдХреЗ рдХрд┐ рдПрдХ рд╕реНрд╡рддрдВрддреНрд░ рджреГрд╖реНрдЯрд┐рдХреЛрдг рдХрд╛рдо рдХрд░ рд░рд╣рд╛ рд╣реИ рдФрд░ рдЕрдкрдиреА рддрд╛рдХрдд рдХрд╛ рдкреНрд░рджрд░реНрд╢рди рдХрд░ рд░рд╣рд╛ рд╣реИред рдЗрд╕рд▓рд┐рдП, рд╣рдо рдЕрдкрдиреА рдХрд▓реНрдкрдирд╛ рдХреЛ рдЕрдзрд┐рдХ рдпрд╛ рдХрдо рдкреНрд░рд╢рдВрд╕рдиреАрдп рдЯреВрдбреВ-рд╕реВрдЪреА рд╡рд┐рдХрд╛рд╕ рдкрд░рд┐рджреГрд╢реНрдп рд╕реЗ рдЬреЛрдбрд╝рдиреЗ рдХреЗ рд▓рд┐рдП рдЬреЛрдбрд╝рддреЗ рд╣реИрдВредрдЧреНрд░рд╛рд╣рдХ рд╕реЗ рд╕рдВрдкрд╛рджрди
рдЕрдзрд┐рдХрд╛рдВрд╢ рдкрд░рд┐рдпреЛрдЬрдирд╛рдУрдВ рдХрд╛ рдореБрдЦреНрдп рджреБрдГрд╕реНрд╡рдкреНрди рдЖрд╡рд╢реНрдпрдХрддрд╛рдУрдВ рдХреЛ рдмрджрд▓ рд░рд╣рд╛ рд╣реИред рдЖрдк рдЬрд╛рдирддреЗ рд╣реИрдВ рдХрд┐ рд╕рдВрдкрд╛рджрди рд╕реЗ рдмрдЪрд╛ рдирд╣реАрдВ рдЬрд╛ рд╕рдХрддрд╛ рд╣реИ, рдФрд░ рдЖрдк рдЬрд╛рдирддреЗ рд╣реИрдВ рдХрд┐ рдпрд╣ рд╕рд╛рдорд╛рдиреНрдп рд╣реИред рд▓реЗрдХрд┐рди рдЖрдк рднрд╡рд┐рд╖реНрдп рдХреЗ рдмрджрд▓рд╛рд╡реЛрдВ рдХреЗ рд▓рд┐рдП рдХреИрд╕реЗ рддреИрдпрд╛рд░ рд╣реЛрддреЗ рд╣реИрдВ?рд╡рд┐рд╢реЗрд╖ рдЯреЛрдбреЛ рддрддреНрд╡
OOP рдХреА рдкреНрд░рдореБрдЦ рд╡рд┐рд╢реЗрд╖рддрд╛рдУрдВ рдореЗрдВ рд╕реЗ рдПрдХ рдирдП рдкреНрд░рдХрд╛рд░ рдХреА рд╢реБрд░реВрдЖрдд рдХреЗ рдорд╛рдзреНрдпрдо рд╕реЗ рд╕рдорд╕реНрдпрд╛ рдХреЛ рд╣рд▓ рдХрд░рдиреЗ рдХреА рдХреНрд╖рдорддрд╛ рд╣реИред рд╢рд╛рдпрдж рдпрд╣ рд╕рдмрд╕реЗ рд╢рдХреНрддрд┐рд╢рд╛рд▓реА рдУрдУрдкреА рддрдХрдиреАрдХ рд╣реИ рдЬреЛ рдПрдХ рдЬрдЯрд┐рд▓ рдФрд░ рдмреЛрдЭрд┐рд▓ рдХрд╛рд░реНрдпрдХреНрд░рдо рдХреЛ рдПрдХрд▓ рд░реВрдк рд╕реЗ рдмрд╛рд╣рд░ рдирд┐рдХрд╛рд▓ рд╕рдХрддреА рд╣реИред рдЙрджрд╛рд╣рд░рдг рдХреЗ рд▓рд┐рдП, рдореБрдЭреЗ рдирд╣реАрдВ рдкрддрд╛ рдХрд┐ рдЯреЛрдбреЛ рддрддреНрд╡ рдХреА рдХреНрдпрд╛ рдЖрд╡рд╢реНрдпрдХрддрд╛ рд╣реИред рдЗрд╕рдХрд╛ рдирд╛рдо рдмрджрд▓рдиреЗ рдореЗрдВ рд╕рдХреНрд╖рдо рд╣реЛрдирд╛ рдЖрд╡рд╢реНрдпрдХ рд╣реЛ рд╕рдХрддрд╛ рд╣реИ, рдЕрддрд┐рд░рд┐рдХреНрдд рд╡рд┐рд╢реЗрд╖рддрд╛рдУрдВ рдХреЛ рдЬреЛрдбрд╝рдирд╛ рдЖрд╡рд╢реНрдпрдХ рд╣реЛ рд╕рдХрддрд╛ рд╣реИ, рдЗрд╕ рддрддреНрд╡ рдХреЛ рд╕реНрдкреЗрд╕рдПрдХреНрд╕ рд╕рд░реНрд╡рд░ рддрдХ рд╕реАрдзреЗ рдкрд╣реБрдВрдЪ рдХреЗ рдорд╛рдзреНрдпрдо рд╕реЗ рдмрджрд▓рдирд╛ рд╕рдВрднрд╡ рд╣реЛ рд╕рдХрддрд╛ рд╣реИ ... рд▓реЗрдХрд┐рди рдореБрдЭреЗ рдпрдХреАрди рд╣реИ рдХрд┐ рдЖрд╡рд╢реНрдпрдХрддрд╛рдУрдВ рдореЗрдВ рдмрджрд▓рд╛рд╡ рд╣реЛрдЧрд╛, рдФрд░ рдореБрдЭреЗ рд╡рд┐рднрд┐рдиреНрди рдкреНрд░рдХрд╛рд░ рдХреЗ рдЯреЛрдбреЛ рдХреА рдЖрд╡рд╢реНрдпрдХрддрд╛ рд╣реЛрдЧреАредexport class EditableTodo extends Todo {
changeTitle(title: string): void {
this.title = title;
this.onChange?.(this);
}
}
рдРрд╕рд╛ рд▓рдЧрддрд╛ рд╣реИ рдХрд┐ рдПрдХ рд╡рд┐рд╢реЗрд╖ рдкреНрд░рдХрд╛рд░ рдХреЛ рдкреНрд░рджрд░реНрд╢рд┐рдд рдХрд░рдиреЗ рдХреЗ рд▓рд┐рдП, рд╣рдореЗрдВ рджреГрд╢реНрдп рдШрдЯрдХреЛрдВ рдХреЛ рднреА рдмрджрд▓рдирд╛ рд╣реЛрдЧрд╛ред рд╡реНрдпрд╡рд╣рд╛рд░ рдореЗрдВ, рдореБрдЭреЗ рдорд┐рд▓реЗ (рдФрд░ рд▓рд┐рдЦреЗ рдЧрдП) рдШрдЯрдХ рдЬрд┐рд╕рдореЗрдВ рдПрдХ рд▓рд╛рдЦ рдЕрд▓рдЧ-рдЕрд▓рдЧ рд╕реНрдерд┐рддрд┐рдпрд╛рдВ рдЬрд┐рд░рд╛рдл рд╕реЗ рдПрдХ рдорд╢реАрди рдмреНрд▓реЙрдХ рдореЗрдВ рдПрдХ рдбрд┐рд╡ рдмреНрд▓реЙрдХ рдХреЛ рдмрджрд▓ рджреЗрддреА рд╣реИрдВред рдЗрд╕ рд╕рдорд╕реНрдпрд╛ рд╕реЗ рдмрдЪрдиреЗ рдХреЗ рд▓рд┐рдП, рдЖрдк рдПрдХ рд╡рд┐рд╢рд╛рд▓ рд╕реНрд╡рд┐рдЪ-рдХреЗрд╕ рд╕реВрдЪреА рдХреЗ рд╕рд╛рде рдПрдХ рдШрдЯрдХ рдмрдирд╛ рд╕рдХрддреЗ рд╣реИрдВред рдпрд╛ рд╡рд┐рдЬрд╝рд┐рдЯрд░ рдкреИрдЯрд░реНрди рдФрд░ рджреЛрд╣рд░реА рдкреНрд░реЗрд╖рдг рд▓рд╛рдЧреВ рдХрд░реЗрдВ , рдФрд░ рдЯреЛрдбреЛ рддрддреНрд╡ рдХреЛ рдЦреБрдж рддрдп рдХрд░рдиреЗ рджреЗрдВ рдХрд┐ рдХрд┐рд╕ рдкреНрд░рдХрд╛рд░ рдХреЗ рдШрдЯрдХ рдХреЛ рдЖрдХрд░реНрд╖рд┐рдд рдХрд░рдирд╛ рд╣реИредexport class Todo {
id: string = '';
constructor(
protected title: string,
private completed: boolean = false,
protected onChange?: (todo: Todo) => void,
) {}
getTitle(): string {
return this.title;
}
isCompleted(): boolean {
return this.completed;
}
toggleCompletion(): void {
this.completed = !this.completed;
this.onChange?.(this);
}
render(renderer: TodoRenderer): any {
return renderer.renderSimpleTodo(this);
}
}
export class EditableTodo extends Todo {
changeTitle(title: string): void {
this.title = title;
this.onChange?.(this);
}
render(renderer: TodoRenderer): any {
return renderer.renderEditableTodo(this);
}
}
export class TodoRenderer {
renderSimpleTodo(todo: Todo): any {
return <SimpleTodoCmp todo={todo}></SimpleTodoCmp>;
}
renderFixedTodo(todo: Todo): any {
return <FixedTodoCmp todo={todo}></FixedTodoCmp>;
}
renderEditableTodo(todo: EditableTodo): any {
return <EditableTodoCmp todo={todo}></EditableTodoCmp>;
}
}
рджреЛрд╣рд░реА рдкреНрд░реЗрд╖рдг рд╡рд┐рдХрд▓реНрдк рд╡рд┐рд╢реЗрд╖ рд░реВрдк рд╕реЗ рдЙрдкрдпреЛрдЧреА рд╣реЛрддрд╛ рд╣реИ рдЬрдм рдПрдХ рдкреНрд░рдХрд╛рд░ рдХреЗ рдЖрдЗрдЯрдо рдореЗрдВ рдЕрд▓рдЧ-рдЕрд▓рдЧ рд╡рд┐рдЪрд╛рд░ рд╣реЛрддреЗ рд╣реИрдВред рдЖрдк рдЙрдиреНрд╣реЗрдВ рдЕрд▓рдЧ-рдЕрд▓рдЧ рдЯреЛрдбрд░реЗрдВрдбрд░ рдХреЛ рд░реЗрдВрдбрд░ рд╡рд┐рдзрд┐ рдореЗрдВ рдмрджрд▓рдХрд░ рдмрджрд▓ рд╕рдХрддреЗ рд╣реИрдВредрдЕрдм рд╣рдо рддреИрдпрд╛рд░ рд╣реИрдВред "рд╡рд┐рд╢реЗрд╖" рдЯреЛрдбреЛ рддрддреНрд╡реЛрдВ рдХреЗ рд▓рд┐рдП рдирдИ рдорд╛рдВрдЧреЛрдВ рдХрд╛ рдбрд░ рдЧрд╛рдпрдм рд╣реЛ рдЧрдпрд╛ рд╣реИред рдореБрдЭреЗ рд▓рдЧрддрд╛ рд╣реИ рдХрд┐ рдбреЗрд╡рд▓рдкрд░реНрд╕ рдЦреБрдж рдкрд╣рд▓ рдХрд░ рд╕рдХрддреЗ рд╣реИрдВ, рдФрд░ рдХреБрдЫ рдРрд╕реЗ рдлреАрдЪрд░реНрд╕ рдкреЗрд╢ рдХрд░ рд╕рдХрддреЗ рд╣реИрдВ рдЬрд┐рдирдХреЗ рд▓рд┐рдП рдирдП рдкреНрд░рдХрд╛рд░реЛрдВ рдХреА рд╢реБрд░реБрдЖрдд рдХреА рдЖрд╡рд╢реНрдпрдХрддрд╛ рд╣реЛрддреА рд╣реИ, рдЬреЛ рдЕрдм рдирдП рдХреЛрдб рдФрд░ рдореМрдЬреВрджрд╛ рдореЗрдВ рдиреНрдпреВрдирддрдо рдкрд░рд┐рд╡рд░реНрддрди рд▓рд┐рдЦрдХрд░ рдЬреЛрдбрд╝реЗ рдЬрд╛рддреЗ рд╣реИрдВредрд╕рд░реНрд╡рд░ рдкрд░ рдбреЗрдЯрд╛ рдХреА рдмрдЪрдд
рд╕рд░реНрд╡рд░ рдХреЗ рд╕рд╛рде рдмрд╛рддрдЪреАрдд рдХреЗ рдмрд┐рдирд╛ рдХрд┐рд╕ рддрд░рд╣ рдХрд╛ рдЕрдиреБрдкреНрд░рдпреЛрдЧ рд╣реИ? рдмреЗрд╢рдХ, рдЖрдкрдХреЛ HTTP рдХреЗ рдорд╛рдзреНрдпрдо рд╕реЗ рд╣рдорд╛рд░реА рд╕реВрдЪреА рдХреЛ рдмрдЪрд╛рдиреЗ рдореЗрдВ рд╕рдХреНрд╖рдо рд╣реЛрдирд╛ рдЪрд╛рд╣рд┐рдП - рдПрдХ рдФрд░ рдирдИ рдЖрд╡рд╢реНрдпрдХрддрд╛ред рд╣рдо рд╕рдорд╕реНрдпрд╛ рдХреЛ рд╣рд▓ рдХрд░рдиреЗ рдХреА рдХреЛрд╢рд┐рд╢ рдХрд░ рд░рд╣реЗ рд╣реИрдВредAppTodoList.spec.tsimport { delay } from 'src/utils/delay';
import { TodoType } from '../core/TodoFactory';
import { AppTodoList } from './AppTodoList';
describe('AppTodoList', () => {
let todoList: AppTodoList;
beforeEach(() => {
todoList = new AppTodoList({
getItems: async () => [{ type: TodoType.Simple, title: 'Loaded todo', completed: false }],
save: () => delay(),
});
});
it('+resolve() should load saved todo items', async () => {
await todoList.resolve();
expect(todoList.getItems().length).toBeGreaterThan(0);
});
it('+resolve() should emit changes', async () => {
const spy = jasmine.createSpy();
todoList.changes.subscribe(spy);
await todoList.resolve();
await delay(1);
expect(spy).toHaveBeenCalled();
});
it('+add() should emit changes', async () => {
const spy = jasmine.createSpy();
todoList.changes.subscribe(spy);
todoList.add({ title: '' });
await delay(1);
expect(spy).toHaveBeenCalled();
});
it('+add() should emit changes after resolve()', async () => {
const spy = jasmine.createSpy();
todoList.changes.subscribe(spy);
await todoList.resolve();
todoList.add({ title: '' });
await delay(1);
expect(spy).toHaveBeenCalledTimes(2);
});
it('+todo.onChange() should emit changes', async () => {
await todoList.resolve();
await delay(1);
const spy = jasmine.createSpy();
todoList.changes.subscribe(spy);
const [todo] = todoList.getItems();
todo.toggleCompletion();
await delay(1);
expect(spy).toHaveBeenCalled();
});
it('+add() should call TodoListApi.save', async () => {
const spy = jasmine.createSpy();
todoList = new AppTodoList({ getItems: async () => [], save: async () => spy() });
todoList.add({ title: '' });
await delay();
expect(spy).toHaveBeenCalled();
});
it('+todo.onChange() should call TodoListApi.save', async () => {
const spy = jasmine.createSpy();
todoList = new AppTodoList({ getItems: async () => [{ title: '' }], save: async () => spy() });
await todoList.resolve();
const [todo] = todoList.getItems();
todo.toggleCompletion();
await delay();
expect(spy).toHaveBeenCalled();
});
it('+add() should save todoList state on success and rollback to it on error', async () => {
const api = { getItems: async () => [], save: () => Promise.resolve() };
todoList = new AppTodoList(api);
todoList.add({ title: '1' });
await delay(1);
const savedData = JSON.stringify(todoList.getItems());
api.save = () => Promise.reject('Mock saving failed');
todoList.add({ title: '2' });
expect(todoList.getItems()).toHaveLength(2);
await delay(1);
expect(JSON.stringify(todoList.getItems())).toBe(savedData);
});
});
export interface TodoListApi {
getItems(): Promise<TodoParams[]>;
save(todoParamsList: TodoParams[]): Promise<void>;
}
export class AppTodoList implements TodoList {
private todoFactory = new TodoFactory();
private changesSubject = new Subject();
changes: Observable = this.changesSubject.asObservable();
private state: TodoList = new TodoListImp();
private subscription: Subscription = this.state.changes.subscribe(() => this.onStateChanges());
private synchronizedTodoParamsList: TodoParams[] = [];
constructor(private api: TodoListApi) {}
async resolve(): Promise<void> {
const todoParamsList = await this.api.getItems();
this.updateState(todoParamsList);
}
private updateState(todoParamsList: TodoParams[]): void {
const todoList = new TodoListImp(todoParamsList);
this.state = todoList;
this.subscription.unsubscribe();
this.subscription = todoList.changes.subscribe(() => this.onStateChanges());
this.synchronizedTodoParamsList = todoParamsList;
this.changesSubject.next({});
}
private async onStateChanges(): Promise<void> {
this.changesSubject.next({});
try {
const params = this.state.getItems().map((todo) => this.todoFactory.serializeTodo(todo));
await this.api.save(params);
this.synchronizedTodoParamsList = params;
} catch {
this.updateState(this.synchronizedTodoParamsList);
}
}
destroy(): void {
this.subscription.unsubscribe();
}
getItems(): Todo[] {
return this.state.getItems();
}
getCompletedItems(): Todo[] {
return this.state.getCompletedItems();
}
getUncompletedItems(): Todo[] {
return this.state.getUncompletedItems();
}
add(todoParams: TodoParams): void {
this.state.add(todoParams);
}
}
рд╣рдореЗрдВ рдирд╣реАрдВ рдкрддрд╛ рдХрд┐ рдПрдкреНрд▓рд┐рдХреЗрд╢рди рдХреЛ рдХреИрд╕реЗ рд╡реНрдпрд╡рд╣рд╛рд░ рдХрд░рдирд╛ рдЪрд╛рд╣рд┐рдПред рд╕рдлрд▓ рд╣реЛрдиреЗ рдФрд░ рдкрд░рд┐рд╡рд░реНрддрдиреЛрдВ рдХреЛ рдкреНрд░рджрд░реНрд╢рд┐рдд рдХрд░рдиреЗ рдХреЗ рд▓рд┐рдП рд╕рд╣реЗрдЬреЗрдВ рдХреА рдкреНрд░рддреАрдХреНрд╖рд╛ рдХрд░реЗрдВ? рдкрд░рд┐рд╡рд░реНрддрдиреЛрдВ рдХреЛ рдкреНрд░рджрд░реНрд╢рд┐рдд рдХрд░рдиреЗ рдХреА рдЕрдиреБрдорддрд┐ рджреЗрдВ, рдФрд░ рддреНрд░реБрдЯрд┐ рдХреЗ рдорд╛рдорд▓реЗ рдореЗрдВ, рд╕рд┐рдВрдХреНрд░рдирд╛рдЗрдЬрд╝ рд░рд╛рдЬреНрдп рдореЗрдВ рд╡рд╛рдкрд╕ рдЬрд╛рдПрдВ? рдпрд╛ рддреНрд░реБрдЯрд┐рдпреЛрдВ рдХреЛ рдкреВрд░реА рддрд░рд╣ рд╕реЗ рдЕрдирджреЗрдЦрд╛ рдХрд░реЗрдВ? рд╕рдмрд╕реЗ рдЕрдзрд┐рдХ рд╕рдВрднрд╛рд╡рдирд╛ рд╣реИ, рдЧреНрд░рд╛рд╣рдХ рдХреЛ рднреА рдпрд╣ рдкрддрд╛ рдирд╣реАрдВ рд╣реИред рдЗрд╕рд▓рд┐рдП, рдЖрд╡рд╢реНрдпрдХрддрд╛рдУрдВ рдореЗрдВ рдкрд░рд┐рд╡рд░реНрддрди рдЕрдкрд░рд┐рд╣рд╛рд░реНрдп рд╣реИ, рд▓реЗрдХрд┐рди рдЙрдиреНрд╣реЗрдВ рд╕рдВрд░рдХреНрд╖рдг рдХреЗ рд▓рд┐рдП рдЬрд┐рдореНрдореЗрджрд╛рд░ рдХреЗрд╡рд▓ рдПрдХ рд╡рд░реНрдЧ рдХреЛ рдкреНрд░рднрд╛рд╡рд┐рдд рдХрд░рдирд╛ рдЪрд╛рд╣рд┐рдПред рдФрд░ рд░рд╛рд╕реНрддреЗ рдореЗрдВ рдЕрдЧрд▓рд╛ рдПрдбрд┐рдЯредрдЗрддрд┐рд╣рд╛рд╕ рдмрджрд▓реЗрдВ
"рд╣рдореЗрдВ рдХрд╛рд░реНрд░рд╡рд╛рдИ / рд░рджреНрдж рдХрд░рдиреЗ рдХреА рдХреНрд╖рдорддрд╛ рдХреА рдЖрд╡рд╢реНрдпрдХрддрд╛ рд╣реИ" ...рдРрд╕рд╛ рд▓рдЧрддрд╛ рд╣реИ рдХрд┐ рдирдП рд╕рдВрдкрд╛рджрди рдХреА рдПрдХ рд▓рд╣рд░ рд╣рдореЗрдВ рдЖрд╢реНрдЪрд░реНрдпрдЪрдХрд┐рдд рдХрд░рддреА рд╣реИред рд▓реЗрдХрд┐рди рдХрд┐рд╕реА рднреА рдорд╛рдорд▓реЗ рдореЗрдВ, рдЖрдк рдкрд░реАрдХреНрд╖рдгреЛрдВ рдХрд╛ рддреНрдпрд╛рдЧ рдирд╣реАрдВ рдХрд░ рд╕рдХрддреЗред рдЕрдм рдпрд╣ рдкреВрд░реА рддрд░рд╣ рд╕реЗ рд╕реНрдкрд╖реНрдЯ рдирд╣реАрдВ рд╣реИ рдХрд┐ рд╡рдВрд╢рд╛рдиреБрдХреНрд░рдо рдкрджрд╛рдиреБрдХреНрд░рдо рдмреЗрд╣рддрд░ рдЕрдиреБрдХреВрд▓ рд╣реИ, рдФрд░ рдХреНрдпрд╛ рд╡рд┐рд░рд╛рд╕рдд рдЖрдорддреМрд░ рдкрд░ рдЙрдкрдпреБрдХреНрдд рд╣реИред рдЗрд╕рд▓рд┐рдП, рдХреБрдЫ рднреА рдмреБрд░рд╛ рдирд╣реАрдВ рд╣реЛрдЧрд╛ рдпрджрд┐ рд╣рдо рдХреЗрд╡рд▓ рд╣рдорд╛рд░реЗ рдЧрдВрджреЗ рд╡рд░реНрдЧ рдХреЛ рдкреВрд░рдХ рдХрд░рддреЗ рд╣реИрдВ (рд╣рдо рдЗрд╕реЗ рдХрд╛рд░реНрдпрд╛рдиреНрд╡рдпрди рд╡рд┐рд╡рд░рдг рдкрд░ рд╡рд┐рдЪрд╛рд░ рдХрд░реЗрдВрдЧреЗ), рдПрдХрдорд╛рддреНрд░ рдЬрд┐рдореНрдореЗрджрд╛рд░реА рдХреЗ рд╕рд┐рджреНрдзрд╛рдВрдд рдХрд╛ рддреНрдпрд╛рдЧ рдХрд░рддреЗ рд╣реБрдПредTodoListHistory.spec.tsimport { TodoParams } from 'src/core/TodoFactory';
import { delay } from 'src/utils/delay';
import { TodoListHistory } from './TodoListHistory';
describe('TodoListHistory', () => {
let history: TodoListHistory;
beforeEach(() => {
history = new TodoListHistory();
});
it('+getState() should returns TodoParams[]', () => {
expect(history.getState()).toEqual([]);
});
it('+setState() should rewrite current state', () => {
const newState = [{ title: '' }] as TodoParams[];
history.setState(newState);
expect(history.getState()).toBe(newState);
});
it('+hasPrev() should returns false on init', () => {
expect(history.hasPrev()).toBe(false);
});
it('+hasPrev() should returns true after setState()', () => {
history.setState([]);
expect(history.hasPrev()).toBe(true);
});
it('+switchToPrev() should switch on prev state', () => {
const prevState = [{ title: '' }] as TodoParams[];
history.setState(prevState);
history.setState([]);
history.switchToPrev();
expect(history.getState()).toBe(prevState);
});
it('+hasPrev() should returns false after switch to first', () => {
history.setState([]);
history.switchToPrev();
expect(history.hasPrev()).toBe(false);
});
it('+hasNext() should returns false on init', () => {
expect(history.hasNext()).toBe(false);
});
it('+hasNext() should returns true after switchToPrev()', () => {
history.setState([]);
history.switchToPrev();
expect(history.hasNext()).toBe(true);
});
it('+switchToNext() should switch on next state', () => {
const prevState = [{ title: '' }] as TodoParams[];
history.setState([]);
history.setState(prevState);
history.switchToPrev();
history.switchToNext();
expect(history.getState()).toBe(prevState);
});
it('+hasNext() should returns false after switchToNext()', () => {
history.setState([]);
history.switchToPrev();
history.switchToNext();
expect(history.hasNext()).toBe(false);
});
it('+hasNext() should returns false after setState()', () => {
history.setState([]);
history.switchToPrev();
history.setState([]);
expect(history.hasNext()).toBe(false);
});
it('+switchToPrev() should switch on prev state after setState()', () => {
const prevState = [{ title: '' }] as TodoParams[];
history.setState(prevState);
history.setState([]);
history.switchToPrev();
history.setState([]);
history.switchToPrev();
expect(history.getState()).toBe(prevState);
});
it('+setState() should not emit changes', async () => {
const spy = jasmine.createSpy();
history.changes.subscribe(spy);
history.setState([]);
await delay();
expect(spy).not.toHaveBeenCalled();
});
it('+switchToPrev() should emit changes', async () => {
history.setState([]);
await delay();
const spy = jasmine.createSpy();
history.changes.subscribe(spy);
history.switchToPrev();
await delay();
expect(spy).toHaveBeenCalled();
});
it('+switchToPrev() should emit changes', async () => {
history.setState([]);
history.switchToPrev();
await delay();
const spy = jasmine.createSpy();
history.changes.subscribe(spy);
history.switchToNext();
await delay();
expect(spy).toHaveBeenCalled();
});
it('+reset() should reset history and apply initial state', async () => {
history.setState([]);
expect(history.hasPrev()).toBe(true);
const initState = [{ title: '' }] as TodoParams[];
history.reset(initState);
expect(history.hasPrev()).toBe(false);
expect(history.getState()).toBe(initState);
});
});
AppTodoList.spec.tsimport { delay } from 'src/utils/delay';
import { TodoType } from '../core/TodoFactory';
import { AppTodoList } from './AppTodoList';
describe('AppTodoList', () => {
let todoList: AppTodoList;
beforeEach(() => {
todoList = new AppTodoList({
getItems: async () => [{ type: TodoType.Simple, title: 'Loaded todo', completed: false }],
save: () => delay(),
});
});
it('+resolve() should load saved todo items', async () => {
await todoList.resolve();
expect(todoList.getItems().length).toBeGreaterThan(0);
});
it('+resolve() should emit changes', async () => {
const spy = jasmine.createSpy();
todoList.changes.subscribe(spy);
await todoList.resolve();
await delay();
expect(spy).toHaveBeenCalled();
});
it('+add() should emit changes', async () => {
const spy = jasmine.createSpy();
todoList.changes.subscribe(spy);
todoList.add({ title: '' });
await delay();
expect(spy).toHaveBeenCalled();
});
it('+add() should emit changes after resolve()', async () => {
const spy = jasmine.createSpy();
todoList.changes.subscribe(spy);
await todoList.resolve();
todoList.add({ title: '' });
await delay();
expect(spy).toHaveBeenCalledTimes(2);
});
it('+todo.onChange() should emit changes', async () => {
await todoList.resolve();
await delay();
const spy = jasmine.createSpy();
todoList.changes.subscribe(spy);
const [todo] = todoList.getItems();
todo.toggleCompletion();
await delay();
expect(spy).toHaveBeenCalled();
});
it('+add() should call TodoListApi.save', async () => {
const spy = jasmine.createSpy();
todoList = new AppTodoList({ getItems: async () => [], save: async () => spy() });
todoList.add({ title: '' });
await delay();
expect(spy).toHaveBeenCalled();
});
it('+todo.onChange() should call TodoListApi.save', async () => {
const spy = jasmine.createSpy();
todoList = new AppTodoList({ getItems: async () => [{ title: '' }], save: async () => spy() });
await todoList.resolve();
const [todo] = todoList.getItems();
todo.toggleCompletion();
await delay();
expect(spy).toHaveBeenCalled();
});
it('+add() should ignore error on save', async () => {
const api = { getItems: async () => [], save: () => Promise.resolve() };
todoList = new AppTodoList(api);
todoList.add({ title: '1' });
await delay();
api.save = jasmine.createSpy().and.returnValue(Promise.reject('Mock saving failed'));
todoList.add({ title: '2' });
expect(todoList.getItems()).toHaveLength(2);
await delay();
expect(api.save).toHaveBeenCalled();
expect(todoList.getItems()).toHaveLength(2);
});
it('+resolve() should provide current todoList state to history', async () => {
expect(todoList.getItems()).toHaveLength(0);
expect(todoList.getHistory().getState()).toHaveLength(0);
await todoList.resolve();
await delay();
expect(todoList.getItems()).toHaveLength(1);
expect(todoList.getHistory().getState()).toHaveLength(1);
});
it('+add() should provide current todoList state to history', async () => {
expect(todoList.getItems()).toHaveLength(0);
expect(todoList.getHistory().getState()).toHaveLength(0);
todoList.add({ title: '' });
await delay();
expect(todoList.getItems()).toHaveLength(1);
expect(todoList.getHistory().getState()).toHaveLength(1);
});
it('+history.switchToPrev() should change todoList state on prev', async () => {
todoList.add({ title: '' });
await delay();
expect(todoList.getItems()).toHaveLength(1);
expect(todoList.getHistory().getState()).toHaveLength(1);
todoList.getHistory().switchToPrev();
await delay();
expect(todoList.getHistory().getState()).toHaveLength(0);
expect(todoList.getItems()).toHaveLength(0);
});
it('+history.switchToPrev() should change todoList state on prev after resolve()', async () => {
await todoList.resolve();
todoList.add({ title: '' });
await delay();
expect(todoList.getItems()).toHaveLength(2);
expect(todoList.getHistory().getState()).toHaveLength(2);
todoList.getHistory().switchToPrev();
await delay();
expect(todoList.getHistory().getState()).toHaveLength(1);
expect(todoList.getItems()).toHaveLength(1);
});
it('+add() should emit changes after history.switchToPrev()', async () => {
todoList.add({ title: '' });
todoList.getHistory().switchToPrev();
await delay();
const spy = jasmine.createSpy();
todoList.changes.subscribe(spy);
todoList.add({ title: '' });
await delay();
expect(spy).toHaveBeenCalled();
});
});
import { TodoParams } from 'src/core/TodoFactory';
import { Observable, Subject } from 'src/utils/Observable';
export class TodoListHistory {
private changesSubject = new Subject();
private history: TodoParams[][] = [this.state];
changes: Observable = this.changesSubject.asObservable();
constructor(private state: TodoParams[] = []) {}
reset(state: TodoParams[]): void {
this.state = state;
this.history = [this.state];
}
getState(): TodoParams[] {
return this.state;
}
setState(state: TodoParams[]): void {
this.deleteHistoryAfterCurrentState();
this.state = state;
this.history.push(state);
}
private nextState(state: TodoParams[]): void {
this.state = state;
this.changesSubject.next({});
}
private deleteHistoryAfterCurrentState(): void {
this.history = this.history.slice(0, this.getCurrentStateIndex() + 1);
}
hasPrev(): boolean {
return this.getCurrentStateIndex() > 0;
}
hasNext(): boolean {
return this.getCurrentStateIndex() < this.history.length - 1;
}
switchToPrev(): void {
const prevStateIndex = Math.max(this.getCurrentStateIndex() - 1, 0);
this.nextState(this.history[prevStateIndex]);
}
switchToNext(): void {
const nextStateIndex = Math.min(this.getCurrentStateIndex() + 1, this.history.length - 1);
this.nextState(this.history[nextStateIndex]);
}
private getCurrentStateIndex(): number {
return this.history.indexOf(this.state);
}
}
import { Observable, Subject, Subscription } from 'src/utils/Observable';
import { Todo } from '../core/Todo';
import { TodoFactory, TodoParams } from '../core/TodoFactory';
import { TodoList, TodoListImp } from '../core/TodoList';
import { TodoListApi } from './TodoListApi';
import { HistoryControl, TodoListHistory } from './TodoListHistory';
export class AppTodoList implements TodoList {
private readonly todoFactory = new TodoFactory();
private readonly history: TodoListHistory = new TodoListHistory();
private changesSubject = new Subject();
readonly changes: Observable = this.changesSubject.asObservable();
private state: TodoList = new TodoListImp();
private stateSubscription: Subscription = this.state.changes.subscribe(() =>
this.onStateChanges(),
);
private historySubscription = this.history.changes.subscribe(() => this.onHistoryChanges());
constructor(private api: TodoListApi) {}
private onStateChanges(): void {
const params = this.state.getItems().map((todo) => this.todoFactory.serializeTodo(todo));
this.history.setState(params);
this.api.save(params).catch(() => {});
this.changesSubject.next({});
}
private onHistoryChanges(): void {
const params = this.history.getState();
this.updateStateTodoList(params);
this.api.save(params).catch(() => {});
}
private updateStateTodoList(todoParamsList: TodoParams[]): void {
const todoList = new TodoListImp(todoParamsList);
this.state = todoList;
this.stateSubscription.unsubscribe();
this.stateSubscription = this.state.changes.subscribe(() => this.onStateChanges());
this.changesSubject.next({});
}
async resolve(): Promise<void> {
const todoParamsList = await this.api.getItems();
this.history.reset(todoParamsList);
this.updateStateTodoList(todoParamsList);
}
destroy(): void {
this.stateSubscription.unsubscribe();
this.historySubscription.unsubscribe();
}
getHistory(): HistoryControl<TodoParams[]> {
return this.history;
}
getItems(): Todo[] {
return this.state.getItems();
}
getCompletedItems(): Todo[] {
return this.state.getCompletedItems();
}
getUncompletedItems(): Todo[] {
return this.state.getUncompletedItems();
}
add(todoParams: TodoParams): void {
this.state.add(todoParams);
}
}
рдЪреВрдВрдХрд┐ рдкрд░рд┐рд╡рд░реНрддрди рдЗрддрд┐рд╣рд╛рд╕ рдмрд╣реБрдд рд╕рдорд╛рди рд╣реИ, рдЗрд╕рд▓рд┐рдП рдЯреВрдбреВ-рд╕реВрдЪреА рдЗрддрд┐рд╣рд╛рд╕ рдХреЗ рдкреНрд░рдмрдВрдзрди рдХреЛ рдЖрдзрд╛рд░ рд╡рд░реНрдЧ рдореЗрдВ рдЕрд▓рдЧ рдХрд░реЗрдВредimport { Todo } from 'src/core/Todo';
import { Observable, Subject, Subscription } from 'src/utils/Observable';
import { TodoFactory, TodoParams } from '../core/TodoFactory';
import { TodoList, TodoListImp } from '../core/TodoList';
import { HistoryControl, HistoryState } from './HistoryState';
export class HistoricalTodoList implements TodoList, HistoryControl {
protected readonly todoFactory = new TodoFactory();
protected readonly history = new HistoryState<TodoParams[]>([]);
private changesSubject: Subject = new Subject();
readonly changes: Observable = this.changesSubject.asObservable();
private state: TodoList = new TodoListImp();
private stateSubscription: Subscription = this.state.changes.subscribe(() =>
this.onStateChanged(this.getSerializedState()),
);
constructor() {}
protected onStateChanged(params: TodoParams[]): void {
this.history.addState(params);
this.changesSubject.next({});
}
protected onHistorySwitched(): void {
this.updateState(this.history.getState());
}
protected updateState(todoParamsList: TodoParams[]): void {
this.state = new TodoListImp(todoParamsList);
this.updateStateSubscription();
this.changesSubject.next({});
}
private updateStateSubscription(): void {
this.stateSubscription.unsubscribe();
this.stateSubscription = this.state.changes.subscribe(() =>
this.onStateChanged(this.getSerializedState()),
);
}
private getSerializedState(): TodoParams[] {
return this.state.getItems().map((todo) => this.todoFactory.serializeTodo(todo));
}
destroy(): void {
this.stateSubscription.unsubscribe();
}
getItems(): Todo[] {
return this.state.getItems();
}
getCompletedItems(): Todo[] {
return this.state.getCompletedItems();
}
getUncompletedItems(): Todo[] {
return this.state.getUncompletedItems();
}
add(todoParams: TodoParams): void {
this.state.add(todoParams);
}
canUndo(): boolean {
return this.history.hasPrev();
}
canRedo(): boolean {
return this.history.hasNext();
}
undo(): void {
this.history.switchToPrev();
this.onHistorySwitched();
}
redo(): void {
this.history.switchToNext();
this.onHistorySwitched();
}
}
import { TodoParams } from 'src/core/TodoFactory';
import { HistoricalTodoList } from './HistoricalTodoList';
import { TodoListApi } from './TodoListApi';
export class ResolvableTodoList extends HistoricalTodoList {
constructor(private api: TodoListApi) {
super();
}
async resolve(): Promise<void> {
const todoParamsList = await this.api.getItems();
this.history.reset(todoParamsList);
this.updateState(todoParamsList);
}
protected onStateChanged(params: TodoParams[]): void {
super.onStateChanged(params);
this.api.save(params).catch(() => this.undo());
}
protected onHistorySwitched(): void {
super.onHistorySwitched();
this.api.save(this.history.getState()).catch(() => {});
}
}
рд╕рдВрд░рдХреНрд╖рдг рд╕рдорд╕реНрдпрд╛ рдХрд╛ рд╕рдорд╛рдзрд╛рди рдЦреБрдж рд╣реА рдЖ рдЧрдпрд╛ред рдЕрдм рд╣рдо рдЗрд╕ рдмрд╛рд░реЗ рдореЗрдВ рдЪрд┐рдВрддрд╛ рдирд╣реАрдВ рдХрд░ рд╕рдХрддреЗ рд╣реИрдВ рдХрд┐ рдЧреНрд░рд╛рд╣рдХ рд▓рдВрдмреА рдЕрд╡рдзрд┐ рдореЗрдВ рдХрд┐рд╕ рд╕рдВрд░рдХреНрд╖рдг рд░рдгрдиреАрддрд┐ рдХрд╛ рдЪрдпрди рдХрд░реЗрдВрдЧреЗред рдмреЗрд╕ рдХреНрд▓рд╛рд╕ рдХрд╛ рд╡рд┐рд╕реНрддрд╛рд░ рдХрд░рддреЗ рд╣реБрдП, рдЖрдк рдЙрд╕реЗ рдЪреБрдирдиреЗ рдХреЗ рд▓рд┐рдП рд╕рднреА 3 рд╡рд┐рдХрд▓реНрдк рдкреНрд░рджрд╛рди рдХрд░ рд╕рдХрддреЗ рд╣реИрдВредрд╕рд╛рд░рд╛рдВрд╢
рдРрд╕рд╛ рд▓рдЧрддрд╛ рд╣реИ рдХрд┐ рд╣рдорд╛рд░реА рдЯреВрдбреВ рд╕реВрдЪреА рд╕реЗ рд╣рдореЗрдВ Google Keep рдХрд╛ рдПрдХ рдЫреЛрдЯрд╛ рдкреНрд░реЛрдЯреЛрдЯрд╛рдЗрдк рдорд┐рд▓ рдЧрдпрд╛ рд╣реИред рдПрдкреНрд▓рд┐рдХреЗрд╢рди рд▓реЙрдиреНрдЪ рдХрд░рдиреЗ рдХреЗ рд▓рд┐рдП рд░рд┐рдкреЙрдЬрд┐рдЯрд░реА рд╕реЗ рд▓рд┐рдВрдХ рдХрд░реЗрдВ рдпрд╛ рдХрдорд┐рдЯ рдХреЗ рдЗрддрд┐рд╣рд╛рд╕ рдкрд░ рдЬрд╛рдПрдВредрдЗрд╕ рдЙрджрд╛рд╣рд░рдг рдХреЛ рдЬреНрдпрд╛рджрд╛рддрд░ рдлреНрд░рдВрдЯ-рдПрдВрдб рдПрдкреНрд▓рд┐рдХреЗрд╢рди рд╕реЗ рдореМрд▓рд┐рдХ рд░реВрдк рд╕реЗ рдХреНрдпрд╛ рдЕрд▓рдЧ рдХрд░рддрд╛ рд╣реИ? рд╣рдо рдкреБрд╕реНрддрдХрд╛рд▓рдпреЛрдВ рдкрд░ рдирд┐рд░реНрднрд░ рдирд╣реАрдВ рдереЗ, рдЗрд╕рд▓рд┐рдП, рдПрдХ рд╡реНрдпрдХреНрддрд┐ рдЬрд┐рд╕рдиреЗ рдХрднреА рднреА рд░рд┐рдПрдХреНрдЯ рдХреЗ рд╕рд╛рде рдХрд╛рдо рдирд╣реАрдВ рдХрд┐рдпрд╛ рд╣реИ, рд╡рд╣ рдЗрд╕ рдПрдкреНрд▓рд┐рдХреЗрд╢рди рдХреЛ рд╕рдордЭ рд╕рдХрддрд╛ рд╣реИред рд╣рдорд╛рд░реЗ рдирд┐рд░реНрдгрдп рдХреЗрд╡рд▓ рдкрд░рд┐рдгрд╛рдо рдкреНрд░рд╛рдкреНрдд рдХрд░рдиреЗ рдХреЗ рдЙрджреНрджреЗрд╢реНрдп рд╕реЗ рдереЗ, рдлреНрд░реЗрдорд╡рд░реНрдХ рдХреЗ рд╡рд┐рд╡рд░рдг рдкрд░ рдзреНрдпрд╛рди рднрдВрдЧ рдХрд┐рдП рдмрд┐рдирд╛, рдЗрд╕рд▓рд┐рдП рдХреЛрдб рдХрдо рдпрд╛ рдЬреНрдпрд╛рджрд╛ рд╕рдорд╕реНрдпрд╛ рдХреЛ рд╣рд▓ рдХрд░рддрд╛ рд╣реИред рд╣рдо рдирдП рдкреНрд░рдХрд╛рд░ рдХреЗ рдЯреЛрдбреЛ рддрддреНрд╡реЛрдВ рдХреЛ рдЬреЛрдбрд╝рдирд╛ рдЖрд╕рд╛рди рдмрдирд╛рдиреЗ рдореЗрдВ рдХрд╛рдордпрд╛рдм рд░рд╣реЗ, рдФрд░ рд╣рдо рд╕рдВрд░рдХреНрд╖рдг рд░рдгрдиреАрддрд┐ рдХреЛ рдмрджрд▓рдиреЗ рдХреЗ рд▓рд┐рдП рддреИрдпрд╛рд░ рд╣реИрдВредрд╣рдореЗрдВ рдХрд┐рди рдХрдард┐рдирд╛рдЗрдпреЛрдВ рдХрд╛ рд╕рд╛рдордирд╛ рдХрд░рдирд╛ рдкрдбрд╝рд╛? рд╣рдордиреЗ рдлреНрд░реЗрдорд╡рд░реНрдХ рдХреЗ рд╕рдВрджрд░реНрдн рдХреЗ рдмрд┐рдирд╛ рдСрдмреНрдЬрд░реНрд╡рд░ рдкреИрдЯрд░реНрди рдХрд╛ рдЙрдкрдпреЛрдЧ рдХрд░рдХреЗ рджреГрд╢реНрдп рдХреЛ рдЕрдкрдбреЗрдЯ рдХрд░рдиреЗ рдХреА рд╕рдорд╕реНрдпрд╛ рдХреЛ рд╣рд▓ рдХрд┐рдпрд╛ред рдЬреИрд╕рд╛ рдХрд┐ рдпрд╣ рдирд┐рдХрд▓рд╛, рдЗрд╕ рдкреИрдЯрд░реНрди рдХреЗ рдЖрд╡реЗрджрди рдХреЛ рдЕрднреА рднреА рдореБрдЦреНрдп рд╕рдорд╕реНрдпрд╛ рдХреЛ рд╣рд▓ рдХрд░рдиреЗ рдХреА рдЖрд╡рд╢реНрдпрдХрддрд╛ рдереА (рднрд▓реЗ рд╣реА рд╣рдореЗрдВ HTML рдЦреАрдВрдЪрдиреЗ рдХреА рдЖрд╡рд╢реНрдпрдХрддрд╛ рдирд╣реАрдВ рдереА)ред рдЗрд╕рд▓рд┐рдП, рд╣рдордиреЗ рдлреНрд░реЗрдорд╡рд░реНрдХ рдореЗрдВ рдирд┐рд░реНрдорд┐рдд рдкрд░рд┐рд╡рд░реНрддрди рдХрд╛ рдкрддрд╛ рд▓рдЧрд╛рдиреЗ рд╡рд╛рд▓реА рдкреНрд░рдгрд╛рд▓реА рдХреА "рд╕реЗрд╡рд╛рдУрдВ" рдХреЛ рдЫреЛрдбрд╝ рдХрд░ рд▓рд╛рдЧрдд рдХрд╛ рдЕрдиреБрдорд╛рди рдирд╣реАрдВ рд▓рдЧрд╛рдпрд╛ рдерд╛редрдореИрдВ рдЗрд╕ рдмрд╛рдд рдкрд░ рдЬреЛрд░ рджреЗрдирд╛ рдЪрд╛рд╣реВрдВрдЧрд╛ рдХрд┐ рд▓реЗрдЦрди рдкрд░реАрдХреНрд╖рдг рдореЗрдВ рдХреЛрдИ рдХрдард┐рдирд╛рдИ рдирд╣реАрдВ рдереАред рдПрдХ рд╕реВрдЪрдирд╛рддреНрдордХ рдЗрдВрдЯрд░рдлрд╝реЗрд╕ рдХреЗ рд╕рд╛рде рд╕рд░рд▓ рд╕реНрд╡рддрдВрддреНрд░ рд╡рд╕реНрддреБрдУрдВ рдХрд╛ рдкрд░реАрдХреНрд╖рдг рдПрдХ рдЦреБрд╢реА рд╣реИред рдХреЛрдб рдХреА рдЬрдЯрд┐рд▓рддрд╛ рдХреЗрд╡рд▓ рдХрд╛рд░реНрдп рдФрд░ рдореЗрд░реЗ рдХреМрд╢рд▓ (рдпрд╛ рд╡рдХреНрд░рддрд╛) рдкрд░ рдирд┐рд░реНрднрд░ рдХрд░рддреА рдереАредрдбреЗрд╡рд▓рдкрд░ рдХреЗ рд╕реНрддрд░ рдХреЗ рдмрд╛рд░реЗ рдореЗрдВ рдХреНрдпрд╛ рдЬреЛ рдЗрд╕реЗ рд╕рдВрднрд╛рд▓реЗрдВрдЧреЗ? рдХреНрдпрд╛ рдЬреВрдирд┐рдпрд░ рд░рд┐рдПрдХреНрдЯрд░ рдбреЗрд╡рд▓рдкрд░ рдРрд╕рд╛ рд╕рдорд╛рдзрд╛рди рд▓рд┐рдЦ рд╕рдХрддрд╛ рд╣реИ? "рдкреНрд░реЛрдЧреНрд░рд╛рдорд┐рдВрдЧ рдПрдХ рд╢рд┐рд▓реНрдк рдХреА рддрд░рд╣ рдЕрдзрд┐рдХ рд╣реИ", рдЗрд╕рд▓рд┐рдП рдУрдУрдкреА рдФрд░ рдкреИрдЯрд░реНрди рдХрд╛ рдЙрдкрдпреЛрдЧ рдХрд░рдиреЗ рдХреЗ рдЕрднреНрдпрд╛рд╕ рдХреЗ рдмрд┐рдирд╛, рдореБрдЭреЗ рд▓рдЧрддрд╛ рд╣реИ рдХрд┐ рдпрд╣ рдореБрд╢реНрдХрд┐рд▓ рд╣реЛрдЧрд╛ред рд▓реЗрдХрд┐рди рдЖрдк рдФрд░ рдЖрдкрдХреА рдХрдВрдкрдиреА рддрдп рдХрд░рддреА рд╣реИ рдХрд┐ рдЖрдк рдХрд┐рд╕рдореЗрдВ рдирд┐рд╡реЗрд╢ рдХрд░рддреЗ рд╣реИрдВред рдХреНрдпрд╛ рдЖрдк OOP рдХрд╛ рдЕрднреНрдпрд╛рд╕ рдХрд░рддреЗ рд╣реИрдВ рдпрд╛ рдЕрдЧрд▓реЗ рдврд╛рдВрдЪреЗ рдХреА рдкреЗрдЪреАрджрдЧрд┐рдпреЛрдВ рдХреЛ рд╕рдордЭрддреЗ рд╣реИрдВ? рдореИрдВ рдХреЗрд╡рд▓ рдЕрдиреБрднрд╡реА рдкреНрд░реЛрдЧреНрд░рд╛рдорд░реЛрдВ рдХреЗ рд╕рд╛рд╣рд┐рддреНрдпрд┐рдХ рдХрд╛рд░реНрдпреЛрдВ рдХреА рдкреНрд░рд╛рд╕рдВрдЧрд┐рдХрддрд╛ рдХреЗ рдмрд╛рд░реЗ рдореЗрдВ рдЖрд╢реНрд╡рд╕реНрдд рд╣реЛ рдЧрдпрд╛, рдФрд░ рдпрд╣ рджрд┐рдЦрд╛рдпрд╛ рдХрд┐ рд╕рд╛рдордиреЗ рдХреА рдХреНрд╖рдорддрд╛ рдкрд░ рдХреНрд▓рд╛рд╕рд┐рдХреНрд╕ рдХреА рд╕рд▓рд╛рд╣ рдХрд╛ рдЙрдкрдпреЛрдЧ рдХреИрд╕реЗ рд╢реБрд░реВ рдХрд░реЗрдВредрдкрдврд╝рдиреЗ рдХреЗ рд▓рд┐рдП рдзрдиреНрдпрд╡рд╛рдж!