2018-10-19 15:17:02 +00:00
|
|
|
import { get, set, computed } from '@ember/object';
|
|
|
|
import Changeset from 'ember-changeset';
|
|
|
|
import lookupValidator from 'ember-changeset-validations';
|
|
|
|
|
|
|
|
// Keep these here for now so forms are easy to make
|
|
|
|
// TODO: Probably move this to utils/form/parse-element-name
|
|
|
|
import parseElementName from 'consul-ui/utils/get-form-name-property';
|
|
|
|
const defaultChangeset = function(data, validators) {
|
|
|
|
const changeset = new Changeset(data, lookupValidator(validators), validators);
|
|
|
|
// TODO: Currently supporting ember-data nicely like this
|
|
|
|
changeset.isSaving = computed('data.isSaving', function() {
|
|
|
|
return get(this, 'data.isSaving');
|
|
|
|
});
|
|
|
|
return changeset;
|
|
|
|
};
|
|
|
|
/**
|
2018-12-04 17:02:31 +00:00
|
|
|
* Form builder/Form factory
|
2018-10-19 15:17:02 +00:00
|
|
|
* Deals with handling (generally change) events and updating data in response to the change
|
|
|
|
* in a typical data down event up manner
|
|
|
|
* validations are included currently using ember-changeset-validations
|
|
|
|
*
|
|
|
|
* @param {string} name - The name of the form, generally this is the name of your model
|
|
|
|
* Generally (until view building is complete) you should name your form elements as `name="modelName[property]"`
|
|
|
|
* or pass this name through using you action and create an Event-like object instead
|
|
|
|
* You can also just not set a name and use `name="property"`, but if you want to use combinations
|
|
|
|
* if multiple forms at least form children should use names
|
|
|
|
*
|
|
|
|
* @param {object} config - Form configuration object. Just a plain object to configure the form should be a hash
|
|
|
|
* with property names relating to the form data. Each property is the configuration for that model/data property
|
|
|
|
* currently the only supported property of these configuration objects is `type` which currently allows you to
|
|
|
|
* set a property as 'array-like'
|
|
|
|
*/
|
|
|
|
export default function(changeset = defaultChangeset, getFormNameProperty = parseElementName) {
|
|
|
|
return function(name = '', obj = {}) {
|
|
|
|
const _children = {};
|
|
|
|
let _validators = null;
|
|
|
|
// TODO make this into a class to reuse prototype
|
2019-05-01 18:09:29 +00:00
|
|
|
const form = {
|
|
|
|
data: null,
|
|
|
|
name: name,
|
2018-10-19 15:17:02 +00:00
|
|
|
getName: function() {
|
2019-05-01 18:09:29 +00:00
|
|
|
return this.name;
|
2018-10-19 15:17:02 +00:00
|
|
|
},
|
|
|
|
setData: function(data) {
|
|
|
|
// Array check temporarily for when we get an empty array from repo.status
|
|
|
|
if (_validators && !Array.isArray(data)) {
|
2019-05-01 18:09:29 +00:00
|
|
|
data = changeset(data, _validators);
|
2018-10-19 15:17:02 +00:00
|
|
|
}
|
2019-05-01 18:09:29 +00:00
|
|
|
set(this, 'data', data);
|
2018-10-19 15:17:02 +00:00
|
|
|
return this;
|
|
|
|
},
|
|
|
|
getData: function() {
|
2019-05-01 18:09:29 +00:00
|
|
|
return this.data;
|
2018-10-19 15:17:02 +00:00
|
|
|
},
|
|
|
|
add: function(child) {
|
|
|
|
_children[child.getName()] = child;
|
|
|
|
return this;
|
|
|
|
},
|
2019-05-08 16:12:16 +00:00
|
|
|
handleEvent: function(e, targetName) {
|
2018-10-19 15:17:02 +00:00
|
|
|
const target = e.target;
|
2019-05-08 16:12:16 +00:00
|
|
|
// currently we only use targetName in {{form-component}} for handling deeply
|
|
|
|
// nested forms, once {{form-component}} handles deeply nested forms targetName can go
|
|
|
|
const parts = getFormNameProperty(targetName || target.name);
|
2018-10-19 15:17:02 +00:00
|
|
|
// split the form element name from `name[prop]`
|
|
|
|
const name = parts[0];
|
|
|
|
const prop = parts[1];
|
|
|
|
//
|
|
|
|
let config = obj;
|
|
|
|
// if the name (usually the name of the model) isn't this form, look at its children
|
2019-05-01 18:09:29 +00:00
|
|
|
if (name !== this.getName()) {
|
2018-10-19 15:17:02 +00:00
|
|
|
if (this.has(name)) {
|
|
|
|
// is its a child form then use the child form
|
|
|
|
return this.form(name).handleEvent(e);
|
|
|
|
}
|
|
|
|
// should probably throw here, unless we support having a name
|
|
|
|
// even if you are referring to this form
|
|
|
|
config = config[name];
|
|
|
|
}
|
|
|
|
const data = this.getData();
|
|
|
|
// ember-data/changeset dance
|
2019-02-21 10:36:15 +00:00
|
|
|
// TODO: This works for ember-data RecordSets and Changesets but not for plain js Objects
|
|
|
|
// see settings
|
2018-10-19 15:17:02 +00:00
|
|
|
const json = typeof data.toJSON === 'function' ? data.toJSON() : get(data, 'data').toJSON();
|
|
|
|
// if the form doesn't include a property then throw so it can be
|
|
|
|
// caught outside, therefore the user can deal with things that aren't in the data
|
2019-02-21 10:36:15 +00:00
|
|
|
// TODO: possibly need to add support for deeper properties using `get` here
|
|
|
|
// for example `client.blocking` instead of just `blocking`
|
2018-10-19 15:17:02 +00:00
|
|
|
if (!Object.keys(json).includes(prop)) {
|
|
|
|
const error = new Error(`${prop} property doesn't exist`);
|
|
|
|
error.target = target;
|
|
|
|
throw error;
|
|
|
|
}
|
|
|
|
// deal with the change of property
|
|
|
|
let currentValue = get(data, prop);
|
|
|
|
// if the value is an array-like or config says its an array
|
|
|
|
if (
|
|
|
|
Array.isArray(currentValue) ||
|
|
|
|
(typeof config[prop] !== 'undefined' &&
|
|
|
|
typeof config[prop].type === 'string' &&
|
|
|
|
config[prop].type.toLowerCase() === 'array')
|
|
|
|
) {
|
|
|
|
// array specific set
|
|
|
|
if (currentValue == null) {
|
|
|
|
currentValue = [];
|
|
|
|
}
|
|
|
|
const method = target.checked ? 'pushObject' : 'removeObject';
|
|
|
|
currentValue[method](target.value);
|
|
|
|
set(data, prop, currentValue);
|
|
|
|
} else {
|
|
|
|
// deal with booleans
|
|
|
|
// but only booleans that aren't checkboxes/radios with values
|
|
|
|
if (
|
|
|
|
typeof target.checked !== 'undefined' &&
|
|
|
|
(target.value.toLowerCase() === 'on' || target.value.toLowerCase() === 'off')
|
|
|
|
) {
|
|
|
|
set(data, prop, target.checked);
|
|
|
|
} else {
|
|
|
|
// text and non-boolean checkboxes/radios
|
|
|
|
set(data, prop, target.value);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
// validate everything
|
|
|
|
return this.validate();
|
|
|
|
},
|
|
|
|
reset: function() {
|
|
|
|
const data = this.getData();
|
|
|
|
if (typeof data.rollbackAttributes === 'function') {
|
|
|
|
this.getData().rollbackAttributes();
|
|
|
|
}
|
|
|
|
return this;
|
|
|
|
},
|
2019-05-01 18:09:29 +00:00
|
|
|
clear: function(cb = {}) {
|
|
|
|
if (typeof cb === 'function') {
|
|
|
|
return (this.clearer = cb);
|
|
|
|
} else {
|
|
|
|
return this.setData(this.clearer(cb)).getData();
|
|
|
|
}
|
|
|
|
},
|
|
|
|
submit: function(cb = {}) {
|
|
|
|
if (typeof cb === 'function') {
|
|
|
|
return (this.submitter = cb);
|
|
|
|
} else {
|
|
|
|
this.submitter(this.getData());
|
|
|
|
}
|
|
|
|
},
|
2018-10-19 15:17:02 +00:00
|
|
|
setValidators: function(validators) {
|
|
|
|
_validators = validators;
|
|
|
|
return this;
|
|
|
|
},
|
|
|
|
validate: function() {
|
|
|
|
const data = this.getData();
|
|
|
|
// just pass along to the Changeset for now
|
|
|
|
if (typeof data.validate === 'function') {
|
|
|
|
data.validate();
|
|
|
|
}
|
|
|
|
return this;
|
|
|
|
},
|
|
|
|
addError: function(name, message) {
|
|
|
|
const data = this.getData();
|
|
|
|
if (typeof data.addError === 'function') {
|
|
|
|
data.addError(...arguments);
|
|
|
|
}
|
|
|
|
},
|
|
|
|
form: function(name) {
|
|
|
|
if (name == null) {
|
|
|
|
return this;
|
|
|
|
}
|
|
|
|
return _children[name];
|
|
|
|
},
|
|
|
|
has: function(name) {
|
|
|
|
return typeof _children[name] !== 'undefined';
|
|
|
|
},
|
|
|
|
};
|
2019-05-01 18:09:29 +00:00
|
|
|
form.submit = form.submit.bind(form);
|
|
|
|
form.reset = form.reset.bind(form);
|
|
|
|
return form;
|
2018-10-19 15:17:02 +00:00
|
|
|
};
|
|
|
|
}
|