unpoly-rails 0.51.1 → 0.52.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.

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