Skip to content

Commit

Permalink
Allow XHR Submissions on forms (#3483)
Browse files Browse the repository at this point in the history
  • Loading branch information
mkhatib authored Jun 10, 2016
1 parent 1f058cc commit 68201a5
Show file tree
Hide file tree
Showing 4 changed files with 196 additions and 4 deletions.
13 changes: 12 additions & 1 deletion build-system/server.js
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ app.use('/api/echo/post', function(req, res) {
res.end(JSON.stringify(req.body, null, 2));
});

app.use('/form/echo-html/post', function(req, res) {
app.use('/form/html/post', function(req, res) {
var form = new formidable.IncomingForm();
form.parse(req, function(err, fields) {
res.setHeader('Content-Type', 'text/html');
Expand All @@ -72,6 +72,17 @@ app.use('/form/echo-html/post', function(req, res) {
});
});

app.use('/form/echo-json/post', function(req, res) {
var form = new formidable.IncomingForm();
form.parse(req, function(err, fields) {
res.setHeader('Content-Type', 'application/json');
if (fields['email'] == '[email protected]') {
res.statusCode = 500;
}
res.end(JSON.stringify(fields));
});
});

// Fetches an AMP document from the AMP proxy and replaces JS
// URLs, so that they point to localhost.
function proxyToAmpProxy(req, res, minify) {
Expand Down
32 changes: 30 additions & 2 deletions examples/forms.amp.html
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
<meta name="viewport" content="width=device-width,minimum-scale=1,initial-scale=1">
<style amp-boilerplate>body{-webkit-animation:-amp-start 8s steps(1,end) 0s 1 normal both;-moz-animation:-amp-start 8s steps(1,end) 0s 1 normal both;-ms-animation:-amp-start 8s steps(1,end) 0s 1 normal both;animation:-amp-start 8s steps(1,end) 0s 1 normal both}@-webkit-keyframes -amp-start{from{visibility:hidden}to{visibility:visible}}@-moz-keyframes -amp-start{from{visibility:hidden}to{visibility:visible}}@-ms-keyframes -amp-start{from{visibility:hidden}to{visibility:visible}}@-o-keyframes -amp-start{from{visibility:hidden}to{visibility:visible}}@keyframes -amp-start{from{visibility:hidden}to{visibility:visible}}</style><noscript><style amp-boilerplate>body{-webkit-animation:none;-moz-animation:none;-ms-animation:none;animation:none}</style></noscript>
<script async src="https://cdn.ampproject.org/v0.js"></script>
<script async custom-element="amp-form" src="https://cdn.ampproject.org/v0/amp-form-0.1.js"></script>
<style amp-custom>
input:invalid {
background-color: #dc4e41;
Expand All @@ -30,9 +31,36 @@
</style>
</head>
<body>
<h2>Subscribe to our weekly Newsletter</h2>

<form method="post" action="http://localhost:8000/form/echo-html/post" target="_blank">
<h2>Subscribe to our weekly Newsletter (non-xhr)</h2>

<form method="post" action="http://localhost:8000/form/html/post" target="_blank">
<fieldset>
<label>
<span>Your name</span>
<input type="text" name="name" required>
</label>
<label>
<span>Your email</span>
<input type="email" name="email" required>
</label>
<input type="submit" value="Subscribe">
</fieldset>

<div class="invalid-message">
There are some input that needs fixing. Please fix them.
</div>
<div class="valid-message">
Everything looks good, you can submit now!
</div>
</form>

<h2>Subscribe to our weekly Newsletter (xhr)</h2>

<form method="post"
action="http://localhost:8000/form/html/post"
action-xhr="http://localhost:8000/form/echo-json/post"
target="_blank">
<fieldset>
<label>
<span>Your name</span>
Expand Down
78 changes: 77 additions & 1 deletion extensions/amp-form/0.1/amp-form.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,14 +16,90 @@

import {isExperimentOn} from '../../../src/experiments';
import {getService} from '../../../src/service';
import {assertHttpsUrl} from '../../../src/url';
import {user} from '../../../src/log';
import {onDocumentReady} from '../../../src/document-ready';
import {xhrFor} from '../../../src/xhr';
import {toArray} from '../../../src/types';
import {startsWith} from '../../../src/string';


/** @type {string} */
const TAG = 'amp-form';

export class AmpForm {

/**
* Adds functionality to the passed form element and listens to submit event.
* @param {!HTMLFormElement} element
*/
constructor(element) {
/** @const @private {!Window} */
this.win_ = element.ownerDocument.defaultView;

/** @const @private {!Element} */
this.form_ = element;

/** @const @private {!Xhr} */
this.xhr_ = xhrFor(this.win_);

/** @const @private {string} */
this.method_ = this.form_.getAttribute('method') || 'GET';

/** @const @private {?string} */
this.xhrAction_ = this.form_.getAttribute('action-xhr');
if (this.xhrAction_) {
assertHttpsUrl(this.xhrAction_, this.form_, 'action-xhr');
user.assert(!startsWith(this.xhrAction_, 'https://cdn.ampproject.org'),
'form action-xhr should not be on cdn.ampproject.org: %s',
this.form_);
}
this.installSubmitHandler_();
}

/** @private */
installSubmitHandler_() {
this.form_.addEventListener('submit', e => this.handleSubmit_(e));
}

/**
* @param {!Event} e
* @private
*/
handleSubmit_(e) {
if (e.defaultPrevented) {
return;
}
if (this.xhrAction_) {
e.preventDefault();
this.xhr_.fetchJson(this.xhrAction_, {
body: new FormData(this.form_),
method: this.method_,
credentials: 'include',
requireAmpResponseSourceOrigin: true,
});
}
}
}


/**
* Installs submission handler on all forms in the document.
* @param {!Window} win
*/
function installSubmissionHandlers(win) {
onDocumentReady(win.document, () => {
toArray(win.document.forms).forEach(form => {
new AmpForm(form);
});
});
}


function installAmpForm(win) {
return getService(win, 'amp-form', () => {
if (isExperimentOn(win, TAG)) {
// TODO: Write the implementation of the service.
installSubmissionHandlers(win);
}
return {};
});
Expand Down
77 changes: 77 additions & 0 deletions extensions/amp-form/0.1/test/test-amp-form.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,5 +14,82 @@
* limitations under the License.
*/

import {createIframePromise} from '../../../../testing/iframe';
import {AmpForm} from '../amp-form';
import * as sinon from 'sinon';

describe('amp-form', () => {

let sandbox;
beforeEach(() => {
sandbox = sinon.sandbox.create();
});

afterEach(() => {
sandbox.restore();
});

it('should assert valid action-xhr when provided', () => {
const form = document.createElement('form');
form.setAttribute('action-xhr', 'http://example.com');
expect(() => new AmpForm(form)).to.throw(
/form action-xhr must start with/);
form.setAttribute('action-xhr', 'https://cdn.ampproject.org/example.com');
expect(() => new AmpForm(form)).to.throw(
/form action-xhr should not be on cdn\.ampproject\.org/);
form.setAttribute('action-xhr', 'https://example.com');
expect(() => new AmpForm(form)).to.not.throw;
});

it('should listen to submit event', () => {
const form = document.createElement('form');
form.addEventListener = sandbox.spy();
form.setAttribute('action-xhr', 'https://example.com');
new AmpForm(form);
expect(form.addEventListener.called).to.be.true;
expect(form.addEventListener.calledWith('submit')).to.be.true;
});

it('should do nothing if defaultPrevented', () => {
const form = document.createElement('form');
form.setAttribute('action-xhr', 'https://example.com');
const ampForm = new AmpForm(form);
const event = {
target: form,
preventDefault: sandbox.spy(),
defaultPrevented: true,
};
ampForm.handleSubmit_(event);
expect(event.preventDefault.called).to.be.false;
});

it('should call fetchJson with the xhr action and form data', () => {
createIframePromise().then(iframe => {
const form = iframe.doc.createElement('form');
const nameInput = iframe.doc.createElement('input');
nameInput.setAttribute('name', 'name');
nameInput.setAttribute('value', 'John Miller');
form.appendChild(nameInput);
form.setAttribute('action-xhr', 'https://example.com');
const ampForm = new AmpForm(form);
ampForm.xhr_.fetchJson = sandbox.spy();
const event = {
target: form,
preventDefault: sandbox.spy(),
defaultPrevented: false,
};
ampForm.handleSubmit_(event);
expect(event.preventDefault.called).to.be.true;
expect(ampForm.xhr_.fetchJson.called).to.be.true;
expect(ampForm.xhr_.fetchJson.calledWith('https://example.com')).to.be.true;

const xhrCall = ampForm.xhr_.fetchJson.getCall(0);
const config = xhrCall.args[1];
expect(config.body.get('name')).to.be.equal('John Miller');
expect(config.method).to.equal('GET');
expect(config.credentials).to.equal('include');
expect(config.requireAmpResponseSourceOrigin).to.be.true;
});
});

});

0 comments on commit 68201a5

Please sign in to comment.