Сегодня я постараюсь показать, как можно использовать возможности недавно вышедшей библиотеки QooxDoo 0.6 RC1 в сочетании с Ruby on Rails.

Зачем (небольшое предисловие)

Для создания RIA-приложений можно использовать различные инструменты. В Ruby on Rails есть встроенная поддержка AJAX на базе библиотек Prototype и Scriptaculous, с их помощью можно делать достаточно гибкие интерфейсные возможности используя JavaScript и движок браузера. Недавно вышедший WebORB уже позволяет стыковать Ruby on Rails с Adobe Flex 2, с его помощью можно делать навороченные интерфейсы используя собственный движок Flex на базе Flash и ActionScript 3.

И у того, и у другого подхода есть как плюсы, так и минусы. В качестве плюсов Prototype можно назвать легкость в использовании, гибкость, малый вес и независимость от установленных компонентов (например, Flash). В качестве минусов - наличие всего лишь базового функционала для создания интерактивных интерфейсов.

Основной плюс Flex - это то, что Flex, по сути, самостоятельный framework с огромным набором компонентов, продуманным и задокументированным API. Минусы - отсутствие open-source среды разработки и зависимость от Flash-проигрывателя с достаточно ограниченными возможностями для интеграции с браузером (хотя бы тот факт что я не могу отобразить внутри Flex любой нужный мне HTML - это для моих задач просто неприемлимое ограничение).

Оба эти подхода стали для меня неприемлимы когда возникла задача создать интерфейс, в котором широкий набор компонентов сочетался бы с гибкостью в представлении информации. В частности, встал вопрос об импортировании готовых HTML-документов с произвольным содержанием и отображении их внутри интерфейса. От Prototype пришлось отказаться, т.к. он примитивен, а от Flex - в связи с тем, что он не умеет отображать внутри себя хоть сколько-нибудь сложный HTML. Пришлось искать третий вариант.

QooxDoo

QooxDoo по набору визуальных компонентов уже вполне сопоставим с Flex. Во всяком случае, он гораздо ближе подошел к званию полноценного JavaScript-фрэймворка чем Prototype. С его помощью уже можно делать достаточно навороченные интерфейсы, работающие с серверной стороной. Основной плюс - это то, что он не требует никаких дополнительных компонентов и работает используя исключительно возможности браузера. Основной минус - большой вес библиотеки (код всей библиотеки весит примерно 950 килобайт, плюс еще заргужаемые картинки и иконки для интерфейса).

Для меня он хорош тем, что может полноценно работать с HTML, в том числе отображать его внутри себя или себя внутри него. Это очень удачно вписывается в концепцию конкретной задачи, которую мне необходимо решить.

Первые шаги

При выборе идеи для демонстрации возможностей QooxDoo, я не буду далеко ходить и возьму за основу идею статьи Rolling with Ruby on Rails, уже успевшей стать классическим документом для быстрого ознакомления с Ruby on Rails (если еще не читали - это большое упущение). Будем создавать книгу рецептов.

Итак, начнем. Создаем новый проект:

rails qooxdoo_cookbook

Заходим в папку проекта, создаем нашу основную модель:

ruby script/generate model Recipe

В файле db/migrate/001_create_recipes.rb прописываем следующее:

class CreateRecipes < ActiveRecord::Migration
  def self.up
    create_table :recipes do |t|
      t.column :title, :string
      t.column :description, :string
      t.column :instructions, :text
      t.column :created_at, :datetime
    end
  end

  def self.down
    drop_table :recipes
  end
end

Это наши инструкции для создания таблицы в базе данных проекта. Создаем БД и запускаем миграцию:

rake migrate

Все, таблица у нас есть, данные есть куда сложить. Теперь делаем контроллер:

ruby script/generate controller Recipes

Файлы контроллера созданы. В файле app/controllers/recipes_controller.rb прописываем:

class RecipesController < ApplicationController
  scaffold :recipe
end

Теперь мы можем запустить сервер и добавить некоторые данные в нашу таблицу:

ruby script/server

Вбиваем некоторые данные в таблицу:

Теперь нам нужно к нашему проекту пристыковать QooxDoo. Для этого мы из скачанного архива (для быстрого старта лучше качать готовый build, чтоб не возиться с компиляцией) в папку public нашего проекта полность копируем папку framework и переименуем ее в qooxdoo (для наглядности). Папочка эта весьма внушительная по размеру, в основном из-за картинок.

Теперь мы создаем интерфейс. В контроллере RecipesController (app/controllers/recipes_controller.rb) мы добавляем пустой метод:

class RecipesController < ApplicationController
  scaffold :recipe

  def index
  end

end

В шаблоне для этого метода (app/views/recipes/index.rhtml) прописываем следующее:

<html>
  <head>
    <script type="text/javascript" src="/qooxdoo/script/qx.js"></script>
    <script type="text/javascript" src="/javascripts/application.js"></script>
  </head>
  <body>
  </body>
</html>

В файле public/javascripts/application.js мы инициализируем наш QooxDoo-интерфейс:

qx.core.Init.getInstance().defineMain(function() {
  // Получаем основной документ
  var rootDoc = qx.ui.core.ClientDocument.getInstance();

  // Создаем заголовок формы и добавляем его в основной документ
  var recipeLabel = new qx.ui.basic.Label("Recipes", "R");
  with(recipeLabel){
    setLeft(10);
    setTop(5);
  }
  rootDoc.add(recipeLabel);

  // Создаем элемент "список", в который мы будем принимать данные с сервера
  var recipeList = new qx.ui.form.List();
  with(recipeList){
    setLeft(10);
    setTop(20);
    setWidth(400);
    setHeight(200);
  }
  rootDoc.add(recipeList);

  // Создаем кнопку для отправки запроса на сервер
  var listRecipesButton = new qx.ui.form.Button("List Recipes");
  with(listRecipesButton){
    setLeft(10);
    setTop(230);
    setWidth(70);
  }
  rootDoc.add(listRecipesButton);

});

Если мы теперь откроем проект в браузере то на странице /recipes/ мы увидим следующее:

Qooxdoo Interface

Мило, не правда ли? :) Старые добрые кнопочки.

Однако, одних только кнопочек недостаточно. Нам необходимо сделать так, чтобы эти кнопочки взаимодействовали с серверной стороной. Для начала привяжем получение списка рецептов. Для этого модифицируем application.js:

qx.core.Init.getInstance().defineMain(function() {
  // Получаем основной документ
  var rootDoc = qx.ui.core.ClientDocument.getInstance();

  // Добавляем объект для связи с сервером
  var recipeService = new qx.io.remote.RemoteRequest(”/recipes/list”, “GET”);
  with(recipeService){

    // Говорим серверу что данные нам нужны в XML
    setRequestHeader(”Accept”, “text/xml”);

    // Вешаем событие, которое вызывается при успешном завершении запроса
    addEventListener(”completed”,
      function(e){
        // Преобразуем полученный XML в объекты с помощью qx.client.Builder и добавляем их в объект recipeList
        new qx.client.Builder().build(recipeList, e.getData().getContent());
      }
    );
  }

  // Вызов удаленного сервиса
  function loadRecipes(){
    recipeService.send();
  }

  // Создаем заголовок формы и добавляем его в основной документ
  var recipeLabel = new qx.ui.basic.Label(”Recipes”, “R”);
  with(recipeLabel){
    setLeft(10);
    setTop(5);
  }
  rootDoc.add(recipeLabel);

  // Создаем элемент “список”, в который мы будем принимать данные с сервера
  var recipeList = new qx.ui.form.List();
  with(recipeList){
    setLeft(10);
    setTop(20);
    setWidth(400);
    setHeight(200);
  }
  rootDoc.add(recipeList);

  // Создаем кнопку для отправки запроса на сервер
  var listRecipesButton = new qx.ui.form.Button(”List Recipes”);
  with(listRecipesButton){
    setLeft(10);
    setTop(230);
    setWidth(70);

    // Вешаем событие, которое вызывается по нажатию кнопки
    addEventListener(”execute”, loadRecipes);
  }
  rootDoc.add(listRecipesButton);

});

Теперь по нажатию на кнопку List Recipes будет отправляться запрос на сервер. Однако, со стороны сервера должен прийти адекватный ответ. Для этого модифицируем контроллер RecipesController, а именно метод list:

class RecipesController < ApplicationController
  scaffold :recipe

  def index
  end

  def list
    @recipe_pages, @recipes = paginate :recipes

    respond_to do |wants|
      wants.xml{render :partial => “list”}
      wants.html{render_scaffold}
    end
  end

end

Теперь если браузер запросит результат в виде HTML, то скрипт выдаст стандартный scaffold. Если же потребует XML - будет рендериться некий partial, который мы задаем в файле app/views/recipes/_list.rxml:

xml.tag!("qx.client.builder.Container"){
  @recipes.each do |recipe|
    xml.tag!("qx.ui.form.ListItem", :label => recipe.title, :value => recipe.id)
  end
}

Вот вроде и всё. Открываем страницу /recipes/, жмем кнопку - и, в идеале, мы должны получить те данные, которые вбивали в самом начале:

Если же мы в самом начале забыли ввести данные или хотим чтобы данных было побольше - открываем страницу /recipes/list/ и вводим их через обычный scaffold :) Вуаля!

Итог

В общем и в целом, мне нравится работать с QooxDoo. Конечно, есть ряд существенных минусов, о которых тоже нельзя умолчать:

* Документация в большинстве своем содержит только перечисление свойств, методов и их параметров. Полноценно задокументированы лишь некоторые классы и методы. Однако, при наличии некоторого опыта, разобраться все же можно, благо названия все говорящие, да и примеры использования классов тоже есть (папка demo/test)
* Не самая высокая скорость работы библиотеки. А что вы хотели от почти метра JavaScript’овых кодов? Для такого объема - более чем прилично. Тем более что, насколько я понимаю, можно делать custom-билды (но об этом как-нибудь отдельно)
* Отсутствие средств разработки (IDE). С этим пока что ничего не поделаешь.

Приятного вам кода.