Свой шаблон для генератора контроллеров в Rails 3

Апрель 20th, 2011

Как известно, в Rails 3 можно использовать новый стиль кодирования, основанный на применении респондеров (Responders). То есть, форматы ответов задаются при помощи ключевого слова respond_to, а в экшенах используется respond_with.

Однако, нисмотря на то, что в таком стиле вы можете писать свои контроллеры уже сейчас, rails-генераторы (scaffold, controller и resource) все еще генерируют контроллеры на основе старого шаблона, в котором используется старый стиль кодирования. Раньше мне приходилось вручную переводить вновь сгенерированные контроллеры в новый стиль, пока я не переопределил стандартный шаблон контроллера.

В Rails 3 теперь можно удобно переопределить стандартный шаблон генератора контроллеров. Самый простой способ, это просто положить готовый шаблон (он приведен в конце статьи) в файл:

yourapp/lib/templates/rails/scaffold_controller/controller.rb

В этом случае, шаблон будет автоматически “подхватываться” при генерировании контроллера.

Второй способ может понадобиться, если нужно переопределить шаблон из гема или плагина. Для этого нужно сначала прописать в файле lib/yourgemname.rb вашего гема следующее:

require 'rails/all'

module Yourgemname
  class Railtie < ::Rails::Railtie
    config.generators.scaffold_controller = :yourgemname_controller
  end
end

Этим мы указываем, что генератор контроллеров должен использовать наш генератор. Файл генератора должен располагаться по пути lib/generators/rails/yourgemname_controller_generator.rb Теперь давайте посмотрим, как выглядит наш собственный генератор:

require 'rails/generators/rails/scaffold_controller/scaffold_controller_generator'

module Rails
  module Generators
    class YourgemnameControllerGenerator < ScaffoldControllerGenerator
      source_root File.expand_path("../templates", __FILE__)
    end
  end
end

Здесь все просто - мы наследуемся от базового класса генератора и указываем новый путь к шаблонам. Шаблон должен располагаться по пути lib/generators/rails/templates/controller.rb. Теперь посмотрим, как выглядит сам шаблон в стиле Rails 3:

class <%= controller_class_name %>Controller < ApplicationController
<% unless options[:singleton] -%>
  # GET /<%= table_name %>
  # GET /<%= table_name %>.xml
  def index
    @<%= table_name %> = <%= orm_class.all(class_name) %>
    respond_with(@<%= table_name %>)
  end
<% end -%>

  # GET /<%= table_name %>/1
  # GET /<%= table_name %>/1.xml
  def show
    @<%= file_name %> = <%= orm_class.find(class_name, "params[:id]") %>
    respond_with(@<%= file_name %>)
  end

  # GET /<%= table_name %>/new
  # GET /<%= table_name %>/new.xml
  def new
    @<%= file_name %> = <%= orm_class.build(class_name) %>
    respond_with(@<%= file_name %>)
  end

  # GET /<%= table_name %>/1/edit
  def edit
    @<%= file_name %> = <%= orm_class.find(class_name, "params[:id]") %>
  end

  # POST /<%= table_name %>
  # POST /<%= table_name %>.xml
  def create
    @<%= file_name %> = <%= orm_class.build(class_name, "params[:#{file_name}]") %>
    flash[:notice] = '<%= class_name %> was successfully created.' if @<%= orm_instance.save %>
    respond_with(@<%= file_name %>)
  end

  # PUT /<%= table_name %>/1
  # PUT /<%= table_name %>/1.xml
  def update
    @<%= file_name %> = <%= orm_class.find(class_name, "params[:id]") %>
    flash[:notice] = '<%= class_name %> was successfully updated.' if @<%= orm_instance.update_attributes("params[:#{file_name}]") %>
    respond_with(@<%= file_name %>)
  end

  # DELETE /<%= table_name %>/1
  # DELETE /<%= table_name %>/1.xml
  def destroy
    @<%= file_name %> = <%= orm_class.find(class_name, "params[:id]") %>
    @<%= orm_instance.destroy %>
    respond_with(@<%= file_name %>)
  end
end

Вот и все. Теперь при генерировании нового контроллера будет использоваться наш шаблон.

P.S: Хочу предупредить, что в Rails 3.1 (которая edge на момент написания статьи) это не будет работать в некоторых случаях, так как в этой версии есть небольшие изменения в шаблоне, а в приведенном мною шаблоне они не учитываются.

Подробнее о хранении товаров с различными свойствами в БД

Март 5th, 2011

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

Первый класс - это класс, который обеспечивает бестабличную ActiveRecord модель, если от него отнаследоваться. Он лежит в app/lib/tableless.rb

class Tableless
  include ActiveModel::Validations
  include ActiveModel::Conversion
  extend ActiveModel::Naming

  def initialize(props = {})
    props.each do |name, value|
      send("#{name}=", value)
    end
  end

  def persisted?
    false
  end
end

Этот код лежит в app/models/product_kinds/base.rb. Код обеспечивает поддержку групп свойств и свойств для моделей товаров (:group, :prop), а так же содержит базовую модель Base с двумя общими для всех типов товаров свойствами :name и :price.

Все остальные классы типов товаров должны наследоваться именно от Base. Смотрим код:

module ProductKinds
  module Props
    class PropOptions
      attr_accessor :as, :default, :collection

      def add(options)
        options.each_pair do |option, value|
          send "#{option}=".to_sym, value
        end
      end
    end

    class Prop
      attr_accessor :name

      def options
        @options
      end

      def options=(options)
        @options = PropOptions.new
        @options.add(options)
      end
    end

    module ClassMethods
      def self.extended(base)
        class_eval { attr_accessor :props, :current_group, :options }
      end

      # Set up group
      def group(name, &block)
        self.props       ||= {}
        self.options     ||= {}
        self.current_group = name;

        if block_given?
          instance_eval(&block)
        end
      end

      def prop(name, *options)
        self.props[self.current_group] ||= []

        prop = Prop.new
        prop.name    = name;
        prop.options = options.extract_options!

        self.props[self.current_group] << prop
        attr_accessor name
      end

      def grouped_prop_names
        if self == ProductKinds::Base
          self.props
        else
          properties = superclass.grouped_prop_names

          # Dirty, but works
          properties[:common] ||= []
          properties[:other]  ||= []
          self.props[:common] ||= []
          self.props[:other]  ||= []

          h = {
            :common => properties[:common] + self.props[:common],
            :other  => properties[:other] + self.props[:other]
          }

          h
        end
      end
    end

    module InstanceMethods
      def grouped_prop_names
        self.class.grouped_prop_names
      end
    end
  end

  class Base < Tableless
    extend  Props::ClassMethods
    include Props::InstanceMethods

    group :common do
      prop :name,  :as => :string
      prop :price, :as => :numeric
    end

    validates :name, :presence => true
    validates :price, :presence => true, :numericality => true
  end
end

Так мы описываем тип товара “Автомобиль”. Этот код лежит в app/models/product_kinds/automobile.rb

module ProductKinds
  class Automobile < Base
    group :common do
      prop :color, :as => :string
    end

    group :other do
      prop :max_speed, :as => :numeric
    end

    validates :color,     :presence => true
    validates :max_speed, :presence => true, :numericality => true
  end
end

Вот модель, описывающая тип товара. В БД у нас есть табличка со всеми существующими типами товаров. Этот код лежит в app/models/product_kind.rb

class ProductKind < ActiveRecord::Base
  validates :name, :presence => true, :uniqueness => true
  validates :classname, :presence => true, :uniqueness => true

  has_many :products

  def self.find_model_by_kind(kind)
    self.require_model_by_kind(kind)
    eval("ProductKinds::#{kind.camelize}")
  end

  def self.require_model_by_kind(kind)
    ProductKind.find_by_classname!(kind) # Проверяем существование
    require File.dirname(__FILE__) + "/product_kinds/#{kind}"
  end
end

А это - непосредственно модель, описывающая сам товар. У товара есть свойство :props, в котором в сериализованном виде хранится объект со свойствами товара, например, тот же Automobile. Мы видим геттер и сеттер для props, которые сериализуют и десериализую объект и, соответственно, строку:

require File.dirname(__FILE__) + '/product_kinds/base'

class Product < ActiveRecord::Base
  validates :props,           :presence => true
  validates :product_kind_id, :presence => true

  belongs_to :product_kind

  def props=(model)
    write_attribute(:props, Marshal::dump(model))
  end

  # Restore concrete product model from :props attr
  def props
    return @props_cache unless @props_cache.nil?
    props = read_attribute(:props)

    unless new_record?
      ProductKind.require_model_by_kind(product_kind.classname)
    end

    @props_cache = Marshal::restore(props) if props
    @props_cache
  end
end

Используется в контроллере это примерно так (создание нового товара):

class ProductsController < ApplicationController
  respond_to :html

  def create
    @kind          = params[:product][:kind]
    @product       = Product.new
    @props         = ProductKind.find_model_by_kind(@kind).new(params[:product][:props])
    @product.props           = @props
    @product.product_kind_id = ProductKind.find_by_classname!(@kind).id

    if @props.valid? && @product.valid?
      @product.save!

      flash[:notice] = 'Product was successfully created.'
      redirect_to new_product_image_path(@product)
    else
      render :new
    end
  end
end

P.S.: Знаю, что некоторые куски кода далеко не оптимальны и можно написать лучше и проще. Все в ваших руках - этот код только для примера, как можно сделать :)

Надеюсь, я сумел разъяснить некоторые детали, которые читатели сего блога хотели знать.

Бестабличные модели в Rails 3 и как их можно использовать

Март 3rd, 2011

По-умолчанию модель ActiveRecord маппится на таблицу в БД. Но, бывают случаи, когда нужна модель без привязки к таблице - так удобно генерировать формы и вообще - где еще хранить бизнес-логику, как не в модели?

В Rails 3 ребята подошли к этому с умом. Разработчики вынесли часть логики ActiveRecord в отдельные модули и классы, обеспечив этим слабую связанность компонентов. Теперь ничто не мешает подмешать нужную логику в свой класс. Давайте посмотрим, как может выглядеть такая бестабличная модель:

class Tableless
  # Подмешиваем валидаторы, методы преобразований
  # и методы соглашения об именовании для ActiveRecord
  include ActiveModel::Validations
  include ActiveModel::Conversion
  extend ActiveModel::Naming

  # Полезняшка, чтобы создавать свойства из конструктора
  def initialize(props = {})
    props.each do |name, value|
      send("#{name}=", value)
    end
  end

  # Переопределяем метод persisted?, чтобы он всегда
  # возвращал false. Это сообщает ActiveRecord,
  # что данные модели не сохраняются в БД.
  def persisted?
    false
  end
end

Всё, теперь можно спокойно наследовать модели от класса Tableless - вы получите нормальную модель с валидацией, но без привязки к БД:

class Person < Tableless
  validates :name, :presence => true
  attr_accessible :name
end

Где подобное может пригодиться? Ну, например в форме отправки письма с сайта. Мне это пригодилось, когда нужно было сделать функциональность на сайте, которая позволяла бы создавать различные типы товаров.

У каждого товара, например у мобильного телефона или автомобиля, могут быть как общие свойства (такие как цена или масса), так и свои собственные, как наличие bluetooth для телефона или максимальная скорость для автомобиля.

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

Получилось очень удобно - теперь создать новый тип товара стало действительно быстро и просто, к тому же из моделей типов товара стало очень удобно генерировать формы и списки свойств, а еще каждый тип товара был обеспечен удобной рельсовой валидацией. Вот пример двух таких моделей, для сотового телефона и автомобиля:

module ProductKinds
  class Cellphone < Base
    group :common do
      prop :color,  :as => :string
      prop :weight, :as => :numeric
    end

    validates :color,  :presence => true
    validates :weight, :presence => true, :numericality => true
  end
end

module ProductKinds
  class Automobile < Base
    group :common do
      prop :color, :as => :string
    end

    group :other do
      prop :max_speed, :as => :numeric
    end

    validates :color,     :presence => true
    validates :max_speed, :presence => true, :numericality => true
  end
end

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

Это связано с тем, что необходимо было наличие общих валидируемых свойств (цена) для всех дочерних моделей. Еще вы можете заметить вызов методов group и prop - они подмешиваются в модели Base. Эти методы определяют свойства товара и разбивают эти свойства по группам. Надеюсь, из кода моделей все понятно. Я не буду тут выкладывать код базового класса моделей Base, так как там нет ничего экстраординарного, но если кому потребуется, то попросите в комментах.

Конечно, у такого подхода (хранение сериализованных моделей в БД) есть и минусы. Например, если код модели изменится, то вам нужно будет пройтись по всем сериализованным моделям в БД и обновить их в соответствии с новым кодом. Это, конечно же, можно и нужно автоматизировать в виде rake-задачи.

Второй минус это поиск. Вы не сможете напрямую искать по БД (кто-то еще так ищет, серьезно?) при помощи SQL запросов, но по-моему, это не так уж важно, когда есть такие офигенные инструменты как sphinx и thinking_sphinx. Можно отдавать данные сфинксу в виде XML, рассериализовав модели - не так уж трудно добавить такой функционал.

Хотя, я считаю, плюсов тут гораздо больше. Вроде это все, о чем я хотел рассказать.

Обработка ActionController::RoutingError в Rails 3

Март 2nd, 2011

Раньше, до третьих рельс, исключение маршрутизатора можно было красиво и спокойно перехватить, чтобы показать страницу с ошибкой 404. Достаточно было поместить в ApplicationController следующее:

rescue_from ActionController::RoutingError, :with => :error_not_found

Ну, и определить метод error_not_found, который бы рендерил страницу с 404 ошибкой. Но в Rails 3 произошло изменение - стал использоваться гем rack-mount, который используется в Rails как middleware, то есть как прослойка между вашим приложением и веб-сервером. И исключение RoutingError переехало именно в middleware, так что теперь его не получится отловить в контроллере, так как это исключение отлавливается и рендерится еще до вызова контроллера.

Разработчик Rails, а именно Хосе Валим, назначил этой проблеме статус low и поместил тикет в milestone 3.1, что означает, что решение разработчики родят только в версии Rails 3.1. На момент написания этой статьи текущая версия - 3.0.5, так что ждать еще достаточно долго. Сейчас разработчики думают над тем, как сделать обработку ошибок маршрутизации по-нормальному. Но Хосе предложил временное решение - использовать globbing в маршрутах. Нужно просто в вашем routes.rb в самом конце прописать это:

match "*path", :to => "application#error_not_found"

И добавить метод home#routing_error по вашему вкусу. Мой таков:


class ApplicationController < ActionController::Base
  def error_not_found
    render :template => "/error/404.html.erb", :status => 404
  end
end

Остальные эксепшены, вроде ActionController::UnknownController перехватываются нормально при помощи rescue_from и их можно преспокойно обрабатывать в ApplicationController, например.

Да, еще есть вот такое решение. С использованием гема vidibus-routing_error можно работать с исключением RoutingError как прежде, через rescue_from.

Шаблоны приложений в Rails 3

Март 1st, 2011

Rails 3 вышел несколько месяцев назад, и одним из многих изменений было обновление API для генераторов и шаблонов приложений. Если вы еще не знаете, то теперь в Rails 3 используется Thor для этих целей, что даёт большую модульность и настраиваемость.

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

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

Вот так, например, выглядит шаблон, подготовленный Аароном Самнером:

# Создание rvmrc файла
create_file ".rvmrc", "rvm gemset use #{app_name}"

gem "haml-rails"
gem "sass"
# hpricot и ruby_parser используются гемом haml
gem "hpricot", :group => :development
gem "ruby_parser", :group => :development
gem "nifty-generators"
gem "simple_form"
gem "jquery-rails"

# Аутентификация и авторизация
gem "devise"
gem "cancan"

# rspec, factory girl, webrat, autotest для тестирования
gem "rails3-generators", :group => [ :development ]
gem "rspec-rails", :group => [ :development, :test ]
gem "factory_girl_rails", :group => [ :development, :test ]
gem "webrat", :group => :test
gem "ffaker", :group => :test
gem "autotest", :group => :test

run 'bundle install'

rake "db:create", :env => 'development'
rake "db:create", :env => 'test'

generate 'nifty:layout --haml'
remove_file 'app/views/layouts/application.html.erb' # вместо этого используется nifty layout
generate 'simple_form:install'
generate 'nifty:config'
remove_file 'public/javascripts/rails.js' # jquery-rails заменит стандартный prototype-ujs
generate 'jquery:install --ui'
generate 'rspec:install'
inject_into_file 'spec/spec_helper.rb', "\nrequire 'factory_girl'", :after => "require 'rspec/rails'"
inject_into_file 'config/application.rb', :after => "config.filter_parameters += [:password]" do
  <<-eos

    # Настройка генераторов
    config.generators do |g|
      g.stylesheets false
      g.form_builder :simple_form
      g.fixture_replacement :factory_girl, :dir => 'spec/factories'
    end
  eos
end
run "echo '--format documentation' >> .rspec"

# Настройка аутентификации и авторизации
generate "devise:install"
generate "devise User"
generate "devise:views"
run "db:migrate"
generate "cancan:ability"

# Удаляем ненужные файлы, копируем конфиг БД и добавляем его в игнор
remove_file 'public/index.html'
remove_file 'rm public/images/rails.png'
run 'cp config/database.yml config/database.example'
run "echo 'config/database.yml' >> .gitignore"

# Создаем git-репозиторий и делаем первый коммит
git :init
git :add => "."
git :commit => "-a -m 'create initial application'"

say <<-eos
  ============================================================================
  Your new Rails application is ready to go.

  Don't forget to scroll up for important messages from installed generators.
eos

Достаточно просто, не правда ли? Воспользоваться таким шаблоном можно так:

$ wget https://gist.github.com/848735.txt
$ rails new appname -m 848735.txt

Я сейчас создаю специальный гем, который поможет упростить создание новыйх приложений Rails 3 - этот гем называется playmo-rails, и уже можно посмотреть некоторые наработки. Мой гем так же использует Thor для генерации различных файлов. Этот гем создан для быстрого старта - можно создать новое приложения, добавляя в него нужные библиотеки и расширения, просто отвечая на вопросы установщика. И это очень удобно.

Пока playmo-rails умеет не так уж много:

  • Создает приложение и использованием Compass
  • Лайот приложения использует html5 boilerplate
  • Можно выбрать установку JQuery либо Mootools
  • Удаляет ненужные файлы из нового rails-приложения

Планов по playmo-rails достаточно много, например, добавить поддержку других фреймворков, полезных общеиспользуемых хелперов, таких как title, динамические страницы 500, 404, 422. В общем, в скором времени, playmo-rails будет той штукой, которая избавит вас от всех манипуляций которые вы делаете с каждым новым rails-приложением.

bundle exec - чтобы не забыть

Февраль 22nd, 2011

Очень помогли сегодня в группе ror2ru.
Я хотел подправить один гем, он у меня лежал на диске, склонированный с гитхаба. Но, чтобы установить этот гем (при установке он генерирует несколько файлов, которые-то и нужно было мне подправить), нужно использовать консольную программу, в моем случае compass, которой указываем название гема, который нужно установить:

compass init rails -r html5-boilerplate ...

Но, так как гем у меня в виде склонированной репы, то естественно, программа compass его “не видит”. Выход нашелся в следующем.

Сначала подключаем в Gemfile требуемый гем локально:

gem 'html5-boilerplate', :path => '~/sandbox/gems/html5-boilerplate'

А затем, запускаем нужную команду через bundle exec:

bundle exec compass init rails -r html5-boilerplate ...

Это запустит вашу команду в контексте текущего Gemfile, поэтому, для программы compass гем html5-boilerplate будет прекрасно “виден”. Полезная штука.

DesignMode: инициализация

Февраль 2nd, 2011

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

Я начал работу над своим редактором и хочу представить первые наработки. Проект называется DesignMode. Это - WYSIWYM редактор для редактирования текста с веб-страницы. Я выложил проект на Github: design_mode. Скоро, скорее всего, сяду писать Roadmap проекта.

Код проекта на Github

Форки приветствуются :)

Как должен выглядеть идеальный WYSIWYG

Январь 31st, 2011

В студии Артемия нашего Лебедева выпустили визуальный редактор на JS под названием Реформатор. Именно так в моем понимании должен выглядеть WYSIWYG.
Мне для моих проектов нужен визуальный редактор, чтобы удобно было возиться с текстами на странице. Так вот, я решил написать собственный. Пока у него будет поддержка только FF > 3, мне этого пока достаточно. Я уже написал немного кода и скоро выложу его на github.

Список того, что должно и не должно быть в моём визуальном редакторе:

  • Никаких возможностей для изменения цвета текста или бэкграунда
  • Все стили берем из стилей css файла для контент-зоны сайта, т.е. того места, где располагается текст.
  • В редакторе все должно выглядеть именно так, как будет выглядеть на сайте.
  • Редактор увеличивает свою высоту по мере набора текста.
  • Удобное боковое меню со стилями (как в Реформаторе).
  • Картинки можно перетаскивать с рабочего стола прямо в редактор и сразу изменять их размер при помощи курсора мыши. Не нужно будет сначала загружать картинку на сайт,а потом вставлять ее в текст.
  • Не будет кучи кнопок, в которых легко запутаться.
  • Не будет выравнивания текста. У нас же будут стили. Если стиль предполагает выравнивание текста по правому краю - то пожалуйста. Это - как задал дизайнер. Редактор сайта не должен хотеть сделать того, чего не задумал дизайнер.
  • По-умолчанию будут: выделение (полужирный, италик, зачеркивание), нумерованный и ненумерованный списки, добавление гиперссылки.
  • Будет undo, redo, очистка форматирования, вставка из word (пока не знаю, как сделать такую вставку наиболее удобно).
  • Текст будет автоматически заворачиваться в параграфы. Озаглавленные параграфы будут заворачиваться в тег section.
  • Поддержка только HTML5.
  • Никаких тебе спелл-чекеров, таблиц спец-символов и прочей ненужной блуды.
  • Все это будет написано на mootools, как расово верном фреймворке.
  • Специальная парсилка для стилей контент-зоны.
  • Мануал, как правильно работать с визуальным редактором.
  • … может - чето забыл, напишу, если вспомню.

Еще одна очень важная фишка, которую хочу выделить:

Предполагается, что визуальный редактор будет использоваться прямо на странице со статьёй. Посмотрите на статью, которую сейчас читаете. Представьте, что если бы вы были прямо сейчас наделены админскими правами то, кликнув (гипотетический клик) на статью вы бы смогли ее прямо здесь же редактировать. Что бы произошло при клике на статью? Примерно это:

  1. Вокруг текста статьи появилась бы рамка - признак того, что мы можем редактировать статью. Или нет - еще лучше: все остальное на странице, кроме статьи, затемнилось бы.
  2. Сбоку бы появилось плавающее меню с кнопками. И это меню можно было бы перетаскивать в любую часть вьюпорта по своему усмотрению, а так же сворачивать.
  3. При ухода фокуса со статьи плавающее меню бы исчезало.
  4. Если мы попытаемся закрыть окно, то появляется диалог с прогрессбаром, в котором отображается процесс сохранения статьи. Только после этого окно закрывается.
  5. Где-то будет расположена кнопка принудительного сохранения статьи. Или ее совсем не будет. Тут подумаю еще.

Ждите ссылку на проект, кому это интересно. А от вас жду предложений по улучшению.

Javascript: текущий элемент списка

Январь 13th, 2011

Навеяно работой. Давно хотел об этом написать.

Часто приходится видеть извращение, когда разрабы пытаются реализовать на JS поведение, что нужно выбрать один элемент из списка.

Опишу подробней в виде кода. Итак, даны стили и список:

<style type="text/css">
    ul li.current { color: red; }
</style>

<ul>
  <li>First</li>
  <li class="current">Second</li>
  <li>Third</li>
</ul>

Как обычно делают разрабы:

$().ready(function() {
    var ul  = $('body ul');

    ul.find('li').click(function() {
        ul.find('li.current').removeClass('current');
        $(this).addClass('current');
    })
})

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

Как нужно делать:

$().ready(function() {
    var ul  = $('body ul');
    var cur = ul.find('li.current');

    ul.find('li').click(function() {
        if (cur.length) {
            cur.removeClass('current');
        }

        cur = $(this);
        cur.addClass('current');
    })
})

Тут мы просто запоминаем текущий элемент. Вроде бы все просто, но чаще всего, почему-то встречается первый подход. Да, кода получилось больше (хотя тут можно его уменьшить), но зато мы меньше теребим DOM.

Некий Karneds написал комментарий, как по его мнению стоит решить этот вопрос. Я было засомневался и убрал пост с главной, чтобы проверить, так ли это на самом деле. Догадка была такая, что сначала при обработке дерева DOM находятся все указанные в селекторе таги и только потом происходит поиск по атрибутам найденных тагов. Чтобы проверить мою догадку я обратился к небезызвестному Сергею Чикуёнку:

Есть список:

<ul>
  <li>First</li>
  <li class="current">Second</li>
  <li>Third</li>
</ul>

Если мы напишем обработчик (jquery):

$('ul li.current').click(function() { ... })

то будет ли это означать, что селектор ul li.current проверит сначала первый элемент списка (First) на наличие класса current, прежде чем совпасть со вторым элементом?

И вот что он ответил:

зависит от браузера. В новых используется встроенный querySelectorAll(), в старых (например IE) будет бегать по элементам, и в данном случае будут проверены все LI-элементы (насколько я знаю, поиск идёт справа налево, то есть сначала выберутся все LI элементы на странице, а потом отфильтруются те, которые находятся внутри UL).

То есть для старых браузеров — да, сначала проверит First (порядок элементов сохраняется)

Так что, догадка подтвердилась. Это, кончно, не касается новых браузеров, которые умеют искать и по атрибутам. Хотя, если текущий элемент будет помечен не атрибутом, а свойством объекта, то всё же касается.

Мораль сей басни - лучше всегда “сохранять” текущий элемент списка в отдельной переменной, а не искать его каждый раз заново.

P.S: Для Тормоза - извиняй, специально для тебя публикую пост с новым адресом, дабы он отметился в RSS ленте.

Уведомлялка о новых твитах для Ubuntu на Ruby

Декабрь 24th, 2010

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

require 'twitter'
last_created_at = 0

Twitter.configure do |config|
  config.consumer_key       = 'your key here'
  config.consumer_secret    = 'your key here'
  config.oauth_token        = 'your token here'
  config.oauth_token_secret = 'your token here'
end

client = Twitter::Client.new

loop do
  tweet = client.home_timeline.first

  if DateTime.parse(tweet.created_at) > last_created_at
    system("notify-send -u normal -t 5000 -i info '#{tweet.user.screen_name}' '#{tweet.text}'")
    last_created_at = DateTime.parse(tweet.created_at)
  end

  sleep 60
end

Эта штука ползает каждую минуту в мой твиттер и показывает, что там новенького. Есть небольшая недоработка: если с предыдущего захода появятся, например, 2 новых твита, то будет показан только крайний. Если кому не лень это исправить - прошу кинуть код в комменты.

Как это юзать? Для начала нужно установить:

  • sudo apt-get install libnotify-bin
  • gem install twitter
  • Зарегистрировать свое приложение тут http://twitter.com/apps/new и вставить в скрипт в конфигурацию полученные ключи и токены

Дальше сохранить предоставленный код в файл и запустить его из консоли:

$ ruby twitter.rb&

Результат выглядит так:
2iho

UPD 13.01.2011

Кирилл Никитин прислал мне исправленный вариант, который я реквестовал выше, за что ему решпект.

require 'twitter'

last_id = nil
tweets = []

Twitter.configure do |config|
  config.consumer_key       = 'your key here'
  config.consumer_secret    = 'your key here'
  config.oauth_token        = 'your token here'
  config.oauth_token_secret = 'your token here'
end

client = Twitter::Client.new

loop do
  if last_id
    tweets = client.home_timeline :since_id => last_id
  else
    tweets << client.home_timeline.first
  end

  last_id = tweets.first.id unless tweets.empty?

  tweets.reverse.each do |tweet|
    system("notify-send -u normal -t 5000 -i info '#{tweet.user.screen_name}' '#{tweet.text}'")
  end

  tweets.clear
  sleep 60
end