breezy 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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
@@ -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
+ ```
@@ -0,0 +1,4 @@
1
+ #= require_self
2
+ #= require breezy/start
3
+
4
+ @Breezy = {}
@@ -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