-
Notifications
You must be signed in to change notification settings - Fork 3
Ajax on rails.textile
이 안내서는 레일스에 내장되어 있는 Ajax/JavsScript 기능성을 다루고 있어서 다양하고 동적인 AJAX 어플리케이션을 쉽게 만들수 있게 해 줄 것입니다. 여기서 다룰 주제는 다음과 같습니다.
- AJAX와 관련된 기술들에 대한 간략한 소개
- 자바스크립트를 레일스방식으로 다루기: Rails helpers, RJS, Prototype and script.aculo.us
- 자바스크립트 기능 테스트하기
‘show me the code’형의 사람이라면 이 부분을 건너띠고 RJS 부분으로 이동하고 싶어 할 수 있습니다. 그러나, 이 부분을 읽어 볼 것을 정말로 권하는데, 레일스에서 Ajax를 잘 이해하려면 여기서 언급되는 DOM의 기본개념, http 요청하는 방법, 그리고 기타 다른 토픽들이 필요할 것이기 때문입니다.
(= A synchronous Ja vaScript + X ML)
Ajax의 기본 용어설명, 웹어플리케이션을 만드는 새로운 방식
DOM 의 기본개념, DOM 이 만들어진 방법, DOM 의 속성, 특징, DOM 이 AJAX 의 핵심인 이유
통상적인 요청과 AJAX요청의 차이점, 레일스에서 AJAX 를 이해하는데 있어서의 이러한 차이점의 중요성(다음 섹션에 언급되는 *_remote 헬퍼메소드와 연관됨)
레일스의 기본 자바스크립트 프레임워크는 Prototype 입니다. Prototype 은 일반적으로 사용할 수 있는 자바스크립트 프레임워크로서 DOM 객체 조작과 AJAX 를 제공하며, 그외에도 유틸리티 함수로부터 객체지향적 구조에 이르는 자바스크립트 기능을 제공해 줌으로써 동적인 웹어프리케이션을 쉽게 개발할 수 있도록 하기 위해 만들어 졌습니다. Prototype 은 특정언어를 위해서 만들어진 것이 아니기 때문에, 이와 관련된 다수의 헬퍼메소드를 레일스가 제공해서 Prototype이 레일스 뷰 파일들과 매끄럽게 통합될 수 있도록 해줍니다.
이러한 헬퍼메소드에 접근하기 위해서 해야 할 것은 대개는 application.html.erb와 같은 웹어플리케이션의 최상위 레이아웃 파일에 Prototype 프레임워크를 아래와 같이 포함시키는 것입니다.
javascript_include_tag ‘prototype’
이제 레일스 어플리케이션에 AJAX에 대한 사랑을 더해 줄 준비가 된 것입니다.
아마도 가장 많이 사용하는 헬퍼메소드인 link_to_remote 로부터 시작해 봅시다. 관련 도큐먼트에 기록된 것을 볼 대 이 헬퍼메소드는 재미있는 특징을 가지고 있는데, 이 헬퍼메소드로 넘겨지는 옵션들은 모든 AJAX 헬퍼메소드들에서도 공유하게 된다는 것입니다. 따라서 이 헬퍼메소드의 옵션과 작동 기전을 배우게 되면 다른 헬퍼메소드를 사용하는데 도움이 될 것입니다.
link_to_remote 함수는 표준 link_to 헬퍼메소드와 그 특징이 같습니다. :
def link_to_remote(name, options = {}, html_options = nil)
그리고 아래에 link_to_remote 헬퍼메소드에 대한 간단한 예제가 있습니다.
link_to_remote “Add to cart”,
:url => add_to_cart_url(product.id),
:update => “cart”
- 제일 처음에 있는 매개변수는 문자열인데, 웹페이지에 나타나는 링크의 텍스트입니다.
- 두번째 매개변수는 options 해쉬이며, AJAX에 관련된 기능을 가질 때 가장 재미있는 부분입니다.
- :url This is the only parameter that is always required to generate the simplest remote link (technically speaking, it is not required, you can pass an empty options hash to link_to_remote – but in this case the URL used for the POST request will be equal to your current URL which is probably not your intention). This URL points to your AJAX action handler. The URL is typically specified by Rails REST view helpers, but you can use the url_for format too.
- :update There are basically two ways of injecting the server response into the page: One is involving RJS and we will discuss it in the next chapter, and the other is specifying a DOM id of the element we would like to update. The above example demonstrates the simplest way of accomplishing this – however, we are in trouble if the server responds with an error message because that will be injected into the page too! However, Rails has a solution for this situation:
link_to_remote “Add to cart”,
:url => add_to_cart_url(product),
:update => { :success => “cart”, :failure => “error” }
If the server returns 200, the output of the above example is equivalent to our first, simple one. However, in case of error, the element with the DOM id error is updated rather than the cart element.
-
position By default (i.e. when not specifying this option, like in the examples before) the repsonse is injected into the element with the specified DOM id, replacing the original content of the element (if there was any). You might want to alter this behavior by keeping the original content – the only question is where to place the new content? This can specified by the position parameter, with four possibilities:
- :before Inserts the response text just before the target element. More precisely, it creates a text node from the response and inserts it as the left sibling of the target element.
- :after Similar behavior to :before, but in this case the response is inserted after the target element.
- :top Inserts the text into the target element, before it’s original content. If the target element was empty, this is equivalent with not specifying :position at all.
- :bottom The counterpart of :top: the response is inserted after the target element’s original content.
A typical example of using :bottom is inserting a new <li> element into an existing list:
link_to_remote “Add new item”,
:url => items_url,
:update => ‘item_list’,
:position => :bottom
- :method Most typically you want to use a POST request when adding a remote link to your view so this is the default behavior. However, sometimes you’ll want to update (PUT) or delete/destroy (DELETE) something and you can specify this with the :method option. Let’s see an example for a typical AJAX link for deleting an item from a list:
link_to_remote “Delete the item”,
:url => item_url(item),
:method => :delete
Note that if we wouldn’t override the default behavior (POST), the above snippet would route to the create action rather than destroy.
-
JavaScript filters You can customize the remote call further by wrapping it with some JavaScript code. Let’s say in the previous example, when deleting a link, you’d like to ask for a confirmation by showing a simple modal text box to the user. This is a typical example what you can accomplish with these options – let’s see them one by one:
- :confirm => msg Pops up a JavaScript confirmation dialog, displaying msg. If the user chooses ‘OK’, the request is launched, otherwise canceled.
- :condition => code Evaluates code (which should evaluate to a boolean) and proceeds if it’s true, cancels the request otherwise.
- :before => code Evaluates the code just before launching the request. The output of the code has no influence on the execution. Typically used show a progress indicator (see this in action in the next example).
- :after => code Evaluates the code after launching the request. Note that this is different from the :success or :complete callback (covered in the next section) since those are triggered after the request is completed, while the code snippet passed to :after is evaluated after the remote call is made. A common example is to disable elements on the page or otherwise prevent further action while the request is completed.
- :submit => dom_id This option does not make sense for link_to_remote, but we’ll cover it for the sake of completeness. By default, the parent element of the form elements the user is going to submit is the current form – use this option if you want to change the default behavior. By specifying this option you can change the parent element to the element specified by the DOM id dom_id.
- :with > code The JavaScript code snippet in code is evaluated and added to the request URL as a parameter (or set of parameters). Therefore, code should return a valid URL query string (like “item_type=8” or “item_type=8&sort=true”). Usually you want to obtain some value(s) from the page – let’s see an example:
link_to_remote “Update record”,
:url => record_url(record),
:method => :put,
:with => “‘status=’ + ‘encodeURIComponent(
This generates a remote link which adds 2 parameters to the standard URL generated by Rails, taken from the page (contained in the elements matched by the ‘status’ and ‘completed’ DOM id).
-
Callbacks Since an AJAX call is typically asynchronous, as it’s name suggests (this is not a rule, and you can fire a synchronous request – see the last option, :type) your only way of communicating with a request once it is fired is via specifying callbacks. There are six options at your disposal (in fact 508, counting all possible response types, but these six are the most frequent and therefore specified by a constant):
- :loading: => code The request is in the process of receiving the data, but the transfer is not completed yet.
- :loaded: => code The transfer is completed, but the data is not processed and returned yet
- :interactive: => code One step after :loaded: The data is fully received and being processed
- :success: => code The data is fully received, parsed and the server responded with “200 OK”
- :failure: => code The data is fully received, parsed and the server responded with anything but “200 OK” (typically 404 or 500, but in general with any status code ranging from 100 to 509)
- :complete: => code The combination of the previous two: The request has finished receiving and parsing the data, and returned a status code (which can be anything).
- Any other status code ranging from 100 to 509: Additionally you might want to check for other HTTP status codes, such as 404. In this case simply use the status code as a number:
link_to_remote “Add new item”,
:url => items_url,
:update => “item_list”,
404 => “alert(‘Item not found!’)”
Let’s see a typical example for the most frequent callbacks, :success, :failure and :complete in action:
link_to_remote “Add new item”,
:url => items_url,
:update => “item_list”,
:before => “$(‘progress’).show()”,
:complete => “$(‘progress’).hide()”,
:success => “display_item_added(request)”,
:failure => “display_error(request)”
- :type If you want to fire a synchronous request for some obscure reason (blocking the browser while the request is processed and doesn’t return a status code), you can use the :type option with the value of :synchronous.
- If you specify the href parameter, the AJAX link will degrade gracefully, i.e. the link will point to the URL even if JavaScript is disabled in the client browser
- link_to_remote gains it’s AJAX behavior by specifying the remote call in the onclick handler of the link. If you supply html_options[:onclick] you override the default behavior, so use this with care!
We are finished with link_to_remote. I know this is quite a lot to digest for one helper function, but remember, these options are common for all the rest of the Rails view helpers, so we will take a look at the differences / additional parameters in the next sections.
There are three different ways of adding AJAX forms to your view using Rails Prototype helpers. They are slightly different, but striving for the same goal: instead of submitting the form using the standard HTTP request/response cycle, it is submitted asynchronously, thus not reloading the page. These methods are the following:
- remote_form_for (and it’s alias form_remote_for) is tied to Rails most tightly of the three since it takes a resource, model or array of resources (in case of a nested resource) as a parameter.
- form_remote_tag AJAXifies the form by serializing and sending it’s data in the background
- submit_to_remote and button_to_remote is more rarely used than the previous two. Rather than creating an AJAX form, you add a button/input
Let’s see them in action one by one!
In the last section we sent some AJAX requests to the server, and inserted the HTML response into the page (with the :update option). However, sometimes a more complicated interaction with the page is needed, which you can either achieve with JavaScript… or with RJS! You are sending JavaScript instructions to the server in both cases, but while in the former case you have to write vanilla JavaScript, in the second you can code Rails, and sit back while Rails generates the JavaScript for you – so basically RJS is a Ruby DSL to write JavaScript in your Rails code.
First we’ll check out how to send JavaScript to the server manually. You are practically never going to need this, but it’s interesting to understand what’s going on under the hood.
def javascript_test
render :text => “alert(‘Hello, world!’)”,
:content_type => “text/javascript”
end
(Note: if you want to test the above method, create a link_to_remote with a single parameter – :url, pointing to the javascript_test action)
What happens here is that by specifying the Content-Type header variable, we instruct the browser to evaluate the text we are sending over (rather than displaying it as plain text, which is the default behavior).
As we said, the purpose of RJS is to write Ruby which is then auto-magically turned into JavaScript by Rails. The above example didn’t look too Ruby-esque so let’s see how to do it the Rails way:
def javascript_test
render :update do |page|
page.alert “Hello from inline RJS”
end
end
The above code snippet does exactly the same as the one in the previous section – going about it much more elegantly though. You don’t need to worry about headers,write ugly JavaScript code into a string etc. When the first parameter to render is :update, Rails expects a block with a single parameter (page in our case, which is the traditional naming convention) which is an instance of the JavaScriptGenerator:“http://api.rubyonrails.org/classes/ActionView/Helpers/PrototypeHelper/JavaScriptGenerator/GeneratorMethods.html” object. As it’s name suggests, JavaScriptGenerator is responsible for generating JavaScript from your Ruby code. You can execute multiple method calls on the page instance – it’s all turned into JavaScript code and sent to the server with the appropriate Content Type, “text/javascript”.
If you don’t want to clutter your controllers with view code (especially when your inline RJS is more than a few lines), you can move your RJS code to a template file. RJS templates should go to the /app/views/ directory, just as .html.erb or any other view files of the appropriate controller, conventionally named js.rjs.
To rewrite the above example, you can leave the body of the action empty, and create a RJS template named javascript_test.js.rjs, containing the following line:
page.alert “Hello from inline RJS”
In this section we’ll go through the methods RJS offers.
It is possible to manipulate multiple elements at once through the page JavaScriptGenerator instance. Let’s see this in action:
page.show :div_one, :div_two
page.hide :div_one
page.remove :div_one, :div_two, :div_three
page.toggle :other_div
The above methods (show, hide, remove, toggle) have the same semantics as the Prototype methods of the same name. You can pass an arbitrary number (but at least one) of DOM ids to these calls.
You can insert content into an element on the page with the insert_html method:
page.insert_html :top, :result, ‘42’
The first parameter is the position of the new content relative to the element specified by the second parameter, a DOM id.
Position can be one of these four values:
- :before Inserts the response text just before the target element.
- :after The response is inserted after the target element.
- :top Inserts the text into the target element, before it’s original content.
- :bottom The counterpart of :top: the response is inserted after the target element’s original content.
The third parameter can either be a string, or a hash of options to be passed to ActionView::Base#render – for example:
page.insert_html :top, :result, :partial => “the_answer”
You can replace the contents (innerHTML) of an element with the replace_html method. The only difference is that since it’s clear where should the new content go, there is no need for a position parameter – so replace_html takes only two arguments,
the DOM id of the element you wish to modify and a string or a hash of options to be passed to ActionView::Base#render.
You can delay the execution of a block of code with delay:
page.delay(10) { page.alert(‘Hey! Just waited 10 seconds’) }
delay takes one parameter (time to wait in seconds) and a block which will be executed after the specified time has passed – whatever else follows a page.delay line is executed immediately, the delay affects only the code in the block.
You can reload the page with the reload method:
page.reload
When using AJAX, you can’t rely on the standard redirect_to controller method – you have to use the +page+’s instance method, also called redirect_to:
page.redirect_to some_url
Sometimes even the full power of RJS is not enough to accomplish everything, but you still don’t want to drop to pure JavaScript. A nice golden mean is offered by the combination of <<, assign and call methods:
page << “alert(‘1+1 equals 3’)”So << is used to execute an arbitrary JavaScript statement, passed as string to the method. The above code is equivalent to:
page.assign :result, 3 page.call :alert, ’1+1 equals ’ + resultassign simply assigns a value to a variable. call is similar to << with a slightly different syntax: the first parameter is the name of the function to call, followed by the list of parameters passed to the function.
JavaScript testing reminds me the definition of the world ‘classic’ by Mark Twain: “A classic is something that everybody wants to have read and nobody wants to read.” It’s similar with JavaScript testing: everyone would like to have it, yet it’s not done by too much developers as it is tedious, complicated, there is a proliferation of tools and no consensus/accepted best practices, but we will nevertheless take a stab at it:
- (Fire)Watir
- Selenium
- Celerity/Culerity
- Cucumber+Webrat
- Mention stuff like screw.unit/jsSpec
Note to self: check out the RailsConf JS testing video