⬆️ ⬇️

Implement ToDoMVC on Jiant

I thank everyone who read, responded and appreciated the first post!



To demonstrate how the ideology of Jiant works, it implemented ToDoMVC, a project created for evaluating various MVC frameworks. Jiant is not an MVC framework, but rather an approach to development with a set of supporting tools. It took me about 8 hours to develop, taking into account the reading and understanding of the specification, the study of the reference implementation, localStorage, with which I had no business (a very simple thing) and everything else. I do not know a lot or a little, but so much. The results are at: github.com/vecnas/todomvc-jiant . In Chrome and Firefox, it works directly from the file system, and in IE from the server.



Specification



github.com/addyosmani/todomvc/wiki/App-Specification - specification in English. Summarize the key points here.

Need to develop a task management tool (ToDo), the following points should be implemented:

  1. Storage. All tasks and their status should be saved in the local storage (HTML5 localStorage) and restored when restarting the browser for example.
  2. Navigation. The application must support browser hash navigation; see the original for more details. The navigation format is set there, but Jiant has a slightly different format, this is due to the ability to set navigation nodes and then return to them. Operated by buttons on the navigation bar. There are three states: show all, show active and show completed
  3. New tasks. These are entered by filling in the input field at the top of the page and pressing the Enter key.
  4. Mass manipulation. A special button marks all tasks as completed or not completed. The button state is also synchronized with the current state of all tasks. That is, if we add a new one when everything is already completed, the indicator will stop showing that everything is done.
  5. Task. Represents three interactive elements - the switch is done-not done, it works by clicking, the button for deleting a task, and by double-clicking on the name - it gives the opportunity to edit the text of the task
  6. Editing. The input field opens, the rest of the elements are hidden, by pressing Enter or losing focus - saves the text, by pressing Escape - cancels all changes; if you save the text is empty - deletes the task
  7. Counter active tasks. Shows the number of current active tasks, synchronized to any changes, the text should be literate ("1 task", but "2 tasks")
  8. Clear completed. Shows the number of completed tasks, deletes them on click and is shown only if there is at least one completed task.
  9. The panel with counters and navigation buttons is shown only if there is at least one task.




At the entrance there is a project template, containing html code and some interactive scripts, as well as all styles.

')

Jiant project structure



At the moment, the ideal structure is as follows:

  1. The application definition file is a json variable declaration describing the application API
  2. "Ignition" is a call to bindUi anywhere where the user is comfortable.
  3. A set of "plug-ins" - application logic, each of which is encapsulated in its sandbox and works through the application API




Initial design



The first stage in the Jiant approach is the most abstract design. The most important and possibly unusual (at least for myself) is to completely disengage from how everything will be realized. I create the first version of the application description, looking at the description of the customer. Here I present the first considerations, after reading the specification, and not the ideal reasoning prepared as a result of the project. That is, everything is vital, as it happens.



States


If you need hash navigation, it means that the application will have states. We list them, directly take a list from the specification, do not think:

states: { "": { go: function () {}, start: function(cb) {}, end: function(cb) {} }, active: { go: function () {}, start: function(cb) {}, end: function(cb) {} }, completed: { go: function () {}, start: function(cb) {}, end: function(cb) {} } } 




Explanation: According to the format, if the application description variable contains the states section - Jiant loads the states listed in it and implements three methods for each: go, start, end. go is used to go to the state, start and end to react to the beginning or end of the state. The most concise entry looks like this:

  states: { "": {}, active: {}, completed: {} } 


But in this case, we will not have auto-completion in the IDE, so for my own convenience and better documentation, I use the first notation. An empty state corresponds to “uncertain” situations - for example, when a browser window is simply loaded without any hashes.

All that is all you need to work hash navigation. All functionality will be added during application initialization by Jiant.



To completely close the topic of state design - you can make one state with two parameters, for example:

  states: { "": { go: function(showActive, showCompleted) {} } } 


And this will work too. But, since we have on state control, then from purely utilitarian considerations it seems more convenient to start three states. Again, this is the first intuitive solution. By the way, these states remained in the final version.



Developments


Now we define application-level events, again in the abstract. It seems intuitively that the following list is correct:

  events: { todoAdded: { fire: function(todo) {}, on: function(cb) {} }, todoRemoved: { fire: function(todo) {}, on: function(cb) {} }, todoStateChanged: { fire: function(todo) {}, on: function(cb) {} } } 




The only thinking caused the last event - whether it is necessary to break it into two - to complete or re-activate todo. Again, the entire event support code runs inside Jiant, the user only needs to define an abstract list of events and use it. Each event has two methods - fairly obvious. The cb (callback, on method parameter) function takes exactly the same arguments as the fire call.



Interface


Now you need to determine the visual part of the application. The list of elements as usual is simply copied from the description of the customer, as convenient. After writing so:

  views: { main: { batchToggleStateCtl: ctl, newTodoTitleInput: input, todoList: container }, controls: { activeCountLabel: label, clearCompletedCtl: ctl, completedCountLabel: label, showAllCtl: ctl, showActiveCtl: ctl, showCompletedCtl: ctl } }, 


- it is useful in html and found that the identifiers for the elements are already registered, the structure is set, so it would be more logical to just apply it. Total:

  views: { header: { newTodoTitleInput: input }, main: { batchToggleStateCtl: ctl, todoList: container }, footer: { activeCountLabel: label, clearCompletedCtl: ctl, completedCountLabel: label, showAllCtl: ctl, showActiveCtl: ctl, showCompletedCtl: ctl } } 


and this structure, created in the 10th minute of design, has not changed until the end of the project.



Templates


Based on the specification, we see one extremely dynamic element - a visual representation of the task. Their number is different, they are added and removed, so for this, according to the ideology of Jiant, you just need to use a template, we define it:

  templates: { tmTodo: { deleteCtl: ctl, editCtl: ctl, stateMarker: label, toggleStateCtl: ctl, titleInput: input, hiddenInEditMode: collection, titleLabel: label } } 


Again so as not to bother the brain once more - we simply transfer everything from the customer’s description, according to the principle “delete task button” - deleteCtl.

Here it is necessary to say something about the patterns. First, you can see that the fields are defined for the template - they are first checked for validity when the application is started, and secondly they are attached after creating an element from the template. Each template has two methods: parseTemplate, parseTemplate2Text, which take parameters for substitution. The template does not contain any logic, only the substitution of values, and this is intentionally the place of logic in javascript code. Later, another application appeared in the application, introduced more to show how to substitute values:

  templates: { .... itemsLeft: {}, itemsLeft1: {} } 


implementation:

  <div id="itemsLeft1" style="display: none;"> <strong>!!count!!</strong> item left </div> <div id="itemsLeft" style="display: none;"> <strong>!!count!!</strong> items left </div> 


and use:

  tm.parseTemplate2Text({count: count}) 




We do not design the data model yet, see below. I want to quickly run and see that nothing breaks, for this you need a starter.



Starter



Based on the ideal structure, the application starter is placed in the app.js file (available in the “customer” base template) and its code is as follows:

  jQuery(function() { jiant.bindUi("", todomvcJiant, true); }); 




Since the html code is already set by the customer (and I don’t want to change, not to change css), the prefix here is empty (the first parameter), the variable todomvcJiant is defined in the application definition file (it contains views, templates, states, events) and we turn on development.



HTML implementation



That's all, we only have the definition file and the starter, no logic, but I really want to run the application. I run html in Chrome and see:

  1. Alert with a message from Jiant, in which it says “No history plugin and states configured. Don't use states or add $ .History plugin »
  2. Another alert with a message about unrealized interface elements
  3. And repeat the text of the second alert in the console: non-existing object


Adding history - is included. It now remains to implement an abstract definition at the html level, for example:

 <section id="main"> <input id="toggle-all" class="batchToggleStateCtl" type="checkbox" style="display: none;"> <label for="toggle-all">Mark all as complete</label> <ul id="todo-list" class="todoList"> </ul> </section> 


- identifiers are already placed, it remains to add classes to the desired elements. Template implementation:

  <div id="tmTodo" style="display: none;"> <li class="stateMarker"> <div class="view hiddenInEditMode"> <input class="toggle toggleStateCtl" type="checkbox"> <label class="editCtl titleLabel"></label> <button class="destroy deleteCtl"></button> </div> <input class="edit titleInput" value=""> </li> </div> 


It should be noted that, unlike common practice, to place templates in <script type = "someUnreadableTypeToFoolBrowser">, Jiant uses the correct html structure, this is due to the fact that there is no DOM model inside the script tag and it is impossible to validate the template field binding to the implementation.



Finally, Jiant stopped reporting unrelated elements, the abstract application model is tied to an implementation and ready to use.



Plugins



Following the ideology of Jiant - any logic is added by plugins. If a new view or event state appears, we add it to the application description file and the implementation to the html. That is, the process of expanding the application is always constant and controlled, which is extremely important for large-scale development, starting small.



Model



The question is whether this application needs a centralized data model? Theoretically, each plugin can contain its own model and synchronize it based on events occurring in the application. In practice, I wrote this version for comparison and, as expected, I got a lot of unnecessary and duplicate code. Therefore, we will implement the model (it would practically be possible to place the implementation directly in the json application description file, but then it will become harder to read, therefore we will render a separate plugin, following the ideology), model.js:

 todomvcJiant.model.todo = (function($, app) { var todos = []; return { add: function(title, completed) { var todo = {title: title, completed: completed ? true : false}; todos.push(todo); app.events.todoAdded.fire(todo); return todo; }, remove: function(todo) { todos = $.grep(todos, function(value) {return value != todo;}); app.events.todoRemoved.fire(todo); return todo; }, getAll: function() { return todos; }, getCompleted: function() { return $.grep(todos, function(value) {return value.completed}); }, getActive: function() { return $.grep(todos, function(value) {return !value.completed}); } } })($, todomvcJiant); 


Simplest array based implementation. It is worth noting just here we run some application events. In this case, adding a model to the json project description is cosmetic (for example, there is no getActive () method), Jiant is not involved in data models at all.



Plugin implementation



Further, for each element of logic from the specification, we write our own plugin and add it. At any moment everything works for us, the functionality is increased, without affecting the rest. Below are a couple of examples, a comment right in the code.



stateCtls.js


 jiant.onUiBound(function($, app) { //      API   var ctlsView = app.views.footer, ctls = { "showActiveCtl": app.states.active, //      "showCompletedCtl": app.states.completed, "showAllCtl": app.states[""] }; $.each(ctls, function(key, state) { ctlsView[key].click(function() { state.go(); }); state.start(function() { //             ctlsView[key].addClass("selected"); }); state.end(function() { ctlsView[key].removeClass("selected"); }); }); }); 




footerVisibility.js




 jiant.onUiBound(function($, app) { app.events.todoAdded.on(updateView); //    app.events.todoRemoved.on(updateView); function updateView() { //   todo  ,         app.model.todo.getAll().length > 0 ? app.views.footer.show() : app.views.footer.hide(); } }); 




Similarly, other plugins are added using onUiBound. When a plugin needs a new API functionality, we create an abstract declaration and implementation. In this case, in most plugins, something like the global smthChanged event was created in response to which the plugins update their status, but this is a particular coincidence.

It is worth noting that html is used only as html, no custom attributes or tags. All the logic of working with UI is written in jQuery functions.

Fun fact - it was not necessary to enter the object identifier. All work is done through direct links to objects. The UI reference to the implementation of the todo object is saved as a todo object field, inside todoRenderer.js and not used by anyone outside, the order is preserved inside the model.



PS



Already when I wrote everything on the last line I realized that after changing the text of the task, the new text is not saved in localStorage until any of the already existing events occurs. To fix, added a new event todoTitleModified, throws in the editor after installing a new text (todoRenderer.js) and subscribing to an event in the persistence module (persistence.js).

Source: https://habr.com/ru/post/176617/



All Articles