Popular Posts

Показаны сообщения с ярлыком javascript. Показать все сообщения
Показаны сообщения с ярлыком javascript. Показать все сообщения

пятница, 13 апреля 2012 г.

Rails Autocomplete

Привет.
Сегодня рассмотрим автозаполнение (autocomplete) модная штука, с ajax. Для этого я буду использовать наработки. Помните пост, где делали теги? Вот именно для них я буду и делать автозаполнение.
И так у нас есть модель пост, с добавлеными туда тегами.
class Post < ActiveRecord::Base
    acts_as_taggable_on :tags
end
Конечно там больше кода, но важно видеть, что я не обманываю и там действительно есть теги.
Хорошо, теперь добавляем в контроллер метод для который будет отдавать все теги:
class PostsController < ApplicationController
  def get_tags
    @tags = Post.tag_counts.order('count DESC').limit(3).where('tags.name LIKE ?', "%#{params[:q]}%")
    render :get_tags, :layout => false
  end
end

Здесь простой запрос  к методу tag_counts, который автоматически генерируется acts_as_taggable_on, потом я ограничиваю количество тегов до 3-х, хотя можно и не ограничивать, и буду искать все имена тегов (tags.name), которые похожи на %params[:q]%,
это говорит о том, что искомый тег может начинаться и заканчиваться с любого символа. То есть  я буду отправлять запрос вида :q => "a", и вот эта буква "а" может встречаться в любом месте имени.
Ответ как видим не требует всего layout, потому что иначе будет отправлена в ответе вся страница со всеми файлами и прочим, нам же нужны только теги.

Добавляем, теперь этот самый шаблон страницы.
#views/posts/get_tags.html.erb
<ul>
<% @tags.each do |tag| %>
    <li><%= tag.name %></li>
<% end %>   
</ul>
Тут как видите просто, оборачиваем имя тега (name) в элементы li.
Я забыл сказать, что буду использовать Ajax.Autocompleter от Script.aculo.us. Вот, он требует такого ответа, хотя лучше ответ в json, но это тут не важно.

Затем добавляем в форму нового поста, поле для автозаполнения:
#views/posts/_form.html.erb
<%= form_for(@post) do |f| %>
  #несколько полей

<div class="field">
    <%= f.label 'Tag List' %><br />
    <%= f.text_field :tag_list, {:id => "autocomplete"} %>
    <div data-url="<%= url_for(get_tags_posts_path) %>" id="autocomplete_choices" class="autocomplete"></div>
</div>

<% end %>

Здесь я добавил атрибут data-url к полю для автозаполнения, этот url будет генерироваться автоматически, именно по этому адресу нужно обращаться для получения тегов.

Не забываем указать в ability.rb, кому можно получать теги:
#models.ability.rb
def initialize(user)
 #код

   if user.role?(:user)
        can :get_tags, Post

 #код
end
Это говорит о том, что теги могут получить только вошедшие в систему пользователи.
Так теперь добавим роутинг.
#config/routes.rb
resources :posts do
  #....

   post :get_tags, :on => :collection
end

Всё.

Осталось написать небольшой js-файл для обработки всего.
Сделайте файл в assets/javascripts/autocomplete.js
Вот нужный код

$(document).observe('dom:loaded', function() {
 var token = $$('input[name=authenticity_token]')[0];
 var url = $('autocomplete_choices').readAttribute('data-url');
 new Ajax.Autocompleter("autocomplete", 
                           "autocomplete_choices", url, 
                           {
                            paramName: "q",
                            tokens: [','],
                            requestHeaders: {'X-CSRF-Token': token.getValue()}
                           });

});

Переменная token нужна для предъявления серверу токена пользователя, это для защиты.
Потом мы читаем data-url, который добавили, помните? Это адрес для отправки запроса.
Потом сам Ajax.Autocompleter. У него первый аргумент -- это поле из которого нужно брать символы, второй это поле в которое будет писаться ответ, а потом идут параметры.
paramName: "q", q - это то, что будет в params, которое читаем в контроллере.
tokens: [','], это важный параметр, указывает разделитель между тегами, если его не будет, то будет на автозаполнении только первый самый тег, остальных нет.
requestHeaders, указывает дополнительный заголовок для сервера, тут отправляем token.getValue(), это значение находится в скрытом поле. Можете в исходном коде страницы посмотреть. Без этого заголовка, сервер не принимает нас, и перенаправляет на страницу входа, можете попробывать.
Теперь, нужно же добавить файл, так как автозаполнение нужно будет всего на двух страницах, странице создания и редактирования пост, то добавляем в _form.html.erb и всё.

<%= javascript_include_tag "autocomplete" %>

<%= form_for(@post) do |f| %>

#ваш код
Конечно это вставит код посреди страницы, но это не страшно.
Так последнее действие это добавление в загрузку нужных файлов.
#assets/javascripts/application.js

//
//= require scriptaculous/lib/prototype
//= require scriptaculous/src/effects
//= require scriptaculous/src/controls

Как видно я зугружая библиотеку прототип и нужные файлы из script.aculo.us -- эффекты и контролы. Поэтому разместите папку scriptaculous в assets/javascripts.

Ах да не забудьте добавить стили:

div.autocomplete {
  position:absolute;
  width:250px;
  background-color:white;
  border:1px solid #888;
  margin:0;
  padding:0;
}
div.autocomplete ul {
  list-style-type:none;
  margin:0;
  padding:0;
}
div.autocomplete ul li.selected { background-color: #ffb;}
div.autocomplete ul li {
  list-style-type:none;
  display:block;
  margin:0;
  padding:2px;
  height:32px;
  cursor:pointer;
} 

Ну или напишите свои, эти со страницы autocomplete.


вторник, 28 февраля 2012 г.

PrototypeJS Классы, Наследование и Модули.

Привет!
Сегодня важная тема, я бы сказал -- это наследование и классы и объекты в prototype, ну также всё, что будет показано относится в равной мере и ко всему javascript.

Проблема

Как всегда сначала описание проблемы. Предположим у меня есть мебель: шкаф и диван. Ну да немного, но для задачи достаточно. Как это выглядит со стороны объектов? Понятно, что и то, и то -- мебель, ведь так? Но на диване я сижу, лежу, и так далее, а в шкафу куча дверек, в шкаф я складываю одежду\посуду\да_что_угодно. Но также есть такие общие вещи как цвет. Вот.
Теперь непосредственно проблема, по идее диван и шкаф виды мебели, но диван и шкаф могут иметь разные свойста (для сиденье? сколько двёрок? и так далее). Как же это описать?
Вот тут-то и появляются так называемые MixIn, или модули, которые могут быть в новинку для javascript программистов, но отнюдь не новы для тех, кто пишет на ruby. Но благодаря, природе javascript в нём можно спокойно делать модули и разбавлять объекты. Я написал много слов, теперь напишу код.
И так. Сначала практика создания классов в prototypejs
Тут просто:
var SomeClass = Class.create({
    initialize: function(){}
});
Как просто, да? Сначала делаем класс Class.create, в нём нужна функция конструктор (но она не обязательна) initialize, что сродни ruby методу с таким же названием. Это функция будет инициализироваться при создании объекта. Всё просто.
Всё теперь пишу нужный код:
var Furniture = Class.create({
    initialize: function(title, price)
    {
      this.title = title;
      this.price = price;
    },
    toString: function()
    {
          return this.title + ": " + this.price + "$"
    }
    });
Две функции, инициализации, и вторая функция для вывода информации о мебели.
Этот класс в java я бы сделал абстрактным, потому что просто мебели как бы не существует, это абстаркция, мебель это всегда что-то -- стул, стол, шкаф...

Теперь пишем классы для Шкафа и Дивана:
var Sofa = Class.create(Furniture, {
    initialize: function($super, title, price)
    {
      $super(title, price);
    }
});
var Cupboard = Class.create(Furniture, {
    
    initialize: function($super, title, price)
    {
      $super(title, price);
    }
});
Как вы, надеюсь, поняли, здесь мы наследуем от мебели (Furniture) с помощью Class.create(ExtObj, {}), где ExtObj -- это "родитель". В инициализаторе вызываем $super, это
заставляет проинициализировать код функции родителя. Про $super, читайте в конце поста.

Ok, я это сделал, теперь время подмешать модули.
Пусть есть такие свойста: цвет, можно ли сидеть, сколько дверек в мебели.
//для цвета
var Color = {
      set_color: function(color)
      {
        this.color = color;
      },
      get_color: function()
      {
        return this.color;
      }
};
//Я могу сесть на эту мебель?
var ForSeating = {
    for_seating: function()
    {
      return true;
    }
};
//Сколько дверек в этой мебели?
var ManyDoors = {
    set_doors: function(doors)
    {
        his.doors = doors;
    },
       how_many: function()
    {
      return this.doors;
    }
};
Ok, круто, как видим все эти модули просты Object\ы  в javascript.
Теперь самое интересное и простое, как подмешать? Для этого есть методы Object, который расширяет Prototype -- addMethods()
Получаем:
//Для дивана модуль который говорит, что можно сидеть и модуль цвета
Sofa.addMethods(ForSeating);
Sofa.addMethods(Color);
//Для шкафа цвет и дверки.


Cupboard.addMethods(Color);
Cupboard.addMethods(ManyDoors);
Что получим?

Вот результаты:
Сначала создаём новые:
var sofa = new Sofa('Cool Sofa', 10);
var cupboard = new Cupboard('My Cupboard', 12);


//Устанавливаем цвета и двери для шкафа
sofa.set_color('red');

cupboard.set_color('black');
cupboard.set_doors(3);

//А вот результат уже
sofa.get_color(); // => 'red'
sofa.for_seating(); // => true
cupboard.get_color(); // => 'black'
cupboard.how_many(); // =>  3
Если мы вызовем для шкафа метод, который скажет можно ли на нём сидеть? то получим



Вот так.

Совсем маленький рефакторинг.

Как мы видили, Модуль Color добавляется и в Sofa и в Cupboard, поэтому лучше "подмешать" в родительский класс:

Furniture.addMethods(Color);

Теперь методы этого модуля будут доступны в обоих объектах.

Для чего всё это нужно?

Это позволяет вынести общие методы для разных объектов, что упростит жизнь, ведь кода нужно меньше, вы не будете повторять себя.
Сделает понятнее программу.
Какие возможные траблы?
Возможно затереть методы, очень часто есть методы set()\get(), которые могут быть реализованы для разных модулей, в итоге останется только один, как горец))). Также сложность следить за программой, у вас в модуле появится ссылка this, на объкт + куча свойств объекта, не сразу понятно как этим пользоваться, но это просто, так же в связи с этим можно случайно переопределить свойство, поэтому рекомендую не полениться и написать get_property()\set_property(), так лучше.

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

Примечание: Наследование PrototypeJS

Снова немного кода, для пояснения некоторых вещей.  Есть несколько классов:
var A = Class.create({
    say: function()
    {
      alert('A');
    },
    method: function()
    {
      alert('method A');
    }
    });
    var B = Class.create(A, {
    say: function()
    {
      alert('B');
    }
    });
    var C = Class.create(A, {
    say: function($super)
   {
      $super();
      alert('C');
    },
    abc: function()
    {
      alert('abc');
    }
    });
   
    var a = new A();
    var b = new B();
    var c = new C();
    a.say(); // => A
    b.say(); // => B
    c.say(); // => A, C
    c.method(); // => method A
Здесь видим наследование B и C наследует A. Но в B мы пишем свой метод say(). а в C вызываем сначала родительский $super() -- это пригодится, когда нужно установить некоторые свойста, которые определяются в родительском, например при инициализации. Также видим, что метод method() из A доступен и в C дочернем классе.  Доки






суббота, 11 февраля 2012 г.

BDD в javascript. Jasmine

Сегодня я начну серию постов о тестировании Javascript кода. Многие из разработчиков уделяют этому мало времени, считая, что это не стоит внимания, потому что javascript -- это детский язык, который можно выучить за пару дней. Это конечно же ошибка, чтобы понять суть javascript  у некоторых уходит достатточно много времени,  и они крайне удивляются особенностям javascript, например:
 "1" + 3 == 13 // true
 "10" - 3 == 7 // true
Я напишу несколько постов сначала jasmine, затем фреймворк для Prototypejs & scriptaculous. В серии jasmine будет сначала просто работа с jasmine, потом работа с ajax & DOM, потом интерграция c prototypejs и jquery. Jasmine из Ruby, Потом уже Unit тесты от Томаса Фукса (script.aculo.us)
Но это совсем не значит, что я не буду писать о другие посты.
и так поехали.

Jasmine -- это BDD фреймворк для тестирования javascript кода. Довольно простой, не зависит от других фреймворков, то есть был написан для javascript кода, а не для prototype\jquery\yui.

Синтаксис.


Jasmine унаследовало или скорее поддерживает синтаксис основного bdd фреймворка -- RSpec. Тесты состоят из двух частей describe() -- указывает поведение, какой-то общий контекст для которого выполняются тесты. Вторая часть -- it(), простое утверждение. Describe() и it() принимают два аргумента, первый -- это строка, которая показывае, что вы делаете. Второй -- это функция, непосредственно тест.

Проект

Допустим мы скачали jasmine с сайта. Теперь посмотрим какова структура проекта (директорий).
- jasmine-standalone
   -- lib
      --- jasmine-1.1.0
           ---- некоторые файлы
  -- src
     -- //ваши файлы тестируемые
 -- spec
     -- // ваши тесты
spec_runner.html //запуск тестов, в этот файл скопируйте просто отсюда и пропишите свои пути к файлам тестов и своего кода.

Вот достаточно просто. Для запуска тестов просто открываем spec_runner.html в  любом браузере. Смотрим какие прошли, какие не прошли и исправляем.

Написание тестов

Но прежде, чем тестировать нужно что-то написать для тестов. У меня есть вполне реальный пример кода, который я использую, это определитель високосного года.
function isLeapYear(year) {

       if (year % 4 == 0) {
                if (year % 100 == 0) {
                        if (year % 40 == 0) {
                                return true;
                       }
                     return false;
                }
                return true;
       }
      return false;
}; 

Работа понятна принимает значение года, возратит true или false, в зависимости от принятого года.

Вот теперь можно и писать тесты:
describe("isLeapYear", function() {
    it("2004 should be leap year", function() {
        expect(isLeapYear(2004)).toBeTruthy();
        expect(isLeapYear(2004)).toEqual(true);
    });
   it("2000 should be leap year", function() {
      expect(isLeapYear(2000)).toBeTruthy();
   });
   it("1700 should not be leap year", function() {
      expect(isLeapYear(1700)).toBeFalsy();
   });
   it("2001 should not be leap year", function() {
     expect(isLeapYear(2001)).toBeFalsy();
   });
});
Как видим в describe я указал, что будут тесировать функцию isLeapYear, хотя тут можно было указать что угодно. А потом идут утверждения it(). В блоке которых я проверяю с expect(), за expect() идёт некотрое утверждение (matchers), которых несколько в jasmine. Всё довольно просто. Но наши тесты не могут быть полными, ведь я могу отправить вместо числа строку или массив или ничего, поэтому улучшаем функцию.
function isLeapYear(year) {
   if (typeof(year) != 'number')
           throw 'Year should be Number';
 ...
}
Если тип аргумента не номер, будет брошено исключение.
Дописываем необходимые тесты:
it("Not_Number should throws exception", function() {
    expect(function() { isLeapYear('Not_Number'); }).toThrow('Year should be Number');
    expect(function() { isLeapYear(); }).toThrow('Year should be Number');
});
Всё отлично работает:

beforeEach & afterEach

Сейчас рассморим работу только beforeEach, так как afterEach будет  работать также. Сейчас я напишу некоторую функция-класс, которая будет делать, то же что и isLeapYear:
var Year = function() {
    this.isLeapYear = function(year) {
            return isLeapYear(year);
    };   
};
Видим класс возвращает просто функцию isLeapYear.
Напишем тесты, но уже  с beforeEach:
describe("Object Year.isLeapYear", function() {

   beforeEach(function () {
          Obj = new Year();
     });

     it("2004 should be leap year", function() {
     expect(Obj.isLeapYear(2004)).toBeTruthy();
   });
});
В beforeEach устанавливаем Obj, который будет экземпляром Year. А в блоке it(), проверяем метод этого объекта Obj.isLeapYear(). Я не писал все тесты, потому что они будут подобны прошлым. Вот результат:

Пишем свой mathcer

Написание свойго утверждения в jasmine довольно просто. Может возникнуть зачем новый mather? Всё довольно просто -- это устраняет дублирование кода, а также улучшает кода, ведь не нужно писать большие манипуляции, а достаточно написать один раз новый matcher.
Matcher -- это функция, которая будет принимать сколько угодно аргументов (всё зависит от того, что вы проверяете) и манипулирует переменной this.actual -- это поступает из expect() блока. Matcher добавляется с помощью this.addMatchers({}) в блоке beforeEach(), возвратить ваш matcher должен либо true, либо false, и так когда есть немного теории.
beforeEach(function() {
    this.addMatchers({
         toBeLeapYear: function () {
            var year = this.actual;

                       if (typeof(year) != 'number')
                          throw 'Year should be Number';

                      if (year % 4 == 0) {
                      if (year % 100 == 0) {
                         if (year % 40 == 0) {
                             return true;
                         }
                         return false;
                      }
                      return true;
                   }
                   return false;
                  } 
    });
});
Как видим -- это наша старая функция isLeapYear().
Теперь спеки:
it("2004 should be leap year", function() {
    expect(2004).toBeLeapYear();
    expect(2000).toBeLeapYear();
});
it("2001 fail test", function() {
    expect(2001).toBeLeapYear();
});

Здесь один тест не пройдёт, что и ожидалось.

---------------------
Что не было рассмотрено: шпионы и асинхронные спеки









понедельник, 9 января 2012 г.

scriptaculous. Делаем свой эффект.

Привет!

Сейчас давайте рассмотрим работу, пожалуй одной из самых распространённых библиотек для javascript - scriptaculous.
Scriptaculous надстройка над другим фреймворком - prototypejs.

Она состоит из частей:
  • Effects - включает различные эффекты и также помощники.
  • Behaviours - это drag, drop, sort, и работа с формами.
  • Controls - аякс и автокомлитеры
  • Miscellaneous - это "другое" или остальное, сюда входят тесты (unit testing), звук (sound), создание элементов и узлов из элементов (builder)
Всё это можно найти на сайте и потом я расскажу о каждой из частей.

Сейчас же я покажу как работать с эффектами, вернее не работать, а как создать собственный эффект.
Но сначала пару слов об эффектах, ну то есть немного теории.
Все эффекты наследуются от Effect.Base.
Наследуемый эффект может определить 3 необязательных метода:

  • setup() - этот метод вызывается до начала выполнения эффекта, и делает необходимые настройки, например там можно определить, некоторые нужные переменные, и так далее.
  • update(pos) - вызывается для каждой итерации. Аргумент pos - это текуший кадр. 
  • finish() - вызовется после выполнения эффектов.
Как я говорил, все методы не обязательны, поэтому наиболее часто вызывается update(pos), в котором и происходит вся "магия" эффекта.

Сейчас перейдём от теории к практике. Сделаем свой эффект.
Effect.MyEffect = Class.create(Effect.Base, {
/*   

initialize - метод-конструктор для эффекта, принимает аргументы<

   element - это id элемента.

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

*/ 
    initialize: function(element, setting)
   {
      this.element = $(element);
    //бросаем исключение, если элемент не найден.
      if(!this.element) throw(Effect._elementDoesNotExistError); 
      this.start(setting);
   },
  setup: function()
  {
    Effect.tagifyText(this.element); //разбиваем элемент
    this.letters = this.element.select('span');
  },
  update: function(position)
  {
     var F = position < 0.5 ? 2*position: 2*(1-position);
     var f_pos;
     var s_pos;
     this.letters.collect(function(letter, j){    
        f_pos = Math.sin(position)*15*F;
        s_pos = - f_pos;
        if(j%2==0)
           letter.setStyle({top: Math.round(f_pos) + 'px'});
        else
           letter.setStyle({top: Math.round(s_pos) + 'px'});
     });
 }
});
Вот и всё! Эффект готов. Для примера, этот эффект принимает id элемента, например, заголовка <h1>Lorem ipsum dolore</h1> Но также можно передать и Id параграфа, тогда будет веселее. А сейчас более подробно. В методе-конструкторе мы принимает id, и оборачиваем его $(), если элемент не найден будем бросать исключение. В методе setup, с помощью tagifyText, мы оборачиваем каждую букву элемента в span. Затем мы просто отбираем все элементы. Как и говорилось в update() вся магия, но суть в чём, если чётное в одну сторону, если нет, то  в другую. Формула, которая я использовал, не несёт в себе какой-либо смысловой нагрузки, поэтому понять не пытайтесь. (Хотя на самом деле это простая формула синуса). Затем мы на полученых значениях сдвигаем либо вверх, либо вниз отобранные span'ы. Вот и всё. Просто же.