remoteable 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +4 -0
- data/Gemfile +4 -0
- data/README.md +67 -0
- data/Rakefile +1 -0
- data/app/helpers/remoteable_helper.rb +61 -0
- data/lib/assets/javascripts/remoteable.js +185 -0
- data/lib/remoteable.rb +7 -0
- data/lib/remoteable/engine.rb +7 -0
- data/lib/remoteable/version.rb +3 -0
- data/remoteable.gemspec +23 -0
- metadata +76 -0
data/.gitignore
ADDED
data/Gemfile
ADDED
data/README.md
ADDED
@@ -0,0 +1,67 @@
|
|
1
|
+
# Remoteable
|
2
|
+
|
3
|
+
Remoteable is meant to be a simple set of conventions about how data is loaded into
|
4
|
+
a browser. The main idea goes back to the idea of how a browser loads built-in elements that aren't
|
5
|
+
inline text. Specifically, the whole idea of remoteable came from the realization that we've been using the
|
6
|
+
"Asynchronous" from "AJAX" since the first day we wrote an `<img>` tag. For example:
|
7
|
+
|
8
|
+
<img src="http://upload.wikimedia.org/wikipedia/commons/6/63/Wikipedia-logo.png">
|
9
|
+
|
10
|
+
What does the browser do when it sees this in the HTML? It goes off, starts downloading the picture and
|
11
|
+
then renders the picture and inserts it where the `<img>` tag lives in the rendered document. What
|
12
|
+
we've been doing by manually managing AJAX requests that go out, get some content, and inject it into the
|
13
|
+
resulting document is akin to manually downloading the images and injecting the result into the DOM. This
|
14
|
+
took me a bit to really grasp. Here's the core remoteable idea:
|
15
|
+
|
16
|
+
<div id="mydiv" data-src="http://somewhere.com/users/3">
|
17
|
+
|
18
|
+
In this instance treat the rendered user div on the page as a single resource that you go out and download and
|
19
|
+
then inject into the DOM as a replacement for the `<div>` placeholder.
|
20
|
+
|
21
|
+
Triggering it to load itself can be done a number of ways. You can target the div yourself:
|
22
|
+
|
23
|
+
$("#mydiv").trigger("refresh")
|
24
|
+
|
25
|
+
You can do a search for divs that have a common URL
|
26
|
+
|
27
|
+
$("*:remoteable(http://somewhere.com/users/3)").trigger("refresh")
|
28
|
+
|
29
|
+
You can refresh all the remoteables:
|
30
|
+
|
31
|
+
$.remoteables.trigger("refresh")
|
32
|
+
|
33
|
+
And so on.
|
34
|
+
|
35
|
+
## Delegates
|
36
|
+
|
37
|
+
There are many times where one part of the page needs to be updated with some content. Given that a remoteable
|
38
|
+
div has a data-src it's very easy to tell the div to refresh itself, but a common pattern is changing the
|
39
|
+
div's `data-src` attribute and asking it to refresh. Handling this pattern is as simple as:
|
40
|
+
|
41
|
+
<div data-remote-delegate="#some_selector" data-src="http://somewhere.com/users/3">
|
42
|
+
|
43
|
+
When a click event hits this element (either directly or through event bubbling), remoteable will wrap the `data-remote-delegate`
|
44
|
+
value in a jQuery selector and call `remoteable("update")` with its own `data-src` url and asks it to refresh. This is roughly
|
45
|
+
equivalent to:
|
46
|
+
|
47
|
+
$("#some_selector").remoteable("update", {url: "http://somewhere.com/users/3"}).trigger("refresh.remoteable");
|
48
|
+
|
49
|
+
This way, the target element(s) become remoteable (if they weren't already), and the rest of the remoteable flow happens
|
50
|
+
as normal. Note, the original event continues to bubble but preventDefault is called on it.
|
51
|
+
|
52
|
+
If the target element is already loading a request that request will be cancelled first.
|
53
|
+
|
54
|
+
## Content on the way back
|
55
|
+
|
56
|
+
When the remoteable request completes the target element will check the response type. If it's HTML it will replace its content
|
57
|
+
with what was returned by the AJAX request. If you'd rather that the returned content *replace* the remoteable, just add
|
58
|
+
`data-replace-self=true` to the element:
|
59
|
+
|
60
|
+
<div data-src="http://somewhere.com/users/3" data-replace-self="true">
|
61
|
+
|
62
|
+
This is useful for divs that show loading status and should be removed once loading is complete. This way you don't have to worry
|
63
|
+
about cleaning up the loading div as it is handled automatically.
|
64
|
+
|
65
|
+
## Redirects
|
66
|
+
|
67
|
+
TODO: writeup
|
data/Rakefile
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
require "bundler/gem_tasks"
|
@@ -0,0 +1,61 @@
|
|
1
|
+
module RemoteableHelper
|
2
|
+
def remote_content_tag(tag, options = {}, &block)
|
3
|
+
normalize_remoteable_params!(options)
|
4
|
+
|
5
|
+
content_tag(tag, options) do
|
6
|
+
yield if options['data-loaded']
|
7
|
+
end
|
8
|
+
end
|
9
|
+
|
10
|
+
def link_to_refresh(link, target, options = {})
|
11
|
+
normalize_remoteable_params!(options)
|
12
|
+
|
13
|
+
link_to link, target, options
|
14
|
+
end
|
15
|
+
|
16
|
+
def link_to_remoteable(link, options = {})
|
17
|
+
normalize_remoteable_params!(options)
|
18
|
+
|
19
|
+
link_to_function link, "", options
|
20
|
+
end
|
21
|
+
|
22
|
+
def refresh_remoteable(options)
|
23
|
+
normalize_remoteable_params!(options)
|
24
|
+
|
25
|
+
%Q{$.remoteables("#{j options["data-src"]}").remoteable("refresh")}
|
26
|
+
end
|
27
|
+
|
28
|
+
private
|
29
|
+
|
30
|
+
def polymorphic_link_from_options(url, options = {})
|
31
|
+
return url if url.is_a?(String)
|
32
|
+
args = [url]
|
33
|
+
args << {:format => options[:format]} if options[:format]
|
34
|
+
polymorphic_url(*args)
|
35
|
+
end
|
36
|
+
|
37
|
+
def normalize_remoteable_params!(options)
|
38
|
+
data_method = options[:method] || "get"
|
39
|
+
|
40
|
+
if(options[:url])
|
41
|
+
url = polymorphic_link_from_options(options[:url], options)
|
42
|
+
options['data-src'] = url
|
43
|
+
options['data-method'] = data_method
|
44
|
+
end
|
45
|
+
|
46
|
+
options['data-loaded'] = !!options[:loaded]
|
47
|
+
Rails.logger.info("Data loaded: #{options['data-loaded']}")
|
48
|
+
options[:class] = options[:loaded_class] if options[:loaded] && options[:loaded_class]
|
49
|
+
|
50
|
+
options[:style] = "display: none;" if options[:hide_until_loaded] && !options['data-loaded']
|
51
|
+
|
52
|
+
options.delete(:method)
|
53
|
+
options.delete(:url)
|
54
|
+
options.delete(:format)
|
55
|
+
options.delete(:loaded)
|
56
|
+
options.delete(:hide_until_loaded)
|
57
|
+
options.delete(:loaded_class)
|
58
|
+
end
|
59
|
+
|
60
|
+
|
61
|
+
end
|
@@ -0,0 +1,185 @@
|
|
1
|
+
(function($) {
|
2
|
+
|
3
|
+
$.extend(
|
4
|
+
$.expr[":"], {
|
5
|
+
remoteable: function(elem, i, match, array) {
|
6
|
+
var url = match[3].split(",")[0];
|
7
|
+
var method = match[3].split(",")[1] || "get";
|
8
|
+
return $(elem).data("src") == url && $(elem).data("method") == method;
|
9
|
+
}
|
10
|
+
}
|
11
|
+
);
|
12
|
+
|
13
|
+
$.remoteables = function(url, method) {
|
14
|
+
var remoteables = "*[data-src]";
|
15
|
+
if(url) {
|
16
|
+
remoteables += "[data-src^='" + url + "']";
|
17
|
+
}
|
18
|
+
if(method) {
|
19
|
+
remoteables += "[data-method='" + method + "']";
|
20
|
+
}
|
21
|
+
|
22
|
+
return $(remoteables);
|
23
|
+
}
|
24
|
+
|
25
|
+
var httpMethods = {
|
26
|
+
put: function(options) {
|
27
|
+
if(options.data == undefined) {
|
28
|
+
options.data = {};
|
29
|
+
}
|
30
|
+
options.data._method = 'PUT';
|
31
|
+
options.type = "POST";
|
32
|
+
return $.ajax(options);
|
33
|
+
},
|
34
|
+
delete: function(options) {
|
35
|
+
if(options.data == undefined) {
|
36
|
+
options.data = {};
|
37
|
+
}
|
38
|
+
options.data._method = 'DELETE';
|
39
|
+
options.type = "POST";
|
40
|
+
return $.ajax(options);
|
41
|
+
},
|
42
|
+
post: function(options) {
|
43
|
+
options.type = "POST";
|
44
|
+
return $.ajax(options);
|
45
|
+
},
|
46
|
+
get: function(options) {
|
47
|
+
options.type = "GET";
|
48
|
+
return $.ajax(options);
|
49
|
+
}
|
50
|
+
};
|
51
|
+
|
52
|
+
var methods = {
|
53
|
+
init: function(options) {
|
54
|
+
options = options || {};
|
55
|
+
return this.each(function() {
|
56
|
+
var $this = $(this),
|
57
|
+
data = $this.data('remoteable');
|
58
|
+
// If the plugin hasn't been initialized yet
|
59
|
+
if(!data) {
|
60
|
+
/*
|
61
|
+
Do more setup stuff here
|
62
|
+
*/
|
63
|
+
$(this).data('remoteable', {
|
64
|
+
target: $this
|
65
|
+
});
|
66
|
+
|
67
|
+
var self = $(this);
|
68
|
+
var method = options["method"] || self.data("method") || "get";
|
69
|
+
self.attr("data-method", method);
|
70
|
+
var url = options["url"] || self.data("src");
|
71
|
+
self.attr("data-src", url);
|
72
|
+
|
73
|
+
self.on("click.remoteable", function(event) {
|
74
|
+
if(self.data("remote-delegate")) {
|
75
|
+
$(self.data('remote-delegate')).remoteable("update", {url: url});
|
76
|
+
} else {
|
77
|
+
if(self.is("a")) {
|
78
|
+
event.preventDefault();
|
79
|
+
if(self.data("remoteable").currentRequest) {
|
80
|
+
self.data("remoteable").currentRequest.abort();
|
81
|
+
}
|
82
|
+
self.data("remoteable").currentRequest = httpMethods[method].call(self, {
|
83
|
+
url: url,
|
84
|
+
complete: function(jqXHR, textStatus) {
|
85
|
+
self.data("remoteable").currentRequest = null;
|
86
|
+
}
|
87
|
+
});
|
88
|
+
event.stopPropagation();
|
89
|
+
}
|
90
|
+
}
|
91
|
+
});
|
92
|
+
|
93
|
+
self.on("refresh.remoteable", function(event) {
|
94
|
+
var url = self.data("src");
|
95
|
+
event.preventDefault();
|
96
|
+
event.stopPropagation();
|
97
|
+
|
98
|
+
if(self.is(":not(a)")) {
|
99
|
+
self.addClass("remoteable-processing");
|
100
|
+
if(self.data("remoteable").currentRequest) {
|
101
|
+
self.data("remoteable").currentRequest.abort();
|
102
|
+
}
|
103
|
+
self.data("remoteable").currentRequest = httpMethods[method].call(self, {
|
104
|
+
url: url,
|
105
|
+
complete: function(jqXHR, textStatus) {
|
106
|
+
self.removeClass("remoteable-processing");
|
107
|
+
self.trigger("ajax:complete.remoteable", [jqXHR, textStatus]);
|
108
|
+
}
|
109
|
+
});
|
110
|
+
}
|
111
|
+
});
|
112
|
+
|
113
|
+
self.on("ajax:complete.remoteable", function(event, jqXHR, textStatus) {
|
114
|
+
event.stopPropagation();
|
115
|
+
event.preventDefault();
|
116
|
+
self.data("remoteable").currentRequest = null;
|
117
|
+
var ct = jqXHR.getResponseHeader("content-type") || "";
|
118
|
+
if(ct.indexOf('html') > -1) {
|
119
|
+
var returned = $(jqXHR.responseText);
|
120
|
+
if(self.data("replace-self")) {
|
121
|
+
self.replaceWith(returned);
|
122
|
+
} else {
|
123
|
+
self.html(returned);
|
124
|
+
self.trigger("refreshed.remoteable", [self.data("src")]);
|
125
|
+
}
|
126
|
+
}
|
127
|
+
if(ct.indexOf('json') > -1) {
|
128
|
+
var url = $.parseJSON(jqXHR.responseText).redirect_to;
|
129
|
+
self.remoteable("update", {url: url});
|
130
|
+
}
|
131
|
+
});
|
132
|
+
|
133
|
+
}
|
134
|
+
});
|
135
|
+
},
|
136
|
+
|
137
|
+
destroy: function() {
|
138
|
+
return this.each(function() {
|
139
|
+
var $this = $(this),
|
140
|
+
data = $this.data('remoteable');
|
141
|
+
|
142
|
+
$this.removeData('remoteable');
|
143
|
+
});
|
144
|
+
},
|
145
|
+
|
146
|
+
refreshAll: function(options) {
|
147
|
+
$.remoteables().remoteable("refresh");
|
148
|
+
},
|
149
|
+
|
150
|
+
initAll: function(options) {
|
151
|
+
$.remoteables().remoteable();
|
152
|
+
},
|
153
|
+
|
154
|
+
update: function(options) {
|
155
|
+
return this.each(function() {
|
156
|
+
var self = $(this);
|
157
|
+
self.remoteable();
|
158
|
+
if(options.url) {
|
159
|
+
self.data("src", options.url);
|
160
|
+
}
|
161
|
+
self.trigger("refresh.remoteable");
|
162
|
+
});
|
163
|
+
},
|
164
|
+
|
165
|
+
refresh: function(options) {
|
166
|
+
return this.each(function() {
|
167
|
+
var self = $(this);
|
168
|
+
self.trigger("refresh.remoteable");
|
169
|
+
});
|
170
|
+
}
|
171
|
+
};
|
172
|
+
|
173
|
+
$.fn.remoteable = function(method) {
|
174
|
+
|
175
|
+
if(methods[method]) {
|
176
|
+
return methods[method].apply(this, Array.prototype.slice.call(arguments, 1));
|
177
|
+
} else if(typeof method === 'object' || !method) {
|
178
|
+
return methods.init.apply(this, arguments);
|
179
|
+
} else {
|
180
|
+
$.error('Method ' + method + ' does not exist on jQuery.remoteable');
|
181
|
+
}
|
182
|
+
|
183
|
+
};
|
184
|
+
|
185
|
+
})(jQuery);
|
data/lib/remoteable.rb
ADDED
data/remoteable.gemspec
ADDED
@@ -0,0 +1,23 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
$:.push File.expand_path("../lib", __FILE__)
|
3
|
+
require "remoteable/version"
|
4
|
+
|
5
|
+
Gem::Specification.new do |s|
|
6
|
+
s.name = "remoteable"
|
7
|
+
s.version = Remoteable::VERSION
|
8
|
+
s.authors = ["Andrew Eberbach"]
|
9
|
+
s.email = ["andrew@ebertech.ca"]
|
10
|
+
s.homepage = ""
|
11
|
+
s.summary = %q{Remoteable AJAX content helper}
|
12
|
+
|
13
|
+
s.rubyforge_project = "remoteable"
|
14
|
+
|
15
|
+
s.files = `git ls-files`.split("\n")
|
16
|
+
s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
|
17
|
+
s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
|
18
|
+
s.require_paths = ["lib"]
|
19
|
+
|
20
|
+
# specify any dependencies here; for example:
|
21
|
+
# s.add_development_dependency "rspec"
|
22
|
+
# s.add_runtime_dependency "rest-client"
|
23
|
+
end
|
metadata
ADDED
@@ -0,0 +1,76 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: remoteable
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
hash: 23
|
5
|
+
prerelease:
|
6
|
+
segments:
|
7
|
+
- 1
|
8
|
+
- 0
|
9
|
+
- 0
|
10
|
+
version: 1.0.0
|
11
|
+
platform: ruby
|
12
|
+
authors:
|
13
|
+
- Andrew Eberbach
|
14
|
+
autorequire:
|
15
|
+
bindir: bin
|
16
|
+
cert_chain: []
|
17
|
+
|
18
|
+
date: 2012-03-31 00:00:00 Z
|
19
|
+
dependencies: []
|
20
|
+
|
21
|
+
description:
|
22
|
+
email:
|
23
|
+
- andrew@ebertech.ca
|
24
|
+
executables: []
|
25
|
+
|
26
|
+
extensions: []
|
27
|
+
|
28
|
+
extra_rdoc_files: []
|
29
|
+
|
30
|
+
files:
|
31
|
+
- .gitignore
|
32
|
+
- Gemfile
|
33
|
+
- README.md
|
34
|
+
- Rakefile
|
35
|
+
- app/helpers/remoteable_helper.rb
|
36
|
+
- lib/assets/javascripts/remoteable.js
|
37
|
+
- lib/remoteable.rb
|
38
|
+
- lib/remoteable/engine.rb
|
39
|
+
- lib/remoteable/version.rb
|
40
|
+
- remoteable.gemspec
|
41
|
+
homepage: ""
|
42
|
+
licenses: []
|
43
|
+
|
44
|
+
post_install_message:
|
45
|
+
rdoc_options: []
|
46
|
+
|
47
|
+
require_paths:
|
48
|
+
- lib
|
49
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
50
|
+
none: false
|
51
|
+
requirements:
|
52
|
+
- - ">="
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
hash: 3
|
55
|
+
segments:
|
56
|
+
- 0
|
57
|
+
version: "0"
|
58
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
59
|
+
none: false
|
60
|
+
requirements:
|
61
|
+
- - ">="
|
62
|
+
- !ruby/object:Gem::Version
|
63
|
+
hash: 3
|
64
|
+
segments:
|
65
|
+
- 0
|
66
|
+
version: "0"
|
67
|
+
requirements: []
|
68
|
+
|
69
|
+
rubyforge_project: remoteable
|
70
|
+
rubygems_version: 1.8.10
|
71
|
+
signing_key:
|
72
|
+
specification_version: 3
|
73
|
+
summary: Remoteable AJAX content helper
|
74
|
+
test_files: []
|
75
|
+
|
76
|
+
has_rdoc:
|