breezy 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/README.md +326 -0
- data/lib/assets/javascripts/breezy.coffee +4 -0
- data/lib/breezy.rb +71 -0
- data/lib/breezy/active_support.rb +21 -0
- data/lib/breezy/configuration.rb +21 -0
- data/lib/breezy/cookies.rb +15 -0
- data/lib/breezy/helpers.rb +28 -0
- data/lib/breezy/render.rb +36 -0
- data/lib/breezy/version.rb +3 -0
- data/lib/breezy/x_domain_blocker.rb +22 -0
- data/lib/breezy/xhr_headers.rb +56 -0
- data/lib/breezy/xhr_redirect.rb +13 -0
- data/lib/breezy/xhr_url_for.rb +11 -0
- data/test/blade_helper.rb +22 -0
- data/test/breezy_template_test.rb +1025 -0
- data/test/breezy_test.rb +171 -0
- data/test/configuration_test.rb +29 -0
- data/test/dependency_tracker_test.rb +66 -0
- data/test/engine_test.rb +7 -0
- data/test/render_test.rb +124 -0
- data/test/test_helper.rb +18 -0
- metadata +168 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 28b02ed9694b5b85d6646507c147c938500ecd7c
|
4
|
+
data.tar.gz: 3d0e115680c7b088dd4ed03376d18bbdfdca392f
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 252ac9b68b92cdfa5ee57e7a49521f6338f54042e3d17d8a34a73de377d4b69f8f680897209e619b6b65a8e6a4b2a9ce715bd67190d12445d4f33bb3accf867c
|
7
|
+
data.tar.gz: 93e38dea643db06c33a8542d116a4baa861c0b2a85f54e12130b6945346f51bc75c76cff45d2bad2588aa813aaef81bd09e4463bc4e45b0b3850ae3dbf8a3ed3
|
data/README.md
ADDED
@@ -0,0 +1,326 @@
|
|
1
|
+
# Breezy
|
2
|
+
Breezy makes it easy (even boring) to create single-page, multi-page, and sometimes-single-page applications with ReactJS and classic Rails.
|
3
|
+
|
4
|
+
[![Sauce Test Status](https://saucelabs.com/browser-matrix/jho406-bensonhurst.svg)](https://saucelabs.com/u/jho406-bensonhurst)
|
5
|
+
|
6
|
+
## What you can do:
|
7
|
+
|
8
|
+
1. Page to page transitions without reloading
|
9
|
+
2. Russian Doll cache aware JS content with structural sharing that makes implementing `shouldComponentUpdate` as easy as extending ReactJS's `PureComponent`
|
10
|
+
3. Defered rendering of slow loading parts of your page without API endpoints (e.g. dashboards that load later on direct visits)
|
11
|
+
4. Async actions that load different parts of your page without API endpoints (e.g. pagination, infinite scroll)
|
12
|
+
|
13
|
+
|
14
|
+
Breezy is the lovechild of Turbolinks (also a hard fork), Server Generated Javascript Responses (created with BreezyTemplates), and ReactJS that bring you all of the above features while keeping complexity low.
|
15
|
+
|
16
|
+
Unlike Turbolink's view-over-the-wire approach, a Breezy app is content-over-the-wire to your ReactJS frontend. Each controller gets two views, one for your content that you write using the included BreezyTemplates, and the other for your markup which you write in ReactJS. Breezy features are achieved with `XMLHTTPRequest`s for the next page's content (or a branch of it via key paths) before firing a `breezy:load` event that you can use with `ReactDOM.render`.
|
17
|
+
|
18
|
+
## Quick Peek
|
19
|
+
Starting with a Rails project with Breezy [installed](#installation), ReactJS in your asset pipeline, and [something](https://github.com/reactjs/react-rails) [to](https://github.com/Shopify/sprockets-commoner) transform JSX to JS.
|
20
|
+
|
21
|
+
Add a route and controller as you normally would.
|
22
|
+
```ruby
|
23
|
+
# config/routes.rb
|
24
|
+
resources :posts
|
25
|
+
|
26
|
+
# app/controllers/posts_controller.rb
|
27
|
+
class PostsController < ApplicationController
|
28
|
+
# Allow breezy to take over HTML requests
|
29
|
+
before_action :use_breezy_html
|
30
|
+
|
31
|
+
def index
|
32
|
+
@greeting = 'hello'
|
33
|
+
end
|
34
|
+
|
35
|
+
def new
|
36
|
+
....some stuff here...
|
37
|
+
end
|
38
|
+
end
|
39
|
+
```
|
40
|
+
|
41
|
+
Use the included BreezyTemplates to create your content.
|
42
|
+
|
43
|
+
```ruby
|
44
|
+
#app/views/posts/index.js.breezy
|
45
|
+
|
46
|
+
json.heading @greeting
|
47
|
+
|
48
|
+
# `defer: true` will no-op the following block on a direct
|
49
|
+
# visit, use null as a standin value, and append additional
|
50
|
+
# javascript in the response to fetch only this content node
|
51
|
+
# (no-oping other sibiling blocks) and graft it in the right
|
52
|
+
# place on the client side.
|
53
|
+
json.dashboard(defer: true) do
|
54
|
+
sleep 10
|
55
|
+
json.num_of_views 100
|
56
|
+
end
|
57
|
+
|
58
|
+
# Go ahead, use your rails helpers, including i18n.
|
59
|
+
json.new_post_path new_post_path
|
60
|
+
|
61
|
+
json.footer 'something'
|
62
|
+
```
|
63
|
+
|
64
|
+
Then write your remaining view in JSX. The content you wrote earlier gets passed here.
|
65
|
+
|
66
|
+
```ruby
|
67
|
+
# app/assets/javascripts/views/PostIndex.js.jsx
|
68
|
+
|
69
|
+
App.Views.PostsIndex = function(json) {
|
70
|
+
// Deferment will use `null` as the standin value.
|
71
|
+
// Hence the need for `json.dashboard || {}`.
|
72
|
+
// Breezy will then fetch the missing node
|
73
|
+
// and call `ReactDOM.render` a second time
|
74
|
+
|
75
|
+
var dashboard = json.dashboard || {};
|
76
|
+
|
77
|
+
return (
|
78
|
+
<h1> {json.heading}</h1>
|
79
|
+
<div> {dashboard.num_of_views} </div>
|
80
|
+
|
81
|
+
# Page to page without reloading
|
82
|
+
<a href={json.new_post_path} data-bz-remote> Create </a>
|
83
|
+
|
84
|
+
<div>{json.footer}</div>
|
85
|
+
)
|
86
|
+
}
|
87
|
+
```
|
88
|
+
|
89
|
+
## Installation
|
90
|
+
Breezy does not include ReactJS, you'll have to download it seperately and include it in your path. Or just include [react-rails](https://github.com/reactjs/react-rails).
|
91
|
+
|
92
|
+
```
|
93
|
+
gem 'breezy', git: 'https://github.com/jho406/Breezy.git'
|
94
|
+
```
|
95
|
+
|
96
|
+
Then use the provided installation generator:
|
97
|
+
```
|
98
|
+
rails g breezy:install
|
99
|
+
```
|
100
|
+
|
101
|
+
If you need to add breezy and JSX views:
|
102
|
+
```
|
103
|
+
rails g breezy:view Posts new index
|
104
|
+
```
|
105
|
+
|
106
|
+
# Navigation and Forms
|
107
|
+
Breezy intercepts all clicks on `<a>` and all submits on `<form>` elements enabled with `data-bz-remote`. Breezy will `preventDefault` then make the same request using XMLHttpRequest with a content type of `.js`. If there's an existing request, Breezy will cancel it unless the `data-bz-async` option is used.
|
108
|
+
|
109
|
+
Once the response loads, a `breezy:load` event will be fired with the JS object that you created with BreezyTemplates. If you used the installation generator, the event will be set for you in the `<head>` element of your layout:
|
110
|
+
|
111
|
+
```javascript
|
112
|
+
document.addEventListener('breezy:load', function(event){
|
113
|
+
var props = {
|
114
|
+
view: event.data.view,
|
115
|
+
data: event.data.data
|
116
|
+
}
|
117
|
+
ReactDOM.render(React.createElement(window.App.Components.View, props), document.getElementById('app'));
|
118
|
+
});
|
119
|
+
```
|
120
|
+
|
121
|
+
## The data-bz-* attribute API
|
122
|
+
|
123
|
+
Attribute | default value | description
|
124
|
+
-------------------|--------------------------|------------
|
125
|
+
`data-bz-remote` | For `<a>` the default is `get`. For forms, the default is `post` if a method is not specified. | Use this to create seamless page to page transitions. Works for both links and forms. You can specify the request method by giving it a value, e.g `<a href='foobar' data-bz-remote=post>`. For forms, the request method of the form is used. `<form action=foobar method='post' data-bz-remote>`.
|
126
|
+
`data-bz-async` | `false` | Fires off an async request. Responses are pushed into a queue will be evaluated in order of click or submit.
|
127
|
+
`data-bz-push-state` | `true` | Captures the element's URL in the browsers history. Normally used with `data-bz-async`.
|
128
|
+
`data-bz-silent` | false | To be used with the `breezy_silent?` ruby helper. Useful if you don't want to perform a redirect or render. Just return a 204, and Breezy will not fire a `breezy:load` event.
|
129
|
+
|
130
|
+
|
131
|
+
# Events
|
132
|
+
Event | Argument `originalEvent.data` | Notes
|
133
|
+
----------------------|--------------------------------|-------
|
134
|
+
`breezy:load` | {data} | Triggered on document, when Breezy has succesfully loaded content, to be used with `ReactDOM.render`. Yes the key is a bit weird. You have to access it like so `event.data.data`.
|
135
|
+
`breezy:click` | {url} | Triggered on the element when a form or a link enabled with data-bz-remote is clicked. Cancellable with event.preventDefault().
|
136
|
+
`breezy:request-error` | null or {xhr} | Triggered on the element when on XHR onError (network issues) or when async option is used and recieves an error response.
|
137
|
+
`breezy:request-start` | {url} | Triggered on the element just before a XHR request is made.
|
138
|
+
`breezy:request-end` | {url} | Triggered on the element, when a XHR request is finished.
|
139
|
+
`breezy:restore` | null | Triggered on document, when a page cached is loaded, just before `breezy:load`
|
140
|
+
|
141
|
+
## JS API Reference
|
142
|
+
|
143
|
+
### Breezy.visit
|
144
|
+
|
145
|
+
Usage:
|
146
|
+
```javascript
|
147
|
+
Breezy.visit(location)
|
148
|
+
Breezy.visit(location, { pushState, silent, async })
|
149
|
+
```
|
150
|
+
Performs an Application Visit to the given _location_ (a string containing a URL or path).
|
151
|
+
|
152
|
+
- If the pushState option is specified, Breezy will determine wheather to add the visitation to the browsers history. The default value is `true`.
|
153
|
+
- If async is specified, Breezy will make an async request and add the onload callback to a queue to be evaluated (calling `breezy:load`) in order of fire. The default value is `false`, this means if there's an existing request or a queue of async requests, Breezy will cancel all of them and give priority to the most recent call.
|
154
|
+
- If silent is specified, a request header X-SILENT will be set. use in tadem with the `breezy_silent?` ruby method for when you want to perform an action but return a 204 instead of a redirect or render. Breezy will ignore 204s and will not attempt to fire `breezy:load`.
|
155
|
+
|
156
|
+
|
157
|
+
### Breezy.graftByKeypath
|
158
|
+
|
159
|
+
Usage:
|
160
|
+
```javascript
|
161
|
+
Breezy.graftByKeypath(keyPath, object, {type});
|
162
|
+
```
|
163
|
+
Place a new object at the specified keypath of Breezy's content tree on the current page and across other pages in its cache. Parent objects are clone and `breezy:load` is finally called.
|
164
|
+
|
165
|
+
When referencing an array of objects, you have the option of providing an id instead of an index. For example:
|
166
|
+
|
167
|
+
```
|
168
|
+
a.b.some_array_element_id_of_your_choice=1.c.d
|
169
|
+
```
|
170
|
+
|
171
|
+
If type is specified as `add`. Breezy will push the object at the keyPath (assuming its an array) instead of of replacing.
|
172
|
+
|
173
|
+
|
174
|
+
### Breezy.replace
|
175
|
+
Usage:
|
176
|
+
```javascript
|
177
|
+
Breezy.replace({data, title, csrf_token, assets})
|
178
|
+
```
|
179
|
+
Replaces the current page content and triggers a `reload:load`. Normally used to inject content to Breezy on a direct visit. Breezy's generators will set this up for you.
|
180
|
+
|
181
|
+
## Ruby Helpers
|
182
|
+
|
183
|
+
|
184
|
+
### use_breezy_html
|
185
|
+
Usage:
|
186
|
+
```ruby
|
187
|
+
class PostController < ApplicationController
|
188
|
+
before_action :use_breezy_html
|
189
|
+
end
|
190
|
+
```
|
191
|
+
|
192
|
+
On direct visits, Breezy will render an empty page. If you used the installation generator, Breezy will also inject your content view created by BreezyTemplates into a script header, then fire a `breezy:load` event that you can use with `ReactDOM.render`.
|
193
|
+
|
194
|
+
### breezy_silient?
|
195
|
+
Usage:
|
196
|
+
|
197
|
+
```
|
198
|
+
class PostController < ApplicationController
|
199
|
+
def create
|
200
|
+
...
|
201
|
+
if breezy_silent?
|
202
|
+
...
|
203
|
+
end
|
204
|
+
end
|
205
|
+
end
|
206
|
+
```
|
207
|
+
|
208
|
+
Used in conjuction with `data-bz-silent` for `204` responses. Great for when you want to run a job and don't want to render anything back to the client.
|
209
|
+
|
210
|
+
## BreezyTemplate Templates, your content view
|
211
|
+
BreezyTemplates is a sibling of JBuilderTemplates, both inheriting from the same [parent](https://github.com/rails/jbuilder/blob/master/lib/jbuilder.rb). Unlike Jbuilder, BreezyTemplate generates Server Generated Javascript and has a few differences listed below.
|
212
|
+
|
213
|
+
###Partials
|
214
|
+
Partials are only supported as an option on attribute or array! `set!`s.
|
215
|
+
Usage:
|
216
|
+
|
217
|
+
```ruby
|
218
|
+
# We use a `nil` because of the last argument hash. The contents of the partial is what becomes the value.
|
219
|
+
json.post nil, partial: "blog_post"
|
220
|
+
|
221
|
+
or
|
222
|
+
|
223
|
+
# Set @post as a local `article` within the `blog_post` partial.
|
224
|
+
json.post @post, partial: "blog_post", as: 'article'
|
225
|
+
|
226
|
+
or
|
227
|
+
# Add more locals
|
228
|
+
json.post @big_post, partial: "blog_post", locals: {email: 'tests@test.com'}
|
229
|
+
|
230
|
+
or
|
231
|
+
|
232
|
+
# Use a partial for each element in an array
|
233
|
+
json.array! @posts, partial: "blog_post", as: :blog_post
|
234
|
+
```
|
235
|
+
|
236
|
+
### Caching
|
237
|
+
Caching is only available as an option on an attribute and can be used in tandem with partials.
|
238
|
+
|
239
|
+
Usage:
|
240
|
+
|
241
|
+
```ruby
|
242
|
+
json.author(cache: ["some_cache_key"]) do
|
243
|
+
json.first_name "tommy"
|
244
|
+
end
|
245
|
+
|
246
|
+
or
|
247
|
+
|
248
|
+
json.profile "hello", cache: "cachekey"
|
249
|
+
|
250
|
+
or
|
251
|
+
|
252
|
+
json.profile nil, cache: "cachekey", partial: "profile", locals: {email: "test@test.com"}
|
253
|
+
```
|
254
|
+
|
255
|
+
|
256
|
+
### No merge of duplicate `set!`s
|
257
|
+
Unlike Jbuilder, BreezyTemplates will not merge duplicate `set!`s. Instead, the last duplicate will override the first.
|
258
|
+
|
259
|
+
Usage:
|
260
|
+
```ruby
|
261
|
+
json.address do
|
262
|
+
json.street '123 road'
|
263
|
+
end
|
264
|
+
|
265
|
+
json.address do
|
266
|
+
json.zip 10002
|
267
|
+
end
|
268
|
+
```
|
269
|
+
|
270
|
+
would become
|
271
|
+
|
272
|
+
```json
|
273
|
+
{address: {zip:10002}}
|
274
|
+
```
|
275
|
+
|
276
|
+
### Deferment
|
277
|
+
You can defer rendering of expensive content using the `defer: true` option available in blocks. Behind the scenes BreezyTemplates will no-op the block entirely, replace the value with a `null` as a standin, and append a `Breezy.visit(/somepath?_breezy_filter=keypath.to.node)` to the response. When the client recieves the payload, `breezy:load` will be fired, then the appended `Breezy.visit` will be called to fetch and graft the missing node before firing `breezy:load` a second time.
|
278
|
+
|
279
|
+
Usage:
|
280
|
+
```ruby
|
281
|
+
json.dashboard(defer: true) do
|
282
|
+
sleep 10
|
283
|
+
json.some_fancy_metric 42
|
284
|
+
end
|
285
|
+
```
|
286
|
+
|
287
|
+
#### Working with arrays
|
288
|
+
If you want to defer elements in an array, you should add a key as an option on `array!` to help breezy generate a more specific keypath, otherwise it'll just use the index.
|
289
|
+
|
290
|
+
```ruby
|
291
|
+
data = [{id: 1, name: 'foo'}, {id: 2, name: 'bar'}]
|
292
|
+
|
293
|
+
json.array! data, key: :id do
|
294
|
+
json.greeting defer: true do
|
295
|
+
json.greet 'hi'
|
296
|
+
end
|
297
|
+
end
|
298
|
+
```
|
299
|
+
|
300
|
+
### Filtering nodes
|
301
|
+
As seen previously, Breezy can filter your content tree for a specific node. This is done by adding a `_breezy_filter=keypath.to.node` in your URL param and setting the content type to `.js`. BreezyTemplates will no-op all node blocks that are not in the keypath, ignore deferment and caching (if an `ActiveRecord::Relation` is encountered, it will append a where clause with your provided id) while traversing, and return the node. Breezy will then graft that node back onto its tree on the client side and call `breezy:onload` with the new tree. This is done automatically when using deferment, but you can use this param separately in tandem with `data-bz-remote`.
|
302
|
+
|
303
|
+
For example, to create seamless ajaxy pagination for a specific part of your page, just create a link like the following:
|
304
|
+
|
305
|
+
```html
|
306
|
+
<a href="posts?page_num=2&_breezy_filter=key.path.to.posts" data-bz-remote> Next Page </a>
|
307
|
+
```
|
308
|
+
|
309
|
+
Filtering works off your existing route and content tree, so no additional API necessary.
|
310
|
+
|
311
|
+
|
312
|
+
##Running the tests
|
313
|
+
|
314
|
+
Ruby:
|
315
|
+
|
316
|
+
```
|
317
|
+
BUNDLE_GEMFILE=Gemfile.rails50 bundle
|
318
|
+
BUNDLE_GEMFILE=Gemfile.rails50 rake test
|
319
|
+
```
|
320
|
+
|
321
|
+
JavaScript:
|
322
|
+
|
323
|
+
```
|
324
|
+
bundle install
|
325
|
+
bundle exec blade runner
|
326
|
+
```
|
data/lib/breezy.rb
ADDED
@@ -0,0 +1,71 @@
|
|
1
|
+
require 'breezy/version'
|
2
|
+
require 'breezy/xhr_headers'
|
3
|
+
require 'breezy/xhr_redirect'
|
4
|
+
require 'breezy/xhr_url_for'
|
5
|
+
require 'breezy/cookies'
|
6
|
+
require 'breezy/x_domain_blocker'
|
7
|
+
require 'breezy/render'
|
8
|
+
require 'breezy/helpers'
|
9
|
+
require 'breezy/configuration'
|
10
|
+
require 'breezy_template'
|
11
|
+
|
12
|
+
module Breezy
|
13
|
+
module Controller
|
14
|
+
include XHRHeaders, Cookies, XDomainBlocker, Render, Helpers
|
15
|
+
|
16
|
+
def self.included(base)
|
17
|
+
if base.respond_to?(:before_action)
|
18
|
+
base.before_action :set_xhr_redirected_to, :set_request_method_cookie
|
19
|
+
base.after_action :abort_xdomain_redirect
|
20
|
+
else
|
21
|
+
base.before_filter :set_xhr_redirected_to, :set_request_method_cookie
|
22
|
+
base.after_filter :abort_xdomain_redirect
|
23
|
+
end
|
24
|
+
|
25
|
+
if base.respond_to?(:helper_method)
|
26
|
+
base.helper_method :breezy_tag
|
27
|
+
base.helper_method :breezy_silient?
|
28
|
+
base.helper_method :breezy_snippet
|
29
|
+
base.helper_method :use_breezy_html
|
30
|
+
base.helper_method :breezy_filter
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
class Engine < ::Rails::Engine
|
36
|
+
config.breezy = ActiveSupport::OrderedOptions.new
|
37
|
+
config.breezy.auto_include = true
|
38
|
+
|
39
|
+
initializer :breezy do |app|
|
40
|
+
ActiveSupport.on_load(:action_controller) do
|
41
|
+
next if self != ActionController::Base
|
42
|
+
|
43
|
+
if app.config.breezy.auto_include
|
44
|
+
include Controller
|
45
|
+
end
|
46
|
+
|
47
|
+
ActionDispatch::Request.class_eval do
|
48
|
+
def referer
|
49
|
+
self.headers['X-XHR-Referer'] || super
|
50
|
+
end
|
51
|
+
alias referrer referer
|
52
|
+
end
|
53
|
+
|
54
|
+
require 'action_dispatch/routing/redirection'
|
55
|
+
ActionDispatch::Routing::Redirect.class_eval do
|
56
|
+
prepend XHRRedirect
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
ActiveSupport.on_load(:action_view) do
|
61
|
+
ActionView::Template.register_template_handler :breezy, BreezyTemplate::Handler
|
62
|
+
require 'breezy_template/dependency_tracker'
|
63
|
+
require 'breezy/active_support'
|
64
|
+
|
65
|
+
(ActionView::RoutingUrlFor rescue ActionView::Helpers::UrlHelper).module_eval do
|
66
|
+
prepend XHRUrlFor
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
# https://github.com/rails/jbuilder/issues/204
|
2
|
+
|
3
|
+
if Rails.version >= '4.1'
|
4
|
+
module ActiveSupport
|
5
|
+
module JSON
|
6
|
+
module Encoding
|
7
|
+
class JSONGemEncoder
|
8
|
+
alias_method :original_jsonify, :jsonify
|
9
|
+
|
10
|
+
def jsonify(value)
|
11
|
+
if ::BreezyTemplate::Template::Digest === value
|
12
|
+
value
|
13
|
+
else
|
14
|
+
original_jsonify(value)
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
module Breezy
|
2
|
+
class Configuration
|
3
|
+
attr_accessor :track_assets
|
4
|
+
|
5
|
+
def initialize
|
6
|
+
@track_assets = ['application.js', 'application.css']
|
7
|
+
end
|
8
|
+
end
|
9
|
+
|
10
|
+
def self.configuration
|
11
|
+
@configuration ||= Configuration.new
|
12
|
+
end
|
13
|
+
|
14
|
+
def self.configuration=(config)
|
15
|
+
@configuration = config
|
16
|
+
end
|
17
|
+
|
18
|
+
def self.configure
|
19
|
+
yield configuration
|
20
|
+
end
|
21
|
+
end
|