Записки Вредного программиста

enjoy, motherfuckers ;)

Собеседование и Unobtrusive Javascript на Rails

Приветствую всех гостей и постояльцев на своем техническом, иногда не совсем, блоге. Кто читает меня в твиттере, наверное, помнят то, что вчера у меня было первое в жизни собеседование на ruby on rails вакансию, кто не читает, милости просим дружить в твиттере.

Встретились мы сначала с генеральным директором, он рассказал о проекте, пока его не буду называть, но это компания, которая занимается развитием сайта, посвященному туризму: туры, бронирование отелей, такси и прочей смежной тематикой. Обсудили нетехнические вопросы. Затем подошел старший программист и по большей части мы уже общались непосредственно с ним. Конечно, сказывалось отсутствие большого опыта программирования на ruby и ruby on rails, но на большую часть вопросов, как мне кажется, я ответил правильно. Вопросы были следующего характера:

  • какие виды связей и как их создать в rails
  • чем отличается include от extend’а
  • имеется ли множественное наследование в ruby

Параллельно с вопросами я рассказывал о своих проектах и на ruby, и на php. Рассказал почему захотел перейти с php на ruby, как решал задачу из предыдущего поста тоже рассказал :).

После собеседования мне дали тестовое задание, оно заключается в следующем: необходимо без сторонних гемов, плагинов и библиотек реализовать простое приложение по управлению записями (обычный CRUD), только все это дело должно располагаться на одной странице, все запросы должны происходить в фоне (ajax) с применением unobtrusive javascript’а. Еще мне нужно было засечь сколько времени займет все это дело.

На ознакомление с мат. частью (railscasts, guides.rubyonrails и пр.), так сказать, ушло около двух часов, на реализацию проекта столько же. Что получилось и ссылку на репозиторий вы можете увидеть в конце данной заметки.

Итак, начнем. Единственную “плюшку”, которую я применил в проекте был gem ‘twitter-bootstrap-rails’ для придания “красивости”, на конечную функциональность он никак не повлиял.

Сначала, как и всегда, создаем проект rails new unobtrusive && unobtrusive. Вписываем в Gemfile, что будем использовать twitter-bootstrap-rails gem ‘twitter-bootstrap-rails’ и делаем bundle, чтобы обновить Gemfile.lock

Далее установим нужные стили, js-файлы, написав в консоли rails g bootstrap:install и обновим наш шаблон application командой rails g bootstrap:layout application fluid.

Скаффолдом создадим все необходимое для задачи rails g scaffold item name:string description:string, теперь мы можем создавать, удалять и редактировать, но без ajax’а. Также давайте добавим красоты нашим item’ам rails g bootstrap:themed Items. Сейчас можно зайти на /items своего приложения и увидеть что получилось. Но нам пока рано останавливаться.

Давайте добавим валидацию, которую я упустил, в файл app/models/item.rb

1
2
3
4
class Item < ActiveRecord::Base
  validates_presence_of :name, :description
  validates_uniqueness_of :name
end

Первой строкой мы добавляем, как, наверное, ясно из название, наличие и имени, и описания, второй мы проверям уникальность имени, чтобы не было двух айтемов с одним и тем же именем.

Далее давайте немного изменим нашу форму app/views/items/_form.html.erb

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
<%= form_for @item, :remote => true, :html => { :class => 'form-horizontal' } do |f| %>
  <div id="item_errors" style="display:none; color: red"></div>

  <fieldset>
    <legend><%= controller.action_name.capitalize %> /Item</legend>

    <div class="control-group">
      <%= f.label :name, :class => 'control-label' %>
      <div class="controls">
        <%= f.text_field :name, :class => 'text_field' %>
      </div>
    </div>

    <div class="control-group">
      <%= f.label :description, :class => 'control-label' %>
      <div class="controls">
        <%= f.text_field :description, :class => 'text_field' %>
      </div>
    </div>

    <div class="form-actions">
      <%= f.submit nil, :class => 'btn btn-primary' %>
      <%= link_to 'Cancel', items_path, :class => 'btn' %>
    </div>
  </fieldset>
<% end %>

Небольшого изменения коснулась первоя строка, а именно добавился атрибут :remote => true, что сообщает рельсам задейстовать ujs (unobtrusive javascript). Также был добавлен тег с id item_errors, где мы будем выводить все ошибки валидации нашей модели.

Так как у нашего приложение будет только одна страница, можно смело избавляться от других вьюшек, заодно изменим наш контроллер app/controllers/items_controller.rb

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
class ItemsController < ApplicationController

  before_filter do
    @items = Item.all
    @item = Item.new
  end

  before_filter :find_an_item, :only => [:edit, :update, :destroy]

  def index
  end

  def create
    @item = Item.new(params[:item])
    if @item.save
      flash[:notice] = "Successfully created item."
      @items = Item.all
    end
  end

  def edit
    @items = Item.all
  end

  def update
    if @item.update_attributes(params[:item])
      flash[:notice] = "Successfully updated item."
    end
    @items = Item.all
    render :action => 'create'
  end

  def destroy
    @item.destroy
    flash[:notice] = "Successfully destroyed item."
    @items = Item.all
  end

  protected
    def find_an_item
      @item = Item.find(params[:id])
    end

end

Тут я, конечно, перемудрил с before_filter’ами, но это то, как смог сделать за два часа. Как видите, контроллер шан стал намного полегче от того состояния, в котором он был после скаффолдинга: мы избавились от редиректов и вынести многое в before_filter’ы.

Далее я добавил в шаблон app/view/layouts/application.html.erb рядом с тем где выводится контент экшнов, вывод flash сообщения.

1
2
3
4
<div class="row-fluid">
  <div class="span9"><span id="flash_notice" style="color:green"></span></div>
</div>
<%= yield %>

Это не весь шаблон, а только измененная его часть.

Дело за вьюшками, создадим _items.html.erb

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<table class="table table-striped">
  <thead>
    <tr>
      <th>ID</th>
      <th>Name</th>
      <th>Description</th>
      <th>Actions</th>
    </tr>
  </thead>
  <tbody>
    <% @items.each do |item| %>
      <tr>
        <td><%= item.id %></td>
        <td><%= link_to item.name, item_path(item) %></td>
        <td><%= item.description %></td>
        <td>
          <%= link_to 'Edit', edit_item_path(item), :class => 'btn btn-mini', :remote => true %>
          <%= link_to 'Destroy', item_path(item), :method => :delete, :confirm => 'Are you sure?', :class => 'btn btn-mini btn-danger', :remote => true %>
        </td>
      </tr>
    <% end %>
  </tbody>
</table>

Здесь нет тоже ничего необычного, кроме атрибута :remote => true у ссылок. Тем самым мы сообщаем рельсам, чтобы они инициировали ajax запрос, вместо обычного get’а.

Так как мы создали все необходимые partial’ы, мы модем изменить наш index view.

1
2
3
<div id="item_form"><%= render 'form' %></div>
<h1>Items</h1>
<div id="items_list"><%= render 'items' %></div>

Который только собирает воедино нашу форму и список айтемов. Если вы запустите сейчас приложение и попытаетесь создать что-либо или отредактировать, получите множество сообщений об ошибках, связанных с отсутствием нужных шаблонов (если вы удалили все вьюшки, кроме index.html.erb).

Теперь переходим к магии: вмето того, чтобы отдавать html файлы, мы будем отдавать javascript, который rails вставит в нушное место. Вместо create.html.erb, у нас будет app/views/items/create.js.erb

1
2
3
4
5
6
7
8
9
10
11
12
<% if @item.errors.any? -%>
  $("#flash_notice").hide(300);
  $("#item_errors").html("<%= escape_javascript(@item.errors.full_messages.join '
').html_safe %>");
  $("#item_errors").show(300);
<% else -%>
  $("#item_errors").hide(300);
  $("#flash_notice").html("<%= escape_javascript(flash[:notice]) %>");
  $("#flash_notice").show(300);
  $(":input[type=text]").val('');
  $("#items_list").html("<%= escape_javascript(render 'items') %>");
<% end -%>

Вкратце мы делаем следующее: если есть ошибки, то выводим их, если нет, то очищаем наши текстовые поля и обновляем список айтемов.

Самая длинная строчка в этом файле мне не очень понравилась, может можно как-то попроще, покороче?

Если запустить наше приложение, то вы сможете создавать новые записи, видеть сообщения об успешном или неуспешном (если возникли ошибки валидации) создании.

Также поступим с двумя оствшимися вьюшками app/views/items/edit.js.erb

1
$("#item_form").html("<%= escape_javascript(render 'form')%>");

Где мы просто обновляем наш список элементов.

и app/views/items/destroy.js.erb

1
2
3
$("#item_errors").hide();
$("#flash.notice").html("<%= escape_javascript(flash[:notice]) %>").show();
$("#items_list").html("<%= escape_javascript( render 'items') %>");

Вот и все :) Заранее хочу извиниться за сумбурное, местами беглое, изложение, но я сейчас нахожусь в ожидании ответа от работодателя и не могу спокойно сидеть на месте :).

Репозиторий доступен, как всегда на github, работающее приложение на heroku

P.S. деплой на heroku

  • запускаем RAILS_ENV=production bundle exec rake assets:precompile, чтобы скомпилировать наши стили и javascript’ы
  • заменяем в Gemfile sqlite3 на pg и далеем bundle
  • создаем приложение на heroku heroku create --stack cedar (у вас приложение должно быть под git’ом и вы должны войти в heroku аккаунт)
  • git push heroku master отправляем наше приложение за тысячи километров :)
  • на heroku сайт пишут, что нужно сделать heroku run rake db:migrate, но у меня с этим не вышло, поэтому я запустил эту же команду в фоновом режиме heroku run:detached rake db:migrate
  • открываем приложение
  • наслаждаемся
  • и еще раз наслаждаемся :)

Комментарии