Qooxdoo。 TODOリストの作成

今日、非常に多くのjavascriptフレームワークがあり、それらの多くのために山ほどのドキュメントが書かれています。 不明な理由により、ロシアの開発者にはあまり人気がないフレームワークについて詳しく説明します。



フレームワークはqooxdooと呼ばれます。 「kuksdu」と発音します(英語の書き起こしを好む人:['kuksdu:])。



このフレームワークについて書くためにHabréでいくつかの試みがありましたが、それらはすべて、新しいバージョンのリリースに関するニュースまたは「あなたが書いたフレームワークを見る」などの記事のいくつかの段落に要約されました。 私は数年前からqooxdooに取り組んできましたが、このギャップを埋めたいと思います。



どんな動物で、どんな動物と一緒に食べられるかについて簡単に説明します。 何よりも、フレームワークはExtJSに似ています。 この場合、「類似」という言葉は完全に正しいわけではありませんが、より適切なものを選択することは困難です。 プロジェクト開発は、 1&1 Internet AGの腸で始まりました。 最初のパブリックバージョン0.1は2005年にリリースされました。 現在の現行バージョン4.1。それについて説明します。 いくつかの点から、私は開発者が頭脳を作成するときにQtに触発されたと言うことができます。 開発者の最初の主なアイデアは、人々にHTML、CSS、およびDOMモデルの知識なしでWebアプリケーションを開発する機会を与えることです。 qooxdooを使用できます。 たとえば、単一ページアプリケーション(以下、SPAと呼びます)の形で管理者を書く必要があり、単一のHTMLタグを知らないが、CSSを聞いたことがない初心者は、実際にこれを行うことができます。 これは、HTML、CSS、およびDOMモデルの知識が突然突然不要になるという意味ではありません。 単純に、最初は、それらなしで実行できます。 たとえば、特に興味深いのは、Web上で何かをする必要があるデスクトップアプリケーションの開発者です。



記事の最後に、役に立つリンクがいくつかあります。 特に、 本番環境でのフレームワークの実際の使用のさまざまなデモと例へのリンクがあります。



このようなフレームワークについて話すのは退屈で面白くないです。 さらに、開発者はすでにこれを行っています。 したがって、フレームワークの機能を示すために簡単な例を作成することにしました。 多くの人がhttp://todomvc.com/プロジェクトについて知っています。 そのため、qooxdooを使用して可能な限り類似したことを行います。 公平を期して、開発者はすでにtodoシートのデモを行っていますが、これは必要なものではありません。



それでは始めましょう。



SPA(qooxdoo用語のデスクトップ)が考慮されることに注意してください。 まず、qooxdoo sdkをダウンロードする必要があります。 このリンクでこれを行うことができます。 SDKには、アプリケーションテンプレートの生成、デバッグバージョンとリリースバージョンの収集、自動ドキュメント、tutusなどの収集を可能にする多数のユーティリティが含まれています。 こちらからツールチェーンのドキュメントを読むことができます



アプリケーションテンプレートを作成するには、次を実行します。



create-application.py --name=todos
      
      





この操作の後、次のアプリケーションフレームワークを取得します。







アプリケーションは空では生成されません。 アラートが表示されるボタンをクリックすると、ボタンが表示されます。

メインのApplication.jsファイルには、次のコードが含まれます。



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





著者のアイデアを見るために、アプリケーションの販売版または製品版を収集する必要があります。

プロジェクトフォルダーに移動して実行すると、最初のオプションが取得されます。



 ./generate.py source
      
      





2番目は開始後に取得できます。



 ./generate.py build
      
      





その後、対応するindex.htmlファイルをブラウザーにロードし、次の図を確認します。







ボタンは押せますが、押せません。 牛を奪うことができます。 ここで、アプリケーションの機能が終了します。 奇跡は起こらなかったので、コードを作成する必要があります。実際にコードを作成します。



せっかちな人のために、すぐにGitHubへのリンクを提供します。このリンクは、すぐに使える既製のバージョンです。 成功するには、githubからのソースに加えて、SDKをダウンロードし、config.jsonファイルに正しいパス「QOOXDOO_PATH」を設定する必要があります。 次に、上記のように、必要なバージョンを収集する必要があります。



さて、アプリケーションを自然な形で順番に作成するプロセスを検討します。

まず、todoシートのウィンドウウィジェットに空白を作成し、ジェネレーターが生成したすべてをApplication.jsから容赦なく削除します。 以下が得られます。



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





組み立て後、この美しさを見ることができます。







それを意味で満たすときです。 次の要素が必要です:ツールバー、todoシートエントリ、およびシートにエントリを追加するための要素。 レコードTodoシートは繰り返し要素であり、個別のウィジェットとして配置します。 ツールバーとレコードをシートに追加するための要素は、個別のウィジェットとして作成できます。これにより、ウィジェットを再利用したり、ウィンドウの一部にしたりできます。 ツールバーを別のウィジェットにし、レコードを追加する要素をWindowの一部として残し、そうすることができることを示します。 上記のすべてを実行し、ウィジェットに命を吹き込みます。



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





この段階で、機能的に完全なアプリケーションが完成しました。 核戦争のように、警告が1つだけあります。







彼をまともな顔にしようとしましょう。 私はすぐに予約をします、私のデザイナーはヤギのバレリーナのようなものですので、私にとっての仕事は、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シートは次のようになります。







これで、今すぐ終了できます。 膨大な数の質問には触れませんでしたが、これは1つの記事の枠組み内では不可能です。 フレームワークを小さなタスクの例として紹介し、できるだけ詳細を掘り下げたくはありませんでした。 詳細については、提供されているリンクをご覧ください。 PMのすべてのエラーとタイプミスについてお書きください。 ご清聴ありがとうございました。



便利なリンク:

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

Githubサンプルコード: https : //github.com/VasisualyLokhankin/todolist_qooxdoo



All Articles