remoteable 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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:
|