Skip to content

Commit

Permalink
UIHandler: add clickElement() and submitForm() helpers (closes #11)
Browse files Browse the repository at this point in the history
  • Loading branch information
jiripudil committed Apr 16, 2018
1 parent 9c69c7d commit a237348
Show file tree
Hide file tree
Showing 4 changed files with 172 additions and 32 deletions.
14 changes: 13 additions & 1 deletion README.md
Expand Up @@ -97,6 +97,18 @@ naja.uiHandler.allowedOrigins.push('https://allowed.origin.com:4000');
The current origin is allowed by default, i.e. it does not matter whether the `href` in the link points to a relative path or an absolute one.


##### Manual dispatch

Since version 1.4.0, `UIHandler` exposes two helper methods for dispatching UI-bound requests manually. This is especially useful if you need to submit a form programmatically, because `form.submit()` does not trigger the form's `submit` event.

```js
naja.uiHandler.clickElement(element);
naja.uiHandler.submitForm(form);
```

Neither `element` nor `form` have to be bound to Naja via the configured selector. However, the aforementioned allowed origin rules still apply, and the `interaction` event (see below) is triggered with `originalEvent` set to undefined.


#### RedirectHandler

`RedirectHandler` performs a redirection if there is `redirect` key in the response payload.
Expand Down Expand Up @@ -200,7 +212,7 @@ The true power of Naja is in how easy you can implement your own extensions to i
- **load:** This event is dispatched after `init` and then after every request, be it successful or not. It can be used to reload things, re-add event listeners, etc. It has no properties.
- **interaction:** This event is dispatched when the user interacts with a DOM element that has the Naja's listener bound to it. It has the following properties:
- `element: HTMLElement`, the element the user interacted with,
- `originalEvent: Event`, the original UI event,
- `originalEvent: ?Event`, the original UI event, or undefined if the request was dispatched via `UIHandler.clickElement()` or `UIHandler.submitForm()` (see above),
- `options: Object`, an empty object that can be populated with options based on the element's attributes.
- **before:** This event is dispatched when the `XMLHttpRequest` object is created but not yet sent. At this point, you can call the event's `preventDefault()` method to cancel the request. The event has the following properties:
- `xhr: XMLHttpRequest`, the XHR object,
Expand Down
7 changes: 5 additions & 2 deletions src/core/FormsHandler.js
Expand Up @@ -21,8 +21,11 @@ export default class FormsHandler {
}

if ((element.tagName === 'form' || element.form) && window.Nette && ! window.Nette.validateForm(element)) {
originalEvent.stopImmediatePropagation();
originalEvent.preventDefault();
if (originalEvent) {
originalEvent.stopImmediatePropagation();
originalEvent.preventDefault();
}

evt.preventDefault();
}
}
Expand Down
93 changes: 64 additions & 29 deletions src/core/UIHandler.js
Expand Up @@ -45,49 +45,84 @@ export default class UIHandler {
}

const el = evt.currentTarget, options = {};

if (evt.type === 'submit') {
this.submitForm(el, options, evt);

} else if (evt.type === 'click') {
this.clickElement(el, options, evt);
}
}

clickElement(el, options = {}, evt) {
let method, url, data;

if ( ! this.naja.fireEvent('interaction', {element: el, originalEvent: evt, options})) {
evt.preventDefault();
if (evt) {
evt.preventDefault();
}

return;
}

if (evt.type === 'submit') {
method = el.method ? el.method.toUpperCase() : 'GET';
url = el.action || window.location.pathname + window.location.search;
data = new FormData(el);
if (el.tagName.toLowerCase() === 'a') {
method = 'GET';
url = el.href;
data = null;

} else if (evt.type === 'click') {
if (el.tagName.toLowerCase() === 'a') {
method = 'GET';
url = el.href;
data = null;

} else if (el.tagName.toLowerCase() === 'input' || el.tagName.toLowerCase() === 'button') {
const {form} = el;
method = form.method ? form.method.toUpperCase() : 'GET';
url = form.action || window.location.pathname + window.location.search;
data = new FormData(form);

if (el.type === 'submit' || el.tagName.toLowerCase() === 'button') {
data.append(el.name, el.value || '');

} else if (el.type === 'image') {
const coords = el.getBoundingClientRect();
data.append(`${el.name}.x`, Math.max(0, Math.floor(evt.pageX - coords.left)));
data.append(`${el.name}.y`, Math.max(0, Math.floor(evt.pageY - coords.top)));
}
} else if (el.tagName.toLowerCase() === 'input' || el.tagName.toLowerCase() === 'button') {
const {form} = el;
method = form.method ? form.method.toUpperCase() : 'GET';
url = form.action || window.location.pathname + window.location.search;
data = new FormData(form);

if (el.type === 'submit' || el.tagName.toLowerCase() === 'button') {
data.append(el.name, el.value || '');

} else if (el.type === 'image') {
const coords = el.getBoundingClientRect();
data.append(`${el.name}.x`, Math.max(0, Math.floor(evt.pageX - coords.left)));
data.append(`${el.name}.y`, Math.max(0, Math.floor(evt.pageY - coords.top)));
}
}

// ignore non-URL URIs (javascript:, data:, ...)
if (/^(?!https?)[^:/?#]+:/i.test(url)) {
if (this.isUrlAllowed(url)) {
if (evt) {
evt.preventDefault();
}

this.naja.makeRequest(method, url, data, options);
}
}

submitForm(form, options = {}, evt) {
if ( ! this.naja.fireEvent('interaction', {element: form, originalEvent: evt, options})) {
if (evt) {
evt.preventDefault();
}

return;
}

if ( ! /^https?/i.test(url) || this.allowedOrigins.some((origin) => new RegExp(`^${origin}`, 'i').test(url))) {
evt.preventDefault();
const method = form.method ? form.method.toUpperCase() : 'GET';
const url = form.action || window.location.pathname + window.location.search;
const data = new FormData(form);

if (this.isUrlAllowed(url)) {
if (evt) {
evt.preventDefault();
}

this.naja.makeRequest(method, url, data, options);
}
}

isUrlAllowed(url) {
// ignore non-URL URIs (javascript:, data:, ...)
if (/^(?!https?)[^:/?#]+:/i.test(url)) {
return false;
}

return ! /^https?/i.test(url) || this.allowedOrigins.some((origin) => new RegExp(`^${origin}`, 'i').test(url));
}
}
90 changes: 90 additions & 0 deletions tests/Naja.UIHandler.js
Expand Up @@ -394,4 +394,94 @@ describe('UIHandler', function () {
mock.verify();
});
});

describe('clickElement()', function () {
it('dispatches request', function () {
const naja = mockNaja();
const mock = sinon.mock(naja);

mock.expects('makeRequest')
.withExactArgs('GET', 'http://localhost:9876/UIHandler/clickElement', null, {})
.once();

const a = document.createElement('a');
a.href = '/UIHandler/clickElement';

const handler = new UIHandler(naja);
handler.clickElement(a);

mock.verify();
});

it('triggers interaction event', function () {
const naja = mockNaja();
const mock = sinon.mock(naja);

mock.expects('makeRequest')
.withExactArgs('GET', 'http://localhost:9876/UIHandler/clickElement', null, {})
.once();

const listener = sinon.spy();
naja.addEventListener('interaction', listener);

const a = document.createElement('a');
a.href = '/UIHandler/clickElement';

const handler = new UIHandler(naja);
handler.clickElement(a);

assert.isTrue(listener.calledWithMatch(sinon.match.object
.and(sinon.match.has('element', a))
.and(sinon.match.has('originalEvent', undefined))
));

mock.verify();
});
});

describe('submitForm()', function () {
it('dispatches request', function () {
const naja = mockNaja();
const mock = sinon.mock(naja);

mock.expects('makeRequest')
.withExactArgs('POST', 'http://localhost:9876/UIHandler/submitForm', sinon.match.instanceOf(FormData), {})
.once();

const form = document.createElement('form');
form.method = 'POST';
form.action = '/UIHandler/submitForm';

const handler = new UIHandler(naja);
handler.submitForm(form);

mock.verify();
});

it('triggers interaction event', function () {
const naja = mockNaja();
const mock = sinon.mock(naja);

mock.expects('makeRequest')
.withExactArgs('POST', 'http://localhost:9876/UIHandler/submitForm', sinon.match.instanceOf(FormData), {})
.once();

const listener = sinon.spy();
naja.addEventListener('interaction', listener);

const form = document.createElement('form');
form.method = 'POST';
form.action = '/UIHandler/submitForm';

const handler = new UIHandler(naja);
handler.submitForm(form);

assert.isTrue(listener.calledWithMatch(sinon.match.object
.and(sinon.match.has('element', form))
.and(sinon.match.has('originalEvent', undefined))
));

mock.verify();
});
});
});

0 comments on commit a237348

Please sign in to comment.