'use strict';
var _ = require('lodash');
var FormUrlEncoded = require('form-urlencoded');
var Resource = require('./resource');
/**
* Forms should not be created on their own, they are normally
* accessed from a containing {@link Resource}.
* @constructor
* @arg {Object} data The form data, including field information
* @arg {Context} context The context of the form.
*
* @classdesc
* The {@link Form} class encapsulates a hypermedia form that can be
* updated with values at runtime and then submitted.
* TODO: More details on field values, etc.
*/
var Form = function(data, context) {
// Cloning is required to keep cloned Form
// instances separate.
_.merge(this, _.cloneDeep(data));
this.$$data = data;
this.$$context = context;
};
/**
* Lookup the field by the given name.
* @arg {string} name The name of the field to look up.
* @returns {Object} Object containing the field values.
*/
Form.prototype.field = function(name) {
return _.find(this.fields, 'name', name);
};
var ContentTypeDataTransformers = {
'application/json': function(data) {
return JSON.stringify(data);
},
'application/x-www-form-urlencoded': function(data) {
return data ? FormUrlEncoded.encode(data) : undefined;
},
'multipart/form-data': function(data) {
var fd = new FormData();
_.forEach(data, function(val, key) { fd.append(key, val); });
return fd;
}
};
/**
* Get the name/value data for all the fields of the form.
*
* @returns {?Object.<string, *>} The name/value data for the fields of the form.
*/
Form.prototype.getRequestData = function() {
if (!this.fields) {
return null;
}
return _(this.fields)
.indexBy('name')
.mapValues(_.property('value'))
.value();
};
/**
* Perform an HTTP request to submit the form. The request itself
* is created based on the URL, method, type, and field values.
* @arg {Object} [options] The options for the request.
* @arg {Object} [options.protocol] Options to pass to the underlying protocol,
* e.g. http/https.
* @returns {Resource} A resource that will eventually be resolved with response details.
*/
Form.prototype.submit = function(options) {
options = this.$$context.withDefaults(options);
var config = _.get(options, 'protocol', {});
config = _.merge({}, config, {
url: this.$$context.resolveUrl(this.href),
method: this.method,
transformRequest: [function(d, h) {
// Handle 'header getter' style headers, instead of bare object.
if (h instanceof Function) {
h = h();
}
var ct = (h['content-type'] || h['Content-Type']);
var extEncoders = _(this.$$context.extensions).pluck('encoders').compact();
var encoders = _(ContentTypeDataTransformers).concat(extEncoders.flatten().value()).reduce(_.merge);
var trans = encoders[ct];
return trans ? trans(d) : d;
}.bind(this)],
headers: { 'Content-Type': this.type || 'application/x-www-form-urlencoded' }
});
if (!config.headers.Accept) {
config.headers.Accept = this.$$context.acceptHeader();
}
if (this.fields) {
var prop = this.method === 'GET' ? 'params' : 'data';
config[prop] = this.getRequestData();
}
var ctx = this.$$context;
var resp = ctx.http(config).then(function(r) {
if (r.status !== 201 || !r.headers.location) {
return r;
}
var loc = r.headers.location;
ctx = ctx.forResource({url: config.url});
return ctx.http({method: 'GET', url: ctx.resolveUrl(loc), headers: config.headers });
});
return Resource.fromRequest(resp, ctx);
};
/**
* Clone the current {@link Form} so that fields can be updated
* and not impact/change the original form field values.
* @returns {Form} the cloned form.
*/
Form.prototype.clone = function() {
return new Form(this.$$data,
this.$$context);
};
module.exports = Form;