📜 ⬆️ ⬇️

Qooxdoo. Developing a TODO List

To date, there are a great many javascript frameworks, many of them are written mountains of documentation. I would like to dwell on the framework, which, for some unknown reason, is not particularly popular with Russian developers.

The framework is called qooxdoo. It is pronounced "kuksdu" (for whom it is more convenient to English transtription: ['kuksdu:]).

On Habré there were several attempts to write about this framework, but they all boiled down to news about the release of a new version or to pairs of paragraphs in articles like "see what frameworks they wrote". I have been working with qooxdoo for several years and I would like to fill this gap.

Briefly about what kind of animal it is and what it is eaten with. Most of all, the framework is “similar” to ExtJS. The word “similar” is not entirely correct, in this case, but I find it difficult to choose a more appropriate one. Development of the project began in the depths of the company 1 & 1 Internet AG . The first public version 0.1 was released in 2005. The current actual version 4.1, about it and we will be talking. Some moments allow me to say that the developers were inspired by Qt when creating their offspring. The main initial idea of ​​the developers is to enable people to develop web applications without knowledge of HTML, CSS and DOM models. With qooxdoo this is possible. A newbie who needs to write, for example, an admin panel in the form of a single page application (hereinafter referred to as SPA) and who does not know a single HTML tag, but has never heard of CSS, in fact, can do it. This does not mean that knowledge of HTML, CSS and DOM models suddenly suddenly became unnecessary. Simply, at first, you can do without them. What will be especially interesting, for example, to developers of desktop applications who needed to do something on the web.
')
At the end of the article you can find some useful links. In particular, there are links to various demos and examples of real use of the framework in production.

Just talk about the framework is boring and uninteresting. In addition, the developers have already done so. Therefore, I decided to make some simple example to demonstrate the capabilities of the framework. Many people know about the project http://todomvc.com/ . So we will do something as similar as possible using qooxdoo. In fairness, the developers have already made a demo todo sheet , but this is not exactly what we need.

So let's get started.

It should be noted that the SPA (Desktop in qooxdoo terminology) will be considered. First you need to download qooxdoo sdk. This can be done at this link . The SDK contains a number of utilities that allow you to generate an application template and compile a debug and release version, build automatic documentation, tuts, etc. You can view the documentation for the toolchain here .

To create an application template, we will run:

create-application.py --name=todos 

After this operation, we get the following application framework:



The application is not generated empty. It will have a button, by clicking on which the alert will be displayed.
The main Application.js file will contain the following code:

 /** * This is the main application class of your custom application "todos" * * @asset(todos/*) */ qx.Class.define("todos.Application", { extend : qx.application.Standalone, members : { /** * This method contains the initial application code and gets called * during startup of the application * * @lint ignoreDeprecated(alert) */ main : function() { // Call super class this.base(arguments); // Enable logging in debug variant if (qx.core.Environment.get("qx.debug")) { // support native logging capabilities, eg Firebug for Firefox qx.log.appender.Native; // support additional cross-browser console. Press F7 to toggle visibility qx.log.appender.Console; } /* ------------------------------------------------------------------------- Below is your actual application code... ------------------------------------------------------------------------- */ // Create a button var button1 = new qx.ui.form.Button("First Button", "todos/test.png"); // Document is the application root var doc = this.getRoot(); // Add button to document at fixed coordinates doc.add(button1, {left: 100, top: 50}); // Add an event listener button1.addListener("execute", function(e) { alert("Hello World!"); }); } } }); 

In order to see the authors' idea, we will need to build the debug or production version of the application.
The first option is obtained if you go to the project folder and run:

 ./generate.py source 

the second can be obtained after launch:

 ./generate.py build 

After that, we load the corresponding index.html file in the browser and see the following image:



The button can be pressed, but you can not click. You can rob cows. At this opportunity the application ends. A miracle did not happen, then we will have to write code, which we actually do.

For the impatient I immediately give a link to github with a ready-made version that can be played with. In order to succeed, besides the source code from the github, you need to download the SDK and write the correct path "QOOXDOO_PATH" in the config.json file. Then you need to collect the required version, as described above.

Well, we will consider the process of creating an application sequentially, in its natural form.
To begin, we will create a template for the widget window for our todo sheet and mercilessly remove from Application.js everything that the generator has generated for us. We have the following.

Window.js
 qx.Class.define("todos.Window", { extend : qx.ui.window.Window, construct: function(){ this.base(arguments); this.set({ caption: "todos", width: 480, height: 640, allowMinimize: false, allowMaximize: false, allowClose: false }); this.addListenerOnce("appear", function(){ this.center(); }, this); } }); 

Application.js
 /** * @asset(todos/*) */ qx.Class.define("todos.Application", { extend : qx.application.Standalone, members : { main : function() { // Call super class this.base(arguments); var wnd = new todos.Window; wnd.show(); } } }); 

After assembly, we will see just such beauty:



It's time to fill it with meaning. We will need the following elements: a toolbar, a todo sheet record and an element for adding a record to the sheet. Writing a todo sheet is a repeating element, we arrange it as a separate widget. The toolbar and the element of adding a record to the sheet can be made as separate widgets, which allows them to be reused, and as part of the Window. The toolbar will be made a separate widget, and the element for adding an entry will be left part of the Window to show that it is possible both ways. Let's do all of the above and fill the widgets with life.

ToDo.js
 qx.Class.define("todos.ToDo", { extend: qx.ui.core.Widget, events : { remove : "qx.event.type.Event" }, properties: { completed: { init: false, check: "Boolean", event: "completedChanged" }, appearance: { refine: true, init: "todo" } }, construct: function(text){ this.base(arguments); var grid = new qx.ui.layout.Grid; grid.setColumnWidth(0, 20); grid.setColumnFlex(1, 1); grid.setColumnWidth(2, 20); grid.setColumnAlign(0, "center", "middle"); grid.setColumnAlign(1, "left", "middle"); grid.setColumnAlign(2, "center", "middle"); this._setLayout(grid); this._add(this.getChildControl("checkbox"), {row: 0, column: 0}); this._add(this.getChildControl("text-container"), {row: 0, column: 1}); this._add(this.getChildControl("icon"), {row: 0, column: 2}); this.getChildControl("label").setValue(text); this.addListener("mouseover", function(){this.getChildControl("icon").show();}, this); this.addListener("mouseout", function(){this.getChildControl("icon").hide();}, this); this.getChildControl("icon").hide(); this.getChildControl("text-container").addListener("dblclick", this.__editToDo, this); }, members : { // overridden _createChildControlImpl: function(id) { var control; switch(id) { case "checkbox": control = new qx.ui.form.CheckBox; this.bind("completed", control, "value"); control.bind("value", this, "completed"); break; case "text-container": control = new qx.ui.container.Composite(new qx.ui.layout.HBox); control.add(this.getChildControl("label"), {flex: 1}); break; case "label": control = new qx.ui.basic.Label; control.bind("value", control, "toolTipText"); break; case "textfield": control = new qx.ui.form.TextField; control.addListener("keypress", function(event){ var key = event.getKeyIdentifier(); switch(key) { case "Enter": this.__editComplete(); break; case "Escape": this.__editCancel(); break; } }, this); control.addListener("blur", this.__editComplete, this); break; case "icon": control = new qx.ui.basic.Image("todos/icon-remove-circle.png"); control.addListener("click", function(){ this.fireEvent("remove"); }, this); break; } return control || this.base(arguments, id); }, __editToDo : function() { var tc = this.getChildControl("text-container"); var tf = this.getChildControl("textfield"); tc.removeAll(); tc.add(tf, {flex: 1}); tf.setValue(this.getChildControl("label").getValue()); tf.focus(); tf.activate(); }, __editComplete : function() { this.getChildControl("label").setValue(this.getChildControl("textfield").getValue()); this.__editCancel(); }, __editCancel : function() { var tc = this.getChildControl("text-container"); tc.removeAll(); tc.add(this.getChildControl("label"), {flex: 1}); } } }); 

StatusBar.js
 qx.Class.define("todos.StatusBar", { extend: qx.ui.core.Widget, events: { removeCompleted: "qx.event.type.Event" }, properties: { todos: { init: [], check: "Array" }, filter: { init: "all", check: ["all", "active", "completed"], event: "filterChanged" } }, construct: function() { this.base(arguments); var grid = new qx.ui.layout.Grid; grid.setColumnWidth(0, 100); grid.setColumnFlex(1, 1); grid.setColumnWidth(2, 130); grid.setColumnAlign(0, "left", "middle"); grid.setColumnAlign(1, "center", "middle"); grid.setColumnAlign(2, "right", "middle"); grid.setRowHeight(0, 26); this._setLayout(grid); this._add(this.getChildControl("info"), {row: 0, column: 0}); this._add(this.getChildControl("filter"), {row: 0, column: 1}); this._add(this.getChildControl("remove-completed-button"), {row: 0, column: 2}); this.update(); }, destruct: function() { this.__rgFilter.dispose(); }, members : { __rgFilter: null, update: function() { var todosCount = this.getTodos().length; var itemsLeft = this.getTodos().filter(function(item){return !item.getCompleted();}).length; this.getChildControl("info").setValue("<b>"+itemsLeft+"</b> items left"); if (itemsLeft === todosCount) { this.getChildControl("remove-completed-button").exclude(); } else { this.getChildControl("remove-completed-button").setLabel("Clear completed ("+(todosCount-itemsLeft)+")"); this.getChildControl("remove-completed-button").show(); } }, // overridden _createChildControlImpl: function(id) { var control; switch(id) { case "info": control = new qx.ui.basic.Label; control.setRich(true); break; case "filter": control = new qx.ui.container.Composite(new qx.ui.layout.HBox); control.add(this.getChildControl("rb-filter-all")); control.add(this.getChildControl("rb-filter-active")); control.add(this.getChildControl("rb-filter-completed")); this.__rgFilter = new qx.ui.form.RadioGroup( this.getChildControl("rb-filter-all"), this.getChildControl("rb-filter-active"), this.getChildControl("rb-filter-completed") ); this.__rgFilter.addListener("changeSelection", this.__onFilterChanged, this); break; case "rb-filter-all": control = new qx.ui.form.RadioButton("All"); control.setUserData("value", "all"); break; case "rb-filter-active": control = new qx.ui.form.RadioButton("Active"); control.setUserData("value", "active"); break; case "rb-filter-completed": control = new qx.ui.form.RadioButton("Completed"); control.setUserData("value", "completed"); break; case "remove-completed-button": control = new qx.ui.form.Button; control.addListener("execute", function(){ this.fireEvent("removeCompleted"); }, this); break; } return control || this.base(arguments, id); }, __onFilterChanged : function(event) { this.setFilter(event.getData()[0].getUserData("value")); } } }); 

Window.js
 qx.Class.define("todos.Window", { extend: qx.ui.window.Window, properties: { appearance: { refine: true, init: "todo-window" }, todos: { init: [], check: "Array", event: "todosChanged" }, filter: { init: "all", check: ["all", "active", "completed"], apply: "__applyFilter" } }, construct: function(){ this.base(arguments); this.set({ caption: "todos", width: 480, height: 640, allowMinimize: false, allowMaximize: false, allowClose: false }); this.setLayout(new qx.ui.layout.VBox(2)); this.add(this.getChildControl("todo-writer")); this.add(this.getChildControl("todos-scroll"), {flex: 1}); this.add(this.getChildControl("statusbar")); this.addListenerOnce("appear", function(){ this.center(); }, this); }, destruct : function() { var todoItems = this.getTodos(); for (var i= 0, l=todoItems.length; i<l; i++) { todoItems[i].dispose(); } }, members : { // overridden _createChildControlImpl: function(id) { var control; switch(id) { case "todo-writer": var grid = new qx.ui.layout.Grid; grid.setColumnWidth(0, 20); grid.setColumnFlex(1, 1); grid.setColumnAlign(0, "center", "middle"); grid.setColumnAlign(1, "left", "middle"); control = new qx.ui.container.Composite(grid); control.add(this.getChildControl("checkbox"), {row: 0, column: 0}); control.add(this.getChildControl("textfield"), {row: 0, column: 1}); break; case "checkbox": control = new qx.ui.form.CheckBox; control.addListener("changeValue", this.__onCheckAllChanged, this); break; case "textfield": control = new qx.ui.form.TextField; control.setPlaceholder("What needs to be done?"); control.addListener("keydown", this.__onWriterTextFieldKeydown, this); break; case "todos-scroll": control = new qx.ui.container.Scroll; control.add(this.getChildControl("todos-container")); break; case "todos-container": control = new qx.ui.container.Composite(new qx.ui.layout.VBox(1)); break; case "statusbar": control = new todos.StatusBar; control.bind("filter", this, "filter"); this.bind("todos", control, "todos"); control.addListener("removeCompleted", this.__onRemoveCompleted, this); break; } return control || this.base(arguments, id); }, __onWriterTextFieldKeydown : function(event) { var key = event.getKeyIdentifier(); switch(key) { case "Enter": var value = event.getTarget().getValue(); if (value) { event.getTarget().setValue(""); var todo = new todos.ToDo(value); this.getTodos().push(todo); todo.addListenerOnce("remove", this.__onTodoRemove, this); todo.addListener("completedChanged", this.__onTodoCompletedChanged, this); this.__updateTodoList(); this.getChildControl("statusbar").update(); var cbAll = this.getChildControl("checkbox"); cbAll.removeListener("changeValue", this.__onCheckAllChanged, this); cbAll.setValue(false); cbAll.addListener("changeValue", this.__onCheckAllChanged, this); } break; case "Escape": event.getTarget().setValue(""); break; } }, __updateTodoList : function() { var toList; switch(this.getFilter()) { case "all": toList = this.getTodos(); break; case "active": toList = this.getTodos().filter(function(item){return !item.getCompleted();}); break; case "completed": toList = this.getTodos().filter(function(item){return item.getCompleted();}); break; } var container = this.getChildControl("todos-container"); container.removeAll(); toList.forEach(function(item){ container.add(item); }); }, __applyFilter : function() { this.__updateTodoList(); }, __onTodoRemove : function(event) { var todo = event.getTarget(); this.setTodos(this.getTodos().filter(function(item){return item !== todo;})); this.getChildControl("todos-container").remove(todo); todo.dispose(); this.getChildControl("statusbar").update(); }, __onTodoCompletedChanged : function() { var cbAll = this.getChildControl("checkbox"); cbAll.removeListener("changeValue", this.__onCheckAllChanged, this); cbAll.setValue(this.getTodos().length === this.getTodos().filter(function(item){return item.getCompleted();}).length); cbAll.addListener("changeValue", this.__onCheckAllChanged, this); this.__updateTodoList(); this.getChildControl("statusbar").update(); }, __onCheckAllChanged : function(event) { var value = event.getData(); this.getTodos().forEach(function(todo){ todo.removeListener("completedChanged", this.__onTodoCompletedChanged, this); todo.setCompleted(value); todo.addListener("completedChanged", this.__onTodoCompletedChanged, this); }, this); this.__updateTodoList(); this.getChildControl("statusbar").update(); }, __onRemoveCompleted : function() { var completed = this.getTodos().filter(function(item){return item.getCompleted();}); this.setTodos(this.getTodos().filter(function(item){return !item.getCompleted();})); completed.forEach(function(todo){ this.getChildControl("todos-container").remove(todo); todo.dispose(); }, this); this.getChildControl("statusbar").update(); this.getChildControl("checkbox").setValue(false); } } }); 

At this stage, we got quite a functionally complete application. There is only one nuance, it is as scary as an atomic war:



Let's try to bring it to a decent look. I will make a reservation right away, the designer of me is like a ballet dancer, so the maximum task for me is to make our todo sheet look just neat, without frills.

The appearance of the application in qooxdoo correspond to the topics. The framework comes with 4 themes. Topics can be expanded, rewritten, etc. The topic in qooxdoo has 5 components and is defined as follows:

 qx.Theme.define("todos.theme.Theme", { meta : { color : todos.theme.Color, decoration : todos.theme.Decoration, font : todos.theme.Font, icon : qx.theme.icon.Tango, appearance : todos.theme.Appearance } }); 

Read more about the topics here .
So, we will make the following changes:

Appearance.js
 /** * * @asset(qx/icon/Tango/* */ qx.Theme.define("todos.theme.Appearance", { extend : qx.theme.simple.Appearance, appearances : { "todo-window" : { include : "window", alias : "window", style : function(){ return { contentPadding: 0 }; } }, "checkbox": { alias : "atom", style : function(states) { var icon; if (states.checked) { icon = "todos/checked.png"; } else if (states.undetermined) { icon = qx.theme.simple.Image.URLS["todos/undetermined.png"]; } else { icon = qx.theme.simple.Image.URLS["blank"]; } return { icon: icon, gap: 8, cursor: "pointer" } } }, "radiobutton": { style : function(states) { return { icon : null, font : states.checked ? "bold" : "default", textColor : states.checked ? "green" : "black", cursor: "pointer" } } }, "checkbox/icon" : { style : function(states) { return { decorator : "checkbox", width : 16, height : 16, backgroundColor : "white" } } }, "todo-window/checkbox" : "checkbox", "todo-window/textfield" : "textfield", "todo-window/todos-scroll" : "scrollarea", "todo-window/todo-writer" : { style : function() { return { padding : [2, 2, 0, 0] }; } }, "todo-window/statusbar" : { style : function() { return { padding : [ 2, 6], decorator : "statusbar", minHeight : 32, height : 32 }; } }, "todo-window/statusbar/info" : "label", "todo-window/statusbar/rb-filter-all" : "radiobutton", "todo-window/statusbar/rb-filter-active" : "radiobutton", "todo-window/statusbar/rb-filter-completed" : "radiobutton", "todo-window/statusbar/remove-completed-button" : { include : "button", alias : "button", style : function() { return { width : 150, allowGrowX : false }; } }, "todo/label" : { include : "label", alias : "label", style : function(states) { return { font : (states.completed ? "line-through" : "default"), textColor : (states.completed ? "light-gray" : "black"), cursor : "text" }; } }, "todo/icon" : { style : function() { return { cursor : "pointer" }; } }, "todo/text-container" : { style : function() { return { allowGrowY : false }; } }, "todo/checkbox" : "checkbox" } }); 

Color.js
 qx.Theme.define("todos.theme.Color", { extend : qx.theme.simple.Color, colors : { "light-gray" : "#BBBBBB", "border-checkbox": "#B6B6B6" } }); 

Decoration.js
 qx.Theme.define("todos.theme.Decoration", { extend : qx.theme.simple.Decoration, decorations : { "statusbar" : { style : { backgroundColor : "background", width: [2, 0, 0, 0], color : "window-border-inner" } }, "checkbox" : { decorator : [ qx.ui.decoration.MBorderRadius, qx.ui.decoration.MSingleBorder ], style : { radius : 3, width : 1, color : "border-checkbox" } } } }); 

Font.js
 qx.Theme.define("todos.theme.Font", { extend : qx.theme.simple.Font, fonts : { "line-through" : { size : 13, family : ["arial", "sans-serif"], decoration : "line-through" } } }); 

After this, our TODO list will look like this:



At this point you can finish. I did not touch upon a huge number of questions, but this is simply impossible in one article. I wanted to introduce the framework using the example of a small task, diving as little as possible into the details. More information can be found on the links provided. All errors and typos please write in a personal. Thanks for attention.

Useful links:
Qooxdoo home page: http://qooxdoo.org/
SDK download page: http://qooxdoo.org/downloads
Various demos: http://qooxdoo.org/demos
Examples of use: http://qooxdoo.org/community/real_life_examples
SPA tutorial: http://manual.qooxdoo.org/current/pages/desktop/tutorials/tutorial-part-1.html
Github example code: https://github.com/VasisualyLokhankin/todolist_qooxdoo

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


All Articles