Приложение TodoMVC UI ( todomvc.com ) широко считается «Hello World» для разработки пользовательского интерфейса. На сайте есть несколько демонстраций с использованием различных UI-фреймворков. В этой статье я исследую использование конечного автомата для разработки приложения пользовательского интерфейса TodoMVC. Я буду использовать ванильный JavaScript для иллюстрации этих концепций, и я буду использовать структуру конечного автомата, которую я представил в предыдущей статье .
Государственные переходы
Первым шагом в использовании конечного автомата является написание переходов состояний для пользовательского интерфейса нашего приложения. Я предполагаю следующие переходы состояний для приложения TodoMVC (снимок экрана внизу страницы), который я рассматриваю здесь:
Инициалы государство | Pre-Event | процессор | Сообщение событие | Конечное состояние |
unknownState | в процессе | processOnload () | onloadSuccess | readyForAdd |
readyForAdd | addTodo | processAddTodo () | addTodoSuccessNoneSelected | readyForAddSelect |
readyForAddSelect | addTodo | processAddTodo () | addTodoSuccessNoneSelected | readyForAddSelect |
readyForAddSelect | changeTodo | processChangeTodo () | changeTodoSuccessSomeSelected | readyForAddSelectUnselectDelete |
readyForAddSelect | changeTodo | processChangeTodo () | changeTodoSuccessAllSelected | readyForAddUnselectDelete |
readyForAddUnselectDelete | addTodo | processAddTodo () | addTodoSuccessSomeSelected | readyForAddSelectUnselectDelete |
readyForAddUnselectDelete | changeTodo | processchangeTodo () | changeTodoSuccessNoneSelected | readyForAddSelect |
readyForAddUnselectDelete | changeTodo | processchangeTodo () | changeTodoSuccessSomeSelected | readyForAddSelectUnselectDelete |
readyForAddUnselectDelete | deleteTodo | processDeleteTodo () | deleteTodoSuccessAllDeleted | readyForAdd |
readyForAddSelectUnselectDelete | addTodo | processAddTodo () | addTodoSuccessSomeSelected | readyForAddUnselectDelete |
readyForAddSelectUnselectDelete | changeTodo | processChangeTodo () | changeTodoSuccessAllSelected | readyForAddUnselectDelete |
readyForAddSelectUnselectDelete | changeTodo | processChangeTodo () | changeTodoSuccessSomeSelected | readyForAddSelectUnselectDelete |
readyForAddSelectUnselectDelete | changeTodo | processChangeTodo () | changeTodoSuccessNoneSelected | readyForAddSelect |
readyForAddSelectUnselectDelete | changeTodo | processChangeTodo () | changeTodoSuccessSomeSelected | readyForAddSelectUnselectDelete |
readyForAddSelectUnselectDelete | deleteTodo | processDeleteTodo () | deleteTodoSuccessNoneSelected | readyForAddSelect |
Я выделил четыре состояния приложения: readyForAdd
, readyForAddSelect
, readyForAddUnselectDelete
, и readyForAddSelectUnselectDelete
. Состояние readyForAdd
, например, подразумевает, что только события добавления могут быть отправлены из этого состояния, в то время как readyForAddSelect
состояние может только генерировать события добавления и выбора .
Вам также может понравиться:
Модель-представление-контроллер (MVC) Deep Dive .
События и состояние конфигурации
Следующим шагом является настройка событий и состояний, записанных в приведенной выше таблице, как объектов JavaScript. События фиксируются как:
JavaScript
xxxxxxxxxx
1
const appEvents = {
2
onload: {
3
process: function(e, handlePostEvent) {
4
var props = {
5
id: "addTodo",
6
todoText: "",
7
label: "Add a to-do item:"
8
};
9
createInputTextElement(document.getElementById("addTodo"), props);
10
handlePostEvent(new CustomEvent('onloadSuccess'));
11
}
12
},
13
onloadSuccess: {
14
nextState: function(e) {
15
return appStates.readyForAdd(e);
16
}
17
},
18
addTodo: {
19
process: function(e, handlePostEvent) {
20
e.detail.id = "changeTodo";
22
e.detail.todoText = appData.todoText();
23
e.detail.itemsCount = appData.itemsCount();
24
createInputCheckboxElement(document.getElementById("changeTodo"), e.detail);
25
var evttype = 'addTodoSuccessNoneSelected';
27
if (appData.selectedCount() > 0 &&
28
appData.itemsCount() - appData.selectedCount() > 0) {
29
evttype = 'addTodoSuccessSomeSelected';
30
}
31
handlePostEvent(new CustomEvent(evttype));
32
}
33
},
34
addTodoSuccessNoneSelected: {
35
nextState: function(e) {
37
return appStates.readyForAddSelect(e);
39
}
40
},
41
addTodoSuccessSomeSelected: {
42
nextState: function(e) {
44
return appStates.readyForAddSelectUnselectDelete(e);
45
}
46
},
47
changeTodo: {
48
process: function(e, handlePostEvent) {
49
if (document.getElementById("deleteTodo").getElementsByTagName("input").length == 0) {
50
e.detail.id = "deleteTodo";
51
createInputButtonElement(document.getElementById("deleteTodo"), e.detail);
52
}
53
var evttype = 'changeTodoSuccessNoneSelected';
54
if (appData.selectedCount() > 0) {
55
if (appData.selectedCount() == appData.itemsCount()) {
56
evttype = 'changeTodoSuccessAllSelected';
57
} else if (appData.itemsCount() - appData.selectedCount() > 0) {
58
evttype = 'changeTodoSuccessSomeSelected';
59
}
60
}
61
handlePostEvent(new CustomEvent(evttype));
62
}
63
},
64
changeTodoSuccessSomeSelected: {
65
nextState: function(e) {
67
return appStates.readyForAddSelectUnselectDelete(e);
68
}
69
},
70
changeTodoSuccessAllSelected: {
71
nextState: function(e) {
73
return appStates.readyForAddUnselectDelete(e);
74
}
75
},
76
changeTodoSuccessNoneSelected: {
77
nextState: function(e) {
79
return appStates.readyForAddSelect(e);
80
}
81
},
82
deleteTodo: {
83
process: function(e, handlePostEvent) {
84
e.detail.id = 'changeTodo';
85
e.detail.itemsCount = appData.itemsCount();
86
e.detail.selTodos = appData.selectedItems();
87
deleteInputCheckboxElements(document.getElementById("changeTodo"), e.detail);
88
let evttype = '';
89
if (appData.itemsCount() > 0 &&
90
appData.selectedCount()==0) {
91
evttype = 'deleteTodoSuccessNoneSelected';
92
}
93
else evttype = 'deleteTodoSuccessAllDeleted';
94
handlePostEvent(new CustomEvent(evttype));
95
}
96
},
98
deleteTodoSuccessNoneSelected: {
99
nextState: function(e) {
100
return appStates.readyForAddSelect(e);
101
}
102
},
103
deleteTodoSuccessAllDeleted: {
105
nextState: function(e) {
106
return appStates.readyForAdd(e);
107
}
108
}
109
};
Состояния приложения фиксируются как:
JavaScript
xxxxxxxxxx
1
const appStates = {
2
readyForAdd: function(e) {
3
document.getElementById("addTodoView").style.display = "block";
4
document.getElementById("changeTodoView").style.display = "none";
5
document.getElementById("deleteTodoView").style.display = "none";
6
document.getElementById("currentState").innerHTML = e.type + ' => readyForAdd';
7
},
8
readyForAddSelect: function(e) {
9
document.getElementById("addTodo").getElementsByTagName("input")[0].value = "";
10
document.getElementById("addTodo").getElementsByTagName("input")[0].focus();
11
document.getElementById("addTodoView").style.display = "block";
12
document.getElementById("changeTodoView").style.display = "block";
13
document.getElementById("deleteTodoView").style.display = "none";
14
document.getElementById("currentState").innerHTML = e.type + ' => readyForAddSelect';
15
},
16
readyForAddUnselectDelete: function(e) {
17
document.getElementById("addTodo").getElementsByTagName("input")[0].value = "";
18
document.getElementById("addTodo").getElementsByTagName("input")[0].focus();
19
document.getElementById("addTodoView").style.display = "block";
20
document.getElementById("changeTodoView").style.display = "block";
21
document.getElementById("deleteTodoView").style.display = "block";
22
document.getElementById("currentState").innerHTML = e.type + ' => readyForAddUnselectDelete';
23
},
24
readyForAddSelectUnselectDelete: function(e) {
25
document.getElementById("addTodo").getElementsByTagName("input")[0].value = "";
26
document.getElementById("addTodo").getElementsByTagName("input")[0].focus();
27
document.getElementById("addTodoView").style.display = "block";
28
if (appData.itemsCount() > 0) document.getElementById("changeTodoView").style.display = "block";
29
else document.getElementById("changeTodoView").style.display = "none";
30
document.getElementById("deleteTodoView").style.display = "block";
31
document.getElementById("currentState").innerHTML = e.type + ' => readyForAddSelectUnselectDelete';
32
}
33
};
34
Просмотр - HTML-шаблон
Шаблон HTML, который я рассматриваю для этой демонстрации:
xxxxxxxxxx
1
<div class="pure-g">
2
<div class="pure-u-1-4"> </div>
3
<section id="todoApp" class="pure-u-1-2">
4
<div id="currentState" class="red, small"> </div>
5
<div> </div>
6
<section id="addTodoView" style="display: none;">
7
<InputComp id="addTodo"></InputComp>
8
</section>
9
<section id="changeTodoView" style="display: none;">
10
<p></p>
11
<CheckboxGroupComp id="changeTodo" maxId="0"></CheckboxGroupComp>
12
<p></p>
13
</section>
14
<section id="deleteTodoView" style="display: none;">
15
<ButtonComp id="deleteTodo"></ButtonComp>
16
</section>
17
</section>
18
<div class="pure-u-1-4"> </div>
19
</div>
Я использую некоторые пользовательские теги полезности, как InputComp
, CheckboxGroupComp
, и ButtonComp
. Код JavaScript загружает динамически сгенерированный контент HTML DOM в эти элементы.
модель
Модель для приложения:
JavaScript
xxxxxxxxxx
1
const appData = {
2
todoText: function() {
3
return document.getElementsByName("addTodo").item(0).value;
4
},
5
selectedCount: function() {
6
return appData.selectedItems().length;
7
},
8
itemsCount: function() {
9
return document.getElementsByName("changeTodo").length;
10
},
11
selectedItems: function() {
12
return Array.from(document.getElementsByName("changeTodo")).filter(e => e.checked).map(e => e.value);
13
}
14
};
контроллер
Контроллер stateTransitionsManager()
получает все события, отправленные приложением, и отправляет их процессору вместе с функцией обратного вызова ( handlePostEvent()
). Когда процессор завершает свою задачу (возможно, длительную), он вызывает функцию обратного вызова, передавая ему новое настраиваемое событие в качестве аргумента.
JavaScript
xxxxxxxxxx
1
function stateTransitionsManager(todoEvent) {
2
var todoEventAft = appEvents[todoEvent.type].process(todoEvent, handlePostEvent);
3
}
4
5
function handlePostEvent(e) {
6
appEvents[e.type].nextState(e);
7
}
Полный исходный код для демонстрации находится на GitHub .
Онлайн-демонстрация этой реализации доступна в приложении Todo .
Демонстрационная страница также отображает переход состояния для каждого состояния в виде информационного текста. Пользователь должен иметь возможность пройти через различные переходы, перечисленные в таблице выше. Вот снимки экрана из вышеприведенной демонстрации, соответствующие четырем состояниям, указанным в таблице:
readyForAdd
readyForAddSelect
readyForAddUnselectDelete
readyForAddSelectUnselectDelete
Выводы
Показано, что предлагаемый шаблон разработки пользовательского интерфейса на основе конечного автомата обеспечивает чистую и простую структуру для разработки надежных приложений пользовательского интерфейса. Как только состояния и события определены, разработка довольно проста.
Сопутствующие работы
Заинтересованные читатели могут также оформить davidkpiano , lucaszmakuch , FrancisStokes , immerjs и overmindjs . Читатели, заинтересованные в применении аналогичных методов для серверных приложений Java Spring Boot, могут ознакомиться с этой статьей DZone .