unpoly-rails 0.51.1 → 0.52.0

Sign up to get free protection for your applications and to get access to all the features.

Potentially problematic release.


This version of unpoly-rails might be problematic. Click here for more details.

@@ -60,6 +60,8 @@ up.form = (($) ->
60
60
  See the documentation for [`form[up-target]`](/form-up-target) for more
61
61
  information on how AJAX form submissions work in Unpoly.
62
62
 
63
+ Emits the event [`up:form:submit`](/up:form:submit).
64
+
63
65
  @function up.submit
64
66
  @param {Element|jQuery|string} formOrSelector
65
67
  A reference or selector for the form to submit.
@@ -154,17 +156,29 @@ up.form = (($) ->
154
156
  options.failTransition = false
155
157
  options.headers[up.protocol.config.validateHeader] = options.validate
156
158
 
157
- up.feedback.start($form)
159
+ up.bus.whenEmitted('up:form:submit', $element: $form).then ->
160
+ up.feedback.start($form)
158
161
 
159
- # If we can't update the location URL, fall back to a vanilla form submission.
160
- unless up.browser.canPushState() || options.history == false
161
- # Don't use up.browser.navigate(); It cannot deal with file inputs.
162
- $form.get(0).submit()
163
- return u.unresolvablePromise()
162
+ # If we can't update the location URL, fall back to a vanilla form submission.
163
+ unless up.browser.canPushState() || options.history == false
164
+ # Don't use up.browser.navigate(); It cannot deal with file inputs.
165
+ $form.get(0).submit()
166
+ return u.unresolvablePromise()
164
167
 
165
- promise = up.replace(target, url, options)
166
- u.always promise, -> up.feedback.stop($form)
167
- promise
168
+ promise = up.replace(target, url, options)
169
+ u.always promise, -> up.feedback.stop($form)
170
+ promise
171
+
172
+ ###*
173
+ This event is [emitted](/up.emit) when a form is [submitted](/up.submit) through Unpoly.
174
+
175
+ @event up:form:submit
176
+ @param {jQuery} event.$element
177
+ The `<form>` element that will be submitted.
178
+ @param event.preventDefault()
179
+ Event listeners may call this method to prevent the form from being submitted.
180
+ @stable
181
+ ###
168
182
 
169
183
  ###*
170
184
  Observes form fields and runs a callback when a value changes.
@@ -103,6 +103,8 @@ up.link = (($) ->
103
103
  or [`[up-modal]`](/a-up-modal), the corresponding UJS behavior will be activated
104
104
  just as if the user had clicked on the link.
105
105
 
106
+ Emits the event [`up:link:follow`](/up:link:follow).
107
+
106
108
  \#\#\# Examples
107
109
 
108
110
  Let's say you have a link with an [`a[up-target]`](/a-up-target) attribute:
@@ -133,6 +135,17 @@ up.link = (($) ->
133
135
  variant = followVariantForLink($link)
134
136
  variant.followLink($link, options)
135
137
 
138
+ ###*
139
+ This event is [emitted](/up.emit) when a link is [followed](/up.follow) through Unpoly.
140
+
141
+ @event up:link:follow
142
+ @param {jQuery} event.$element
143
+ The link element that will be followed.
144
+ @param event.preventDefault()
145
+ Event listeners may call this method to prevent the link from being followed.
146
+ @stable
147
+ ###
148
+
136
149
  ###*
137
150
  @function defaultFollow
138
151
  @internal
@@ -22,13 +22,11 @@ in your controllers and views. If your server-side app uses another language
22
22
  or framework, you should be able to implement the protocol in a very short time.
23
23
 
24
24
 
25
- \#\#\# Redirect detection
25
+ \#\#\# Redirect detection for IE11
26
26
 
27
- Unpoly requires an additional response header to detect redirects, which are
28
- otherwise undetectable for any AJAX client.
29
-
30
- After the form's action performs a redirect, the next response should include the new
31
- URL in the HTTP headers:
27
+ On Internet Explorer 11, Unpoly cannot detect the final URL after a redirect.
28
+ You can fix this edge case by delivering an additional HTTP header
29
+ with the *last* response in a series of redirects:
32
30
 
33
31
  ```http
34
32
  X-Up-Location: /current-url
@@ -179,7 +177,7 @@ up.protocol = (($) ->
179
177
  @internal
180
178
  ###
181
179
  locationFromXhr = (xhr) ->
182
- xhr.getResponseHeader(config.locationHeader)
180
+ xhr.getResponseHeader(config.locationHeader) || xhr.responseURL
183
181
 
184
182
  ###*
185
183
  @function up.protocol.titleFromXhr
@@ -459,7 +459,7 @@ up.proxy = (($) ->
459
459
 
460
460
  registerAliasForRedirect = (response) ->
461
461
  request = response.request
462
- if request.url != response.url
462
+ if response.url && request.url != response.url
463
463
  newRequest = request.copy(
464
464
  method: response.method
465
465
  url: response.url
@@ -218,6 +218,9 @@ up.syntax = (($) ->
218
218
  @stable
219
219
  ###
220
220
  compiler = (selector, args...) ->
221
+ # Developer might still call top-level compiler registrations even when we don't boot
222
+ # due to an unsupported browser. In that case do no work and exit early.
223
+ return unless up.browser.isSupported()
221
224
  callback = args.pop()
222
225
  options = u.options(args[0])
223
226
  insertCompiler(compilers, selector, options, callback)
@@ -264,6 +267,9 @@ up.syntax = (($) ->
264
267
  @stable
265
268
  ###
266
269
  macro = (selector, args...) ->
270
+ # Developer might still call top-level compiler registrations even when we don't boot
271
+ # due to an unsupported browser. In that case do no work and exit early.
272
+ return unless up.browser.isSupported()
267
273
  callback = args.pop()
268
274
  options = u.options(args[0])
269
275
  if isBooting
@@ -1330,9 +1330,10 @@ up.util = (($) ->
1330
1330
  ###
1331
1331
  requestDataAsQuery = (data, opts) ->
1332
1332
  opts = options(opts, purpose: 'url')
1333
+
1333
1334
  if isString(data)
1334
- data
1335
- if isFormData(data)
1335
+ data.replace(/^\?/, '')
1336
+ else if isFormData(data)
1336
1337
  # Until FormData#entries is implemented in all major browsers we must give up here.
1337
1338
  # However, up.form will prefer to serialize forms as arrays, so we should be good
1338
1339
  # in most cases. We only use FormData for forms with file inputs.
@@ -1409,6 +1410,18 @@ up.util = (($) ->
1409
1410
  data = [data, newPair].join('&')
1410
1411
  data
1411
1412
 
1413
+ ###*
1414
+ Merges the request data in `source` into `target`.
1415
+ Will modify the passed-in `target`.
1416
+
1417
+ @return
1418
+ The merged form data.
1419
+ ###
1420
+ mergeRequestData = (target, source) ->
1421
+ each requestDataAsArray(source), (field) ->
1422
+ target = appendRequestData(target, field.name, field.value)
1423
+ target
1424
+
1412
1425
  ###*
1413
1426
  Throws a [JavaScript error](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Error)
1414
1427
  with the given message.
@@ -1729,6 +1742,7 @@ up.util = (($) ->
1729
1742
  requestDataAsArray: requestDataAsArray
1730
1743
  requestDataAsQuery: requestDataAsQuery
1731
1744
  appendRequestData: appendRequestData
1745
+ mergeRequestData: mergeRequestData
1732
1746
  requestDataFromForm: requestDataFromForm
1733
1747
  offsetParent: offsetParent
1734
1748
  fixedToAbsolute: fixedToAbsolute
@@ -4,6 +4,6 @@ module Unpoly
4
4
  # The current version of the unpoly-rails gem.
5
5
  # This version number is also used for releases of the Unpoly
6
6
  # frontend code.
7
- VERSION = '0.51.1'
7
+ VERSION = '0.52.0'
8
8
  end
9
9
  end
data/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "unpoly",
3
- "version": "0.51.1",
3
+ "version": "0.52.0",
4
4
  "description": "Unobtrusive JavaScript framework",
5
5
  "main": "dist/unpoly.js",
6
6
  "files": [
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: ..
3
3
  specs:
4
- unpoly-rails (0.50.2)
4
+ unpoly-rails (0.51.1)
5
5
  rails (>= 3)
6
6
 
7
7
  GEM
@@ -0,0 +1,17 @@
1
+ module FormTest
2
+ class RedirectsController < ApplicationController
3
+
4
+ layout 'integration_test'
5
+
6
+ def new
7
+ end
8
+
9
+ def create
10
+ redirect_to target_form_test_redirect_path
11
+ end
12
+
13
+ def target
14
+ end
15
+
16
+ end
17
+ end
@@ -0,0 +1,27 @@
1
+ <div class="example">
2
+
3
+ <h2>Redirecting form</h2>
4
+
5
+ <%= form_tag "/form_test/redirect", method: :post, 'up-target' => '.result' do %>
6
+
7
+ <p>
8
+ <input type="text" name="text-param" value="text-value" />
9
+ </p>
10
+
11
+ <p>
12
+ <input type="password" name="password-param" value="password-value" />
13
+ </p>
14
+
15
+ <p>
16
+ <select name="select-param">
17
+ <option value="select-value">select-label</option>
18
+ </select>
19
+ </p>
20
+
21
+ <p>
22
+ <button type="submit">Submit</button>
23
+ </p>
24
+
25
+ <% end %>
26
+
27
+ </div>
@@ -0,0 +1,4 @@
1
+ <div class="example">
2
+ <p>Redirect target reached</p>
3
+ <p>URL should be <code><%= target_form_test_redirect_path %></code></p>
4
+ </div>
@@ -56,6 +56,7 @@
56
56
  </li>
57
57
  <li><%= link_to 'Form (basic)', '/form_test/basic/new' %></li>
58
58
  <li><%= link_to 'Form (upload)', '/form_test/upload/new' %></li>
59
+ <li><%= link_to 'Form (redirect)', '/form_test/redirect/new' %></li>
59
60
  <li><%= link_to 'Error', '/error_test/trigger' %></li>
60
61
  <li><%= link_to 'Booting with non-GET method', '/method_test/page1' %></li>
61
62
  <li><%= link_to 'Fragment update', '/replace_test/page1' %></li>
@@ -15,6 +15,11 @@ Rails.application.routes.draw do
15
15
  namespace :form_test do
16
16
  resource :basic, only: [:new, :create]
17
17
  resource :upload, only: [:new, :create]
18
+ resource :redirect, only: [:new, :create] do
19
+ member do
20
+ get :target
21
+ end
22
+ end
18
23
  end
19
24
 
20
25
  end
@@ -20,3 +20,5 @@ beforeEach ->
20
20
  contentType: options.contentType || 'text/html'
21
21
  responseHeaders: options.responseHeaders
22
22
  responseText: responseText
23
+ responseURL: options.responseURL
24
+
@@ -23,6 +23,14 @@ describe 'up.browser', ->
23
23
  # No params should be left in the form
24
24
  expect($form.find('input')).not.toExist()
25
25
 
26
+ it 'merges params from the given URL and the { data } option', ->
27
+ submitForm = spyOn(up.browser, 'submitForm')
28
+ up.browser.navigate('/foo?param1=param1%20value', method: 'GET', data: { param2: 'param2 value' })
29
+ expect(submitForm).toHaveBeenCalled()
30
+ $form = $('form.up-page-loader')
31
+ expect($form).toExist()
32
+ expect($form.attr('action')).toMatchUrl('/foo?param1=param1%20value&param2=param2%20value')
33
+
26
34
  describe "for POST requests", ->
27
35
 
28
36
  it "creates a POST form, adds all { data } params a hidden fields and submits the form", ->
@@ -36,6 +44,17 @@ describe 'up.browser', ->
36
44
  expect($form.find('input[name="param1"][value="param1 value"]')).toExist()
37
45
  expect($form.find('input[name="param2"][value="param2 value"]')).toExist()
38
46
 
47
+ it 'merges params from the given URL and the { data } option', ->
48
+ submitForm = spyOn(up.browser, 'submitForm')
49
+ up.browser.navigate('/foo?param1=param1%20value', method: 'POST', data: { param2: 'param2 value' })
50
+ expect(submitForm).toHaveBeenCalled()
51
+ $form = $('form.up-page-loader')
52
+ expect($form).toExist()
53
+ expect($form.attr('action')).toMatchUrl('/foo')
54
+ expect($form.attr('method')).toEqual('POST')
55
+ expect($form.find('input[name="param1"][value="param1 value"]')).toExist()
56
+ expect($form.find('input[name="param2"][value="param2 value"]')).toExist()
57
+
39
58
  u.each ['PUT', 'PATCH', 'DELETE'], (method) ->
40
59
 
41
60
  describe "for #{method} requests", ->
@@ -263,6 +263,24 @@ describe 'up.form', ->
263
263
 
264
264
  describe 'up.submit', ->
265
265
 
266
+ it 'emits a preventable up:form:submit event', asyncSpec (next) ->
267
+ $form = affix('form[action="/form-target"][up-target=".response"]')
268
+
269
+ listener = jasmine.createSpy('submit listener').and.callFake (event) ->
270
+ event.preventDefault()
271
+
272
+ $form.on('up:form:submit', listener)
273
+
274
+ up.submit($form)
275
+
276
+ next =>
277
+ expect(listener).toHaveBeenCalled()
278
+ event = listener.calls.mostRecent().args[0]
279
+ expect(event.$element).toEqual($form)
280
+
281
+ # No request should be made because we prevented the event
282
+ expect(jasmine.Ajax.requests.count()).toEqual(0)
283
+
266
284
  describeCapability 'canPushState', ->
267
285
 
268
286
  beforeEach ->
@@ -6,6 +6,24 @@ describe 'up.link', ->
6
6
 
7
7
  describe 'up.follow', ->
8
8
 
9
+ it 'emits a preventable up:link:follow event', asyncSpec (next) ->
10
+ $link = affix('a[href="/destination"][up-target=".response"]')
11
+
12
+ listener = jasmine.createSpy('follow listener').and.callFake (event) ->
13
+ event.preventDefault()
14
+
15
+ $link.on('up:link:follow', listener)
16
+
17
+ up.follow($link)
18
+
19
+ next =>
20
+ expect(listener).toHaveBeenCalled()
21
+ event = listener.calls.mostRecent().args[0]
22
+ expect(event.$element).toEqual($link)
23
+
24
+ # No request should be made because we prevented the event
25
+ expect(jasmine.Ajax.requests.count()).toEqual(0)
26
+
9
27
  describeCapability 'canPushState', ->
10
28
 
11
29
  it 'loads the given link via AJAX and replaces the response in the given target', asyncSpec (next) ->
@@ -182,6 +182,65 @@ describe 'up.proxy', ->
182
182
  # See that the promise was not rejected due to an internal error.
183
183
  expect(result.state).toEqual('pending')
184
184
 
185
+
186
+ describe 'when the XHR object has a { responseURL } property', ->
187
+
188
+ it 'sets the { url } property on the response object', (done) ->
189
+ promise = up.request('/request-url#request-hash')
190
+
191
+ u.nextFrame =>
192
+ @respondWith
193
+ responseURL: '/response-url'
194
+
195
+ promise.then (response) ->
196
+ expect(response.request.url).toMatchUrl('/request-url')
197
+ expect(response.request.hash).toEqual('#request-hash')
198
+ expect(response.url).toMatchUrl('/response-url')
199
+ done()
200
+
201
+ it 'considers a redirection URL an alias for the requested URL', asyncSpec (next) ->
202
+ up.request('/foo')
203
+
204
+ next =>
205
+ expect(jasmine.Ajax.requests.count()).toEqual(1)
206
+ @respondWith
207
+ responseURL: '/bar'
208
+
209
+ next =>
210
+ up.request('/bar')
211
+
212
+ next =>
213
+ # See that the cached alias is used and no additional requests are made
214
+ expect(jasmine.Ajax.requests.count()).toEqual(1)
215
+
216
+ it 'does not considers a redirection URL an alias for the requested URL if the original request was never cached', asyncSpec (next) ->
217
+ up.request('/foo', method: 'post') # POST requests are not cached
218
+
219
+ next =>
220
+ expect(jasmine.Ajax.requests.count()).toEqual(1)
221
+ @respondWith
222
+ responseURL: '/bar'
223
+
224
+ next =>
225
+ up.request('/bar')
226
+
227
+ next =>
228
+ # See that an additional request was made
229
+ expect(jasmine.Ajax.requests.count()).toEqual(2)
230
+
231
+ it 'does not considers a redirection URL an alias for the requested URL if the response returned a non-200 status code', asyncSpec (next) ->
232
+ up.request('/foo')
233
+
234
+ next =>
235
+ expect(jasmine.Ajax.requests.count()).toEqual(1)
236
+ @respondWith
237
+ responseURL: '/bar'
238
+ status: 500
239
+
240
+ next =>
241
+ up.request('/bar')
242
+
243
+
185
244
  describe 'CSRF', ->
186
245
 
187
246
  beforeEach ->
@@ -485,6 +485,14 @@ describe 'up.util', ->
485
485
  ])
486
486
  expect(string).toEqual("foo-key=foo#{encodedSpace}value&bar-key=bar#{encodedSpace}value")
487
487
 
488
+ it 'returns a given query string', ->
489
+ string = up.util.requestDataAsQuery('foo=bar')
490
+ expect(string).toEqual('foo=bar')
491
+
492
+ it 'strips a leading question mark from the given query string', ->
493
+ string = up.util.requestDataAsQuery('?foo=bar')
494
+ expect(string).toEqual('foo=bar')
495
+
488
496
  it 'returns an empty string for an empty object', ->
489
497
  string = up.util.requestDataAsQuery({})
490
498
  expect(string).toEqual('')
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: unpoly-rails
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.51.1
4
+ version: 0.52.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Henning Koch
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2018-01-15 00:00:00.000000000 Z
11
+ date: 2018-02-08 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rails
@@ -162,6 +162,7 @@ files:
162
162
  - spec_app/app/controllers/css_test_controller.rb
163
163
  - spec_app/app/controllers/error_test_controller.rb
164
164
  - spec_app/app/controllers/form_test/basics_controller.rb
165
+ - spec_app/app/controllers/form_test/redirects_controller.rb
165
166
  - spec_app/app/controllers/form_test/uploads_controller.rb
166
167
  - spec_app/app/controllers/method_test_controller.rb
167
168
  - spec_app/app/controllers/pages_controller.rb
@@ -178,6 +179,8 @@ files:
178
179
  - spec_app/app/views/error_test/trigger.erb
179
180
  - spec_app/app/views/error_test/unexpected_response.erb
180
181
  - spec_app/app/views/form_test/basics/new.erb
182
+ - spec_app/app/views/form_test/redirects/new.erb
183
+ - spec_app/app/views/form_test/redirects/target.erb
181
184
  - spec_app/app/views/form_test/submission_result.erb
182
185
  - spec_app/app/views/form_test/uploads/new.erb
183
186
  - spec_app/app/views/layouts/integration_test.erb