Qooxdoo. We develop TODO List

Today there are a great many javascript frameworks, mountains of documentation are written for many of them. I would like to dwell on a framework that, for an unknown reason, is not very popular with Russian developers.

The framework is called qooxdoo. Pronounced "kuksdu" (who prefer English transcription: ['kuksdu:]).

There were several attempts at Habré to write about this framework, but they all boiled down to news about the release of a new version or to a couple of paragraphs in articles such as “see what frameworks you 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 suitable one. Project development began in the bowels of 1 & 1 Internet AG. The first public version 0.1 was released in 2005. The current current version 4.1, and we will talk about it. Some points allow me to say that the developers were inspired by Qt when creating their brainchild. The main initial idea of ​​developers is to give people the opportunity to develop web applications without knowledge of HTML, CSS and DOM models. Using qooxdoo is possible. A beginner who needs to write, for example, an admin 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, can really do this. This does not mean that knowledge of HTML, CSS, and the DOM model suddenly abruptly become 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 the real use of the framework in production.

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

So let's get started.

It should be noted that SPA (Desktop in qooxdoo terminology) will be considered. First you need to download qooxdoo sdk. You can do this at this link . The SDK contains a number of utilities that allow you to generate an application template and collect the debug and release versions, collect automatic documentation, tutus, etc. You can read the documentation on the toolchain here .

To create the application template, we 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 an 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, e.g. 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 idea of ​​the authors, we will need to collect a sales 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 starting:

./generate.py build

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



You can click on the button, but you can not click. You can rob the cows. This is where the possibilities of the application end. A miracle did not happen, then we will have to write code, which is what we will do.

For the impatient, I immediately give a link to github with a ready-made version that you can play with. In order to succeed, in addition to the source from the github, you need to download the SDK and set 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 with, we will create a blank for the window widget for our todo sheet and mercilessly remove from Application.js everything that the generator generated for us there. We get 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 this beauty:



It's time to fill it with meaning. We will need the following elements: a toolbar, a todo sheet entry, and an element to add an entry to the sheet. Record todo sheet is a repeating element, we will arrange it as a separate widget. The toolbar and the element of adding an entry to the sheet can be made as separate widgets, which will allow them to be reused, or part of the Window. We will make the toolbar a separate widget, and leave the element for adding a record as part of the Window to show that it is possible and so on. We will 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(""+itemsLeft+" 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

На этом этапе мы получили вполне себе функционально законченное приложение. Есть только один нюанс, оно страшно, как атомная война:



Попробуем привести его к пристойному виду. Оговорюсь сразу, дизайнер из меня, как из козла балерина, поэтому задача максимум для меня добиться, чтобы наш todo лист выглядел просто аккуратно, без изысков.

За внешний вид приложения в qooxdoo отвечают темы. Фреймворк поставляется с 4 темами. Темы можно расширять, переписывать и т.д. Тема в qooxdoo имеет 5 составляющих и определяется таким образом:

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
  }
});

Подробнее про темы можно почитать тут.
Итак, сделаем следующие изменения:

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"
    }
  }
});

После этого наш TODO лист будет выглядеть так:



На этом пока можно закончить. Я не затронул огромное количество вопросов, но это просто невозмозможно в рамках одной статьи. Хотелось познакомить с фреймворком на примере небольшой задачи, как можно меньше углубляясь в детали. Подробнее можно почитать по приведенным ссылкам. Обо всех ошибках и опечатках прошу писать в личку. Спасибо за внимание.

Полезные ссылки:
Домашняя страница qooxdoo: http://qooxdoo.org/
Страница загрузки SDK: http://qooxdoo.org/downloads
Разнообразные демо: http://qooxdoo.org/demos
Примеры использования: http://qooxdoo.org/community/real_life_examples
SPA туториал: http://manual.qooxdoo.org/current/pages/desktop/tutorials/tutorial-part-1.html
Код примера на гитхабе: https://github.com/VasisualyLokhankin/todolist_qooxdoo

Also popular now: