pagelet_rails 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (33) hide show
  1. checksums.yaml +7 -0
  2. data/MIT-LICENSE +20 -0
  3. data/README.md +402 -0
  4. data/Rakefile +37 -0
  5. data/app/assets/config/pagelet_rails_manifest.js +0 -0
  6. data/app/assets/javascripts/pagelet_rails/jquery.ajaxprogress.js +76 -0
  7. data/app/assets/javascripts/pagelet_rails.js +185 -0
  8. data/app/assets/javascripts/views/modal_dialog.jst.ejs +13 -0
  9. data/app/assets/javascripts/views/modal_dialog_iframe.jst.ejs +7 -0
  10. data/app/assets/javascripts/views/pagelet_load_failed.jst.ejs +7 -0
  11. data/app/assets/javascripts/views/pagelet_loading_overlay.jst.ejs +15 -0
  12. data/app/controllers/pagelet_controller.rb +4 -0
  13. data/app/controllers/pagelet_proxy_controller.rb +20 -0
  14. data/app/helpers/pagelets_helper.rb +82 -0
  15. data/app/views/layouts/pagelet_rails/container.html.slim +24 -0
  16. data/app/views/layouts/pagelet_rails/inner.slim +1 -0
  17. data/app/views/layouts/pagelet_rails/loading_placeholder.slim +14 -0
  18. data/config/routes.rb +7 -0
  19. data/lib/action_controller/action_caching.rb +16 -0
  20. data/lib/action_controller/caching/actions.rb +210 -0
  21. data/lib/pagelet_rails/concerns/cache.rb +65 -0
  22. data/lib/pagelet_rails/concerns/controller.rb +73 -0
  23. data/lib/pagelet_rails/concerns/options.rb +76 -0
  24. data/lib/pagelet_rails/concerns/placeholder.rb +35 -0
  25. data/lib/pagelet_rails/concerns/response_wrapper.rb +33 -0
  26. data/lib/pagelet_rails/concerns/routes.rb +83 -0
  27. data/lib/pagelet_rails/encryptor.rb +49 -0
  28. data/lib/pagelet_rails/engine.rb +4 -0
  29. data/lib/pagelet_rails/router.rb +23 -0
  30. data/lib/pagelet_rails/version.rb +3 -0
  31. data/lib/pagelet_rails.rb +28 -0
  32. data/lib/tasks/pagelet_rails_tasks.rake +4 -0
  33. metadata +104 -0
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 7868690d26811fd0be59cc8e007923e113a32a43
4
+ data.tar.gz: 205efc19edefe7b723e6c48cb1818e3cd94dc127
5
+ SHA512:
6
+ metadata.gz: 0ea8943752cd56f0d4b0eea71d1750b7524b1327cd5451b9e40a2140ccea980b7d4dff27532d37a5e123c2461bcc4080f2663759f3d468a3d17af2cb13db16e5
7
+ data.tar.gz: e3a2532c559637364cff3a0bb1fbc6b1696b8fb53590ed9fa27792b38bf2ab4044abdab25e228b6c00746e4ccf5a4e99538692ee75e3109e7183aaf397fd1607
data/MIT-LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright 2016 Anton Katunin
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,402 @@
1
+ # PageletRails
2
+
3
+ [Demo](https://polar-river-18908.herokuapp.com)
4
+
5
+ ## Why?
6
+
7
+ * Do you have pages with a lot of information?
8
+ * The pages where you need to get data from 5 or 10 different sources?
9
+ * What if one of them is slow?
10
+ * Does this mean your users have to wait?
11
+
12
+ Don't make your users wait for page to load.
13
+
14
+ ## Example
15
+
16
+ ![](https://camo.githubusercontent.com/50f4078cc4015e3df89afc753a5ff79828ac0e8e/68747470733a2f2f73332e616d617a6f6e6177732e636f6d2f662e636c2e6c792f6974656d732f303031323133314d324b3147335831483276314f2f313433303033383036373738372e6a7067)
17
+
18
+ For example let's take facebook user home page. It has A LOT of data, but it loads very quickly. How? The answer is [perceived performance](https://en.wikipedia.org/wiki/Perceived_performance). It's not about in how many milliseconds you can serve request, but how fast it **feels** to the user.
19
+
20
+ The page body is served instantly and all the data is loaded after. Even for facebook it takes multiple seconds to fully load the page. But it feels instant, that it's all about.
21
+
22
+ ## Who is doing that?
23
+
24
+ Originally I saw such solution implemented at Facebook and Linkedin. Each page consists of small blocks, where each is responsible for it's own functionality and does not depend on the page where it's included. You can read more on that below.
25
+
26
+ * [BigPipe: Pipelining web pages for high performance](https://www.facebook.com/notes/facebook-engineering/bigpipe-pipelining-web-pages-for-high-performance/389414033919/)
27
+ * [Engineering the New LinkedIn Profile](https://engineering.linkedin.com/profile/engineering-new-linkedin-profile)
28
+ * [Play Framework & SF Scala: Jim Brikman, Composable Applications in Play, 7/23/14](https://www.youtube.com/watch?v=4b1XLka0UIw)
29
+
30
+ ## What is Pagelet?
31
+
32
+ You can break a web page into number of sections, where each one is responsible for its own functionality. Pagelet is the name for each section. It is a part of the page which has it's own route, controller and view.
33
+
34
+ The closest alternative in ruby is [cells gem](https://github.com/apotonick/cells). After using it for long time I've faced many limitations of its approach. Cells has a custom Rails-like syntax but not quite. That is frustrating as you have to learn and remember those differences. The second issue, and the biggest, cells are internal only and not designed to be routable. This stops many great possibilities for improving perceived performance, as request has to wait for all cells to render.
35
+
36
+ Pagelet_rails is built on top of Rails and uses it as much as possible. The main philosophy: **Do not reinvent the wheel, build on shoulders of giants.**
37
+
38
+
39
+ # Usage
40
+
41
+
42
+ ## Installation
43
+ Add this line to your application's Gemfile:
44
+
45
+ ```ruby
46
+ gem 'pagelet_rails'
47
+ ```
48
+
49
+ Or install it yourself as:
50
+ ```bash
51
+ $ gem install pagelet_rails
52
+ ```
53
+
54
+ ## Structure
55
+
56
+ ```
57
+ app
58
+ ├── pagelets
59
+ │ ├── current_time
60
+ │ │ ├── current_time_controller.rb
61
+ │ │ ├── views
62
+ │ │ │ ├── show.erb
63
+ ```
64
+
65
+ ## Example
66
+
67
+ ```ruby
68
+ # app/pagelets/current_time/current_time_controller.rb
69
+ class CurrentTime::CurrentTimeController < ApplicationController
70
+ include PageletRails::Concerns::Controller
71
+
72
+ # add pagelets_current_time_path route
73
+ # which gives "/pagelets/current_time" url route
74
+ pagelet_resource only: [:show]
75
+
76
+ def show
77
+ end
78
+ end
79
+ ```
80
+
81
+
82
+ ```erb
83
+ <!-- Please note view path -->
84
+ <!-- app/pagelets/current_time/views/show.erb -->
85
+ <div class="panel-heading">Current time</div>
86
+
87
+ <div class="panel-body">
88
+ <p><%= Time.now %></p>
89
+ <p>
90
+ <%= link_to 'Refresh', pagelets_current_time_path, remote: true %>
91
+ </p>
92
+ </div>
93
+ ```
94
+
95
+ And now use it anywhere in your view
96
+
97
+ ```erb
98
+ <!-- app/views/dashboard/show.erb -->
99
+ <%= pagelet :pagelets_current_time %>
100
+ ```
101
+
102
+ ## Pagelet view helper
103
+
104
+ `pagelet` helper allows you to render pagelets in views. Name of pagelet is its path.
105
+
106
+ For example pagelet with route `pagelets_current_time_path` will have `pagelets_current_time` name.
107
+
108
+ ### remote
109
+
110
+ Example
111
+ ```erb
112
+ <%= pagelet :pagelets_current_time, remote: true %>
113
+ ```
114
+
115
+ Options for `remote`:
116
+ * `true`, `:ajax` - always render pagelet through ajax
117
+ * `:turbolinks` - same as `:ajax` but inline for turbolinks page visit
118
+ * `false` or missing - render inline
119
+ * `:stream` - (aka BigPipe) render placeholder and render full version at the end of html. See streaming for more info.
120
+
121
+ ### params
122
+
123
+ Example
124
+ ```erb
125
+ <%= pagelet :pagelets_current_time, params: { id: 123 } %>
126
+ ```
127
+
128
+ `params` are the parameters to pass to pagelet path. Same as `pagelets_current_time_path(id: 123)`
129
+
130
+ ### html
131
+
132
+ ```erb
133
+ <%= pagelet :pagelets_current_time, html: { class: 'panel' } %>
134
+ ```
135
+
136
+ You can specify html attributes to pagelet with `html` option
137
+
138
+ ### placeholder
139
+
140
+ ```erb
141
+ <%= pagelet :pagelets_current_time, placeholder: { text: 'Loading...', height: 300 } %>
142
+ ```
143
+
144
+ Configuration for placeholder before pagelet is loaded.
145
+
146
+
147
+ ### other
148
+
149
+ You can pass any other data and it will be available in `pagelet_options`
150
+
151
+ ```erb
152
+ <%= pagelet :pagelets_current_time, title: 'Hello' %>
153
+ ```
154
+
155
+ ```ruby
156
+ # ...
157
+ def show
158
+ @title = pagelet_options.title
159
+ end
160
+ #...
161
+ ```
162
+
163
+
164
+ ## Pagelet options
165
+
166
+ `pagelet_options` is similar to `params` object, but for private data and config. Options can be global for all actions or specific actions only.
167
+
168
+ ```ruby
169
+ class CurrentTime::CurrentTimeController < ::ApplicationController
170
+ include PageletRails::Concerns::Controller
171
+
172
+ # Set default option for all actions
173
+ pagelet_options remote: true
174
+
175
+ # set option for :show and :edit actions only
176
+ pagelet_options :show, :edit, remote: :turbolinks
177
+
178
+ def show
179
+ end
180
+
181
+ def new
182
+ end
183
+
184
+ def edit
185
+ end
186
+
187
+ end
188
+ ```
189
+
190
+ ```erb
191
+ <%= pagelet :new_pagelets_current_time %><!-- defaults to remote: true -->
192
+ <%= pagelet :pagelets_current_time %> <!-- defaults to remote: turbolinks -->
193
+
194
+ <%= pagelet :pagelets_current_time, remote: false %> <!-- force remote: false -->
195
+ ```
196
+
197
+ ## Inline routes
198
+
199
+ Because pagelets are small you will have many of them. In order to keep them under control pagelet_rails provides helpers.
200
+
201
+ You can inline routes inside you controller.
202
+
203
+ ```ruby
204
+ class CurrentTime::CurrentTimeController < ::ApplicationController
205
+ include PageletRails::Concerns::Controller
206
+
207
+ pagelet_resource only: [:show]
208
+ # same as in config/routes.rb:
209
+ #
210
+ # resource :current_time, only: [:show]
211
+ #
212
+
213
+ pagelet_resources
214
+ # same as in config/routes.rb:
215
+ #
216
+ # resources :current_time
217
+ #
218
+
219
+ pagelet_routes do
220
+ # this is the same context as in config/routes.rb:
221
+ get 'show_me_time' => 'current_time/current_time#show'
222
+ end
223
+
224
+ end
225
+ ```
226
+
227
+ ## Pagelet cache
228
+
229
+ Cache of pagelet rails is built on top of [actionpack-action_caching gem](https://github.com/rails/actionpack-action_caching).
230
+
231
+ Simple example
232
+
233
+ ```ruby
234
+ # app/pagelets/current_time/current_time_controller.rb
235
+ class CurrentTime::CurrentTimeController < ::ApplicationController
236
+ include PageletRails::Concerns::Controller
237
+
238
+ pagelet_options expires_in: 10.minutes
239
+
240
+ def show
241
+ end
242
+
243
+ end
244
+ ```
245
+
246
+ ### cache_path
247
+
248
+ Is a hash of additional parameters for cache key.
249
+
250
+ * `Hash` - static hash
251
+ * `Proc` - dynamic params, it must return hash. Eg. `Proc.new { params.permit(:sort_by) }`
252
+ * `Lambda` - same as `Proc` but accepts `controller` as first argument
253
+ * `String` - any custom identifier
254
+
255
+ ### expires_in
256
+
257
+ Set the cache expiry. For example `expires_in: 1.hour`.
258
+
259
+ Warning: if `expires_in` is missing, it will be cached indefinitely.
260
+
261
+ ### cache
262
+
263
+ This is toggle to enable caching without specifying options. `cache_defaults` options will be used (see below).
264
+
265
+ If any of `cache_path`, `expires_in` and `cache` is present then cache will be enabled.
266
+
267
+
268
+ ### cache_defaults
269
+
270
+ You can set default options for caching.
271
+
272
+ ```ruby
273
+ class PageletController < ::ApplicationController
274
+ include PageletRails::Concerns::Controller
275
+
276
+ pagelet_options cache_defaults: {
277
+ expires_in: 5.minutes,
278
+ cache_path: Proc.new {
279
+ { user_id: current_user.id }
280
+ }
281
+ }
282
+ end
283
+ ```
284
+
285
+ In the example above cache will be scoped per `user_id` and for 5 minutes unless it is overwritten in pagelet itself.
286
+
287
+
288
+ ## Advanced functionality
289
+
290
+ ### Partial update
291
+
292
+ ```erb
293
+ <!-- app/pagelets/current_time/views/show.erb -->
294
+ <div class="panel-heading">Current time</div>
295
+
296
+ <div class="panel-body">
297
+ <p><%= Time.now %></p>
298
+ <p>
299
+ <%= link_to 'Refresh', pagelets_current_time_path, remote: true %>
300
+ </p>
301
+ </div>
302
+ ```
303
+ Please note `remote: true` option for `link_to`.
304
+
305
+ This is default Rails functionality with small addition. If that link is inside pagelet, than controller response will be replaced in that pagelet.
306
+
307
+ ```ruby
308
+ # app/pagelets/current_time/current_time_controller.rb
309
+ class CurrentTime::CurrentTimeController < ::ApplicationController
310
+ include PageletRails::Concerns::Controller
311
+
312
+ pagelet_resource only: [:show]
313
+
314
+ def show
315
+ end
316
+
317
+ end
318
+ ```
319
+
320
+ This will partially update the page and replace only that pagelet.
321
+
322
+
323
+ ### Streaming
324
+
325
+ This is the most efficient way to deliver data with minimum delays. The placeholder will be rendered first and the full version will be delivered at the end of page and replaced with Javascript code. Everything is delivered in the same request.
326
+
327
+ This mode requires rendering of templates with [streaming mode](http://api.rubyonrails.org/classes/ActionController/Streaming.html) enabled.
328
+
329
+ ```ruby
330
+ #...
331
+ def show
332
+ render :show, stream: true
333
+ end
334
+ #...
335
+ ```
336
+
337
+ In you layout add `pagelet_stream` right before `</body>` tag.
338
+
339
+ ```erb
340
+ <!-- app/views/layouts/application.erb -->
341
+
342
+ <body>
343
+ <%= yield %>
344
+
345
+ <% pagelet_stream %>
346
+ </body>
347
+ ```
348
+
349
+ Usage:
350
+
351
+ ```erb
352
+ <%= pagelet :pagelets_current_time, remote: :stream %>
353
+ ```
354
+
355
+ **Warning!!!** You also should have webserver compatible for streaming like puma, passenger or unicorn (requires special config).
356
+
357
+ **Warning!!!** you need to have multiple threads/processes configured in the web server. This is required so the page could fetch assets while content is streaming.
358
+
359
+ Finally if everything is done right you should see significant rendering speed improvements especially on old browsers, slow network or with cold cache.
360
+
361
+
362
+ ### Super smart caching
363
+
364
+ Probably one of the coolest functionality of pagelet_rails is "super smart caching". It allows you to render pagelets through ajax and cache them, but if page is reloaded the pagelet is rendered instantly from cache.
365
+
366
+ So on the first page load user sees "Loading..." blocks, but after the content is instant.
367
+
368
+ The best thing, it's enabled by default if pagelet has caching enabled and is rendering through ajax request.
369
+
370
+ ### Ajax Batching
371
+
372
+ Only relevant for `remote: true` and `remote: :turbolinks` when request is loaded through ajax. Loading each pagelet with a separate request is inefficient if you need to do make many requests. That's why you need to group multiple requests into one. By default all ajax requests are grouped into single request. But you can have full control of it. You can specify how to group requests with `ajax_group` option.
373
+
374
+ ```erb
375
+ <%= pagelet :pagelets_current_time, remote: true, ajax_group: 'main' %>
376
+ <%= pagelet :pagelets_current_time, remote: true, ajax_group: 'leftpanel' %>
377
+ ```
378
+
379
+ There will be one request per group. Missing value is considered a separate group as well.
380
+
381
+
382
+ ## Todo
383
+
384
+ * package as gem
385
+ * assets support
386
+ * ~~batch request~~
387
+ * ~~each pagelet makes a separate http call, it's very inefficient for pages with many pagelets. Goal is to group multiple pagelets into single http request.~~
388
+ * ~~streaming of components at the end of body~~
389
+ * ~~goal is to serve the page with placeholders but hold connection and render pagelets in the same request before `</body>` tag~~
390
+ * ~~partial updates~~
391
+ * ~~turbolinks support~~
392
+ * ~~smart caching~~
393
+ * delay load of not visible pagelets (aka. below the fold)
394
+ * do not load pagelets which are not visible to the user until user scrolls down. For example like Youtube comments.
395
+ * fix streaming with nested layouts (rails bug?)
396
+
397
+ ## Contributing
398
+ Contribution directions go here.
399
+
400
+ ## License
401
+ The gem is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT).
402
+
data/Rakefile ADDED
@@ -0,0 +1,37 @@
1
+ begin
2
+ require 'bundler/setup'
3
+ rescue LoadError
4
+ puts 'You must `gem install bundler` and `bundle install` to run rake tasks'
5
+ end
6
+
7
+ require 'rdoc/task'
8
+
9
+ RDoc::Task.new(:rdoc) do |rdoc|
10
+ rdoc.rdoc_dir = 'rdoc'
11
+ rdoc.title = 'PageletRails'
12
+ rdoc.options << '--line-numbers'
13
+ rdoc.rdoc_files.include('README.md')
14
+ rdoc.rdoc_files.include('lib/**/*.rb')
15
+ end
16
+
17
+ APP_RAKEFILE = File.expand_path("../test/dummy/Rakefile", __FILE__)
18
+ load 'rails/tasks/engine.rake'
19
+
20
+
21
+ load 'rails/tasks/statistics.rake'
22
+
23
+
24
+
25
+ require 'bundler/gem_tasks'
26
+
27
+ require 'rake/testtask'
28
+
29
+ Rake::TestTask.new(:test) do |t|
30
+ t.libs << 'lib'
31
+ t.libs << 'test'
32
+ t.pattern = 'test/**/*_test.rb'
33
+ t.verbose = false
34
+ end
35
+
36
+
37
+ task default: :test
File without changes
@@ -0,0 +1,76 @@
1
+ /*!
2
+ * jQuery ajaxProgress Plugin v0.5.0
3
+ * Requires jQuery v1.5.0 or later
4
+ *
5
+ * http://www.kpozin.net/ajaxprogress
6
+ *
7
+ * (c) 2011, Konstantin Pozin
8
+ * Licensed under MIT license.
9
+ */
10
+ (function($) {
11
+
12
+ // Test whether onprogress is supported
13
+ var support = $.support.ajaxProgress = ("onprogress" in $.ajaxSettings.xhr());
14
+
15
+ // If it's not supported, we can't do anything
16
+ if (!support) {
17
+ return;
18
+ }
19
+
20
+ var NAMESPACE = ".ajaxprogress";
21
+
22
+ // Create global "ajaxProgress" event
23
+ $.fn.ajaxProgress = function (f) {
24
+ return this.bind("ajaxProgress", f);
25
+ };
26
+
27
+ // Hold on to a reference to the jqXHR object so that we can pass it to the progress callback.
28
+ // Namespacing the handler with ".ajaxprogress"
29
+ $("html").bind("ajaxSend" + NAMESPACE, function(event, jqXHR, ajaxOptions) {
30
+ ajaxOptions.__jqXHR = jqXHR;
31
+ });
32
+
33
+ /**
34
+ * @param {XMLHttpRequestProgressEvent} evt
35
+ * @param {Object} options jQuery AJAX options
36
+ */
37
+ function handleOnProgress(evt, options) {
38
+
39
+ // Trigger the global event.
40
+ // function handler(jqEvent, progressEvent, jqXHR) {}
41
+ if (options.global) {
42
+ $.event.trigger("ajaxProgress", [evt, options.__jqXHR]);
43
+ }
44
+
45
+ // Trigger the local event.
46
+ // function handler(jqXHR, progressEvent)
47
+ if (typeof options.progress === "function") {
48
+ options.progress(options.__jqXHR, evt);
49
+ }
50
+ }
51
+
52
+
53
+ // We'll work with the original factory method just in case
54
+ var makeOriginalXhr = $.ajaxSettings.xhr.bind($.ajaxSettings);
55
+
56
+ // Options to be passed into $.ajaxSetup;
57
+ var newOptions = {};
58
+
59
+ // Wrap the XMLHttpRequest factory method
60
+ newOptions.xhr = function () {
61
+
62
+ // Reference to the extended options object
63
+ var s = this;
64
+
65
+ var newXhr = makeOriginalXhr();
66
+ if (newXhr) {
67
+ newXhr.addEventListener("progress", function(evt) {
68
+ handleOnProgress(evt, s);
69
+ });
70
+ }
71
+ return newXhr;
72
+ };
73
+
74
+ $.ajaxSetup(newOptions);
75
+
76
+ })(jQuery);