1
0
mirror of synced 2024-11-22 05:16:05 +03:00

Adds full multi-option support for checkboxes and radios

This commit is contained in:
Justin Schroeder 2019-10-08 13:50:53 -04:00
parent 27bd2bda26
commit 3ad166ac90
13 changed files with 558 additions and 175 deletions

View File

@ -19,6 +19,8 @@ export default {
css: true, // Dynamically inject css as a <style> tag
compileTemplate: true // Explicitly convert template to render function
}),
buble() // Transpile to ES5
buble({
objectAssign: 'Object.assign'
}) // Transpile to ES5
]
}

165
dist/formulate.esm.js vendored
View File

@ -111,16 +111,33 @@ function map (original, callback) {
}
/**
* Function to reduce an object's properties
* @param {Object} original
* @param {Function} callback
* @param {*} accumulator
* Shallow equal.
* @param {} objA
* @param {*} objB
*/
function reduce (original, callback, accumulator) {
for (var key in original) {
accumulator = callback(accumulator, key, original[key]);
function shallowEqualObjects (objA, objB) {
if (objA === objB) {
return true
}
return accumulator
if (!objA || !objB) {
return false
}
var aKeys = Object.keys(objA);
var bKeys = Object.keys(objB);
var len = aKeys.length;
if (bKeys.length !== len) {
return false
}
for (var i = 0; i < len; i++) {
var key = aKeys[i];
if (objA[key] !== objB[key]) {
return false
}
}
return true
}
/**
@ -128,36 +145,45 @@ function reduce (original, callback, accumulator) {
* render that element.
* @return {object}
*/
function context () {
return defineModel.call(this, Object.assign({
type: this.type,
value: this.value,
classification: this.classification,
component: this.component,
id: this.id,
label: this.label,
labelPosition: labelPosition.call(this),
attributes: attributeReducer.call(this, this.$attrs)
}, typeContext.call(this)))
}
var context = {
context: function context () {
if (this.debug) {
console.log(((this.type) + " re-context"));
}
return defineModel.call(this, Object.assign({}, {type: this.type,
value: this.value,
classification: this.classification,
component: this.component,
id: this.id || this.defaultId,
label: this.label,
labelPosition: this.logicalLabelPosition,
attributes: this.elementAttributes},
this.typeContext))
},
typeContext: typeContext,
elementAttributes: elementAttributes,
logicalLabelPosition: logicalLabelPosition
};
/**
* Given (this.type), return an object to merge with the context
* @return {object}
* @return {object}
*/
function typeContext () {
var this$1 = this;
switch (this.classification) {
case 'select':
return {
options: createOptionList(this.options),
optionGroups: this.optionGroups ? map(this.optionGroups, function (k, v) { return createOptionList(v); }) : false,
options: createOptionList.call(this, this.options),
optionGroups: this.optionGroups ? map(this.optionGroups, function (k, v) { return createOptionList.call(this$1, v); }) : false,
placeholder: this.$attrs.placeholder || false
}
case 'group':
if (this.options) {
return {
options: createOptionList(this.options),
component: 'FormulateInputGroup'
options: createOptionList.call(this, this.options)
}
}
break
@ -170,20 +196,21 @@ function typeContext () {
* Reducer for attributes that will be applied to each core input element.
* @return {object}
*/
function attributeReducer (attributes) {
if ( attributes === void 0 ) attributes = {};
function elementAttributes () {
var attrs = Object.assign({}, this.localAttributes);
if (this.id) {
attributes.id = this.id;
attrs.id = this.id;
} else {
attrs.id = this.defaultId;
}
return attributes
return attrs
}
/**
* Determine the a best-guess location for the label (before or after).
* @return {string} before|after
*/
function labelPosition () {
function logicalLabelPosition () {
if (this.labelPosition) {
return this.labelPosition
}
@ -202,17 +229,22 @@ function labelPosition () {
* @return {array}
*/
function createOptionList (options) {
if (!Array.isArray(options)) {
return reduce(options, function (options, value, label) { return options.concat({ value: value, label: label, id: nanoid(15) }); }, [])
if (!Array.isArray(options) && options && typeof options === 'object') {
var optionList = [];
var that = this;
for (var value in options) {
optionList.push({ value: value, label: options[value], id: ((that.elementAttributes.id) + "_" + value) });
}
return optionList
} else if (Array.isArray(options) && !options.length) {
return [{ value: this.name, label: (this.label || this.name), id: nanoid(15) }]
return [{ value: this.value, label: (this.label || this.name), id: this.context.id || nanoid(9) }]
}
return options
}
/**
* Create a getter/setter model factory
* @return object
* Defines the model used throughout the existing context.
* @param {object} context
*/
function defineModel (context) {
return Object.defineProperty(context, 'model', {
@ -257,7 +289,7 @@ var script = {
},
formulateValue: {
type: [String, Number, Object, Boolean, Array],
default: false
default: ''
},
value: {
type: [String, Number, Object, Boolean, Array],
@ -273,7 +305,7 @@ var script = {
},
id: {
type: [String, Boolean, Number],
default: function () { return nanoid(9); }
default: false
},
label: {
type: [String, Boolean],
@ -286,16 +318,47 @@ var script = {
help: {
type: [String, Boolean],
default: false
},
debug: {
type: Boolean,
default: false
}
},
computed: {
context: context,
classification: function classification () {
data: function data () {
return {
defaultId: nanoid(9),
localAttributes: {}
}
},
computed: Object.assign({}, context,
{classification: function classification () {
var classification = this.$formulate.classify(this.type);
return (classification === 'box' && this.options) ? 'group' : classification
},
component: function component () {
return this.$formulate.component(this.type)
return (this.classification === 'group') ? 'FormulateInputGroup' : this.$formulate.component(this.type)
}}),
watch: {
'$attrs': {
handler: function handler (value) {
this.updateLocalAttributes(value);
},
deep: true
}
},
created: function created () {
this.updateLocalAttributes(this.$attrs);
},
mounted: function mounted () {
if (this.debug) {
console.log('MOUNTED:' + this.$options.name + ':' + this.type);
}
},
methods: {
updateLocalAttributes: function updateLocalAttributes (value) {
if (!shallowEqualObjects(value, this.localAttributes)) {
this.localAttributes = value;
}
}
}
};
@ -414,7 +477,7 @@ var __vue_render__ = function() {
_c("label", {
staticClass:
"formulate-input-label formulate-input-label--before",
attrs: { for: _vm.id },
attrs: { for: _vm.context.attributes.id },
domProps: { textContent: _vm._s(_vm.context.label) }
})
],
@ -442,7 +505,7 @@ var __vue_render__ = function() {
_c("label", {
staticClass:
"formulate-input-label formulate-input-label--after",
attrs: { for: _vm.id },
attrs: { for: _vm.context.attributes.id },
domProps: { textContent: _vm._s(_vm.context.label) }
})
],
@ -562,7 +625,8 @@ var script$1 = {
var options = ref.options;
var labelPosition = ref.labelPosition;
var attributes = ref.attributes;
var rest = objectWithoutProperties( ref, ["options", "labelPosition", "attributes"] );
var classification = ref.classification;
var rest = objectWithoutProperties( ref, ["options", "labelPosition", "attributes", "classification"] );
var context = rest;
return this.options.map(function (option) { return this$1.groupItemContext(context, option); })
}
@ -647,10 +711,18 @@ __vue_render__$2._withStripped = true;
* Default base for input components.
*/
var FormulateInputMixin = {
model: {
prop: 'formulateValue',
event: 'input'
},
props: {
context: {
type: Object,
required: true
},
formulateValue: {
type: [Object, Array, Boolean, String, Number],
default: ''
}
},
computed: {
@ -664,7 +736,7 @@ var FormulateInputMixin = {
return this.context.attributes || {}
},
hasValue: function hasValue () {
return !!this.context.model
return !!this.model
}
}
};
@ -1005,7 +1077,7 @@ var script$4 = {
return this.context.optionGroups || false
},
placeholderSelected: function placeholderSelected () {
return !!(!this.hasValue && this.context.attributes.placeholder)
return !!(!this.hasValue && this.context.attributes && this.context.attributes.placeholder)
}
}
};
@ -1255,6 +1327,7 @@ Formulate.prototype.install = function install (Vue, options) {
for (var componentName in this.options.components) {
Vue.component(componentName, this.options.components[componentName]);
}
Object.freeze(this);
};
/**

165
dist/formulate.min.js vendored
View File

@ -114,16 +114,33 @@ var Formulate = (function (exports, isPlainObject, nanoid) {
}
/**
* Function to reduce an object's properties
* @param {Object} original
* @param {Function} callback
* @param {*} accumulator
* Shallow equal.
* @param {} objA
* @param {*} objB
*/
function reduce (original, callback, accumulator) {
for (var key in original) {
accumulator = callback(accumulator, key, original[key]);
function shallowEqualObjects (objA, objB) {
if (objA === objB) {
return true
}
return accumulator
if (!objA || !objB) {
return false
}
var aKeys = Object.keys(objA);
var bKeys = Object.keys(objB);
var len = aKeys.length;
if (bKeys.length !== len) {
return false
}
for (var i = 0; i < len; i++) {
var key = aKeys[i];
if (objA[key] !== objB[key]) {
return false
}
}
return true
}
/**
@ -131,36 +148,45 @@ var Formulate = (function (exports, isPlainObject, nanoid) {
* render that element.
* @return {object}
*/
function context () {
return defineModel.call(this, Object.assign({
type: this.type,
value: this.value,
classification: this.classification,
component: this.component,
id: this.id,
label: this.label,
labelPosition: labelPosition.call(this),
attributes: attributeReducer.call(this, this.$attrs)
}, typeContext.call(this)))
}
var context = {
context: function context () {
if (this.debug) {
console.log(((this.type) + " re-context"));
}
return defineModel.call(this, Object.assign({}, {type: this.type,
value: this.value,
classification: this.classification,
component: this.component,
id: this.id || this.defaultId,
label: this.label,
labelPosition: this.logicalLabelPosition,
attributes: this.elementAttributes},
this.typeContext))
},
typeContext: typeContext,
elementAttributes: elementAttributes,
logicalLabelPosition: logicalLabelPosition
};
/**
* Given (this.type), return an object to merge with the context
* @return {object}
* @return {object}
*/
function typeContext () {
var this$1 = this;
switch (this.classification) {
case 'select':
return {
options: createOptionList(this.options),
optionGroups: this.optionGroups ? map(this.optionGroups, function (k, v) { return createOptionList(v); }) : false,
options: createOptionList.call(this, this.options),
optionGroups: this.optionGroups ? map(this.optionGroups, function (k, v) { return createOptionList.call(this$1, v); }) : false,
placeholder: this.$attrs.placeholder || false
}
case 'group':
if (this.options) {
return {
options: createOptionList(this.options),
component: 'FormulateInputGroup'
options: createOptionList.call(this, this.options)
}
}
break
@ -173,20 +199,21 @@ var Formulate = (function (exports, isPlainObject, nanoid) {
* Reducer for attributes that will be applied to each core input element.
* @return {object}
*/
function attributeReducer (attributes) {
if ( attributes === void 0 ) attributes = {};
function elementAttributes () {
var attrs = Object.assign({}, this.localAttributes);
if (this.id) {
attributes.id = this.id;
attrs.id = this.id;
} else {
attrs.id = this.defaultId;
}
return attributes
return attrs
}
/**
* Determine the a best-guess location for the label (before or after).
* @return {string} before|after
*/
function labelPosition () {
function logicalLabelPosition () {
if (this.labelPosition) {
return this.labelPosition
}
@ -205,17 +232,22 @@ var Formulate = (function (exports, isPlainObject, nanoid) {
* @return {array}
*/
function createOptionList (options) {
if (!Array.isArray(options)) {
return reduce(options, function (options, value, label) { return options.concat({ value: value, label: label, id: nanoid(15) }); }, [])
if (!Array.isArray(options) && options && typeof options === 'object') {
var optionList = [];
var that = this;
for (var value in options) {
optionList.push({ value: value, label: options[value], id: ((that.elementAttributes.id) + "_" + value) });
}
return optionList
} else if (Array.isArray(options) && !options.length) {
return [{ value: this.name, label: (this.label || this.name), id: nanoid(15) }]
return [{ value: this.value, label: (this.label || this.name), id: this.context.id || nanoid(9) }]
}
return options
}
/**
* Create a getter/setter model factory
* @return object
* Defines the model used throughout the existing context.
* @param {object} context
*/
function defineModel (context) {
return Object.defineProperty(context, 'model', {
@ -260,7 +292,7 @@ var Formulate = (function (exports, isPlainObject, nanoid) {
},
formulateValue: {
type: [String, Number, Object, Boolean, Array],
default: false
default: ''
},
value: {
type: [String, Number, Object, Boolean, Array],
@ -276,7 +308,7 @@ var Formulate = (function (exports, isPlainObject, nanoid) {
},
id: {
type: [String, Boolean, Number],
default: function () { return nanoid(9); }
default: false
},
label: {
type: [String, Boolean],
@ -289,16 +321,47 @@ var Formulate = (function (exports, isPlainObject, nanoid) {
help: {
type: [String, Boolean],
default: false
},
debug: {
type: Boolean,
default: false
}
},
computed: {
context: context,
classification: function classification () {
data: function data () {
return {
defaultId: nanoid(9),
localAttributes: {}
}
},
computed: Object.assign({}, context,
{classification: function classification () {
var classification = this.$formulate.classify(this.type);
return (classification === 'box' && this.options) ? 'group' : classification
},
component: function component () {
return this.$formulate.component(this.type)
return (this.classification === 'group') ? 'FormulateInputGroup' : this.$formulate.component(this.type)
}}),
watch: {
'$attrs': {
handler: function handler (value) {
this.updateLocalAttributes(value);
},
deep: true
}
},
created: function created () {
this.updateLocalAttributes(this.$attrs);
},
mounted: function mounted () {
if (this.debug) {
console.log('MOUNTED:' + this.$options.name + ':' + this.type);
}
},
methods: {
updateLocalAttributes: function updateLocalAttributes (value) {
if (!shallowEqualObjects(value, this.localAttributes)) {
this.localAttributes = value;
}
}
}
};
@ -417,7 +480,7 @@ var Formulate = (function (exports, isPlainObject, nanoid) {
_c("label", {
staticClass:
"formulate-input-label formulate-input-label--before",
attrs: { for: _vm.id },
attrs: { for: _vm.context.attributes.id },
domProps: { textContent: _vm._s(_vm.context.label) }
})
],
@ -445,7 +508,7 @@ var Formulate = (function (exports, isPlainObject, nanoid) {
_c("label", {
staticClass:
"formulate-input-label formulate-input-label--after",
attrs: { for: _vm.id },
attrs: { for: _vm.context.attributes.id },
domProps: { textContent: _vm._s(_vm.context.label) }
})
],
@ -565,7 +628,8 @@ var Formulate = (function (exports, isPlainObject, nanoid) {
var options = ref.options;
var labelPosition = ref.labelPosition;
var attributes = ref.attributes;
var rest = objectWithoutProperties( ref, ["options", "labelPosition", "attributes"] );
var classification = ref.classification;
var rest = objectWithoutProperties( ref, ["options", "labelPosition", "attributes", "classification"] );
var context = rest;
return this.options.map(function (option) { return this$1.groupItemContext(context, option); })
}
@ -650,10 +714,18 @@ var Formulate = (function (exports, isPlainObject, nanoid) {
* Default base for input components.
*/
var FormulateInputMixin = {
model: {
prop: 'formulateValue',
event: 'input'
},
props: {
context: {
type: Object,
required: true
},
formulateValue: {
type: [Object, Array, Boolean, String, Number],
default: ''
}
},
computed: {
@ -667,7 +739,7 @@ var Formulate = (function (exports, isPlainObject, nanoid) {
return this.context.attributes || {}
},
hasValue: function hasValue () {
return !!this.context.model
return !!this.model
}
}
};
@ -1008,7 +1080,7 @@ var Formulate = (function (exports, isPlainObject, nanoid) {
return this.context.optionGroups || false
},
placeholderSelected: function placeholderSelected () {
return !!(!this.hasValue && this.context.attributes.placeholder)
return !!(!this.hasValue && this.context.attributes && this.context.attributes.placeholder)
}
}
};
@ -1258,6 +1330,7 @@ var Formulate = (function (exports, isPlainObject, nanoid) {
for (var componentName in this.options.components) {
Vue.component(componentName, this.options.components[componentName]);
}
Object.freeze(this);
};
/**

165
dist/formulate.umd.js vendored
View File

@ -117,16 +117,33 @@
}
/**
* Function to reduce an object's properties
* @param {Object} original
* @param {Function} callback
* @param {*} accumulator
* Shallow equal.
* @param {} objA
* @param {*} objB
*/
function reduce (original, callback, accumulator) {
for (var key in original) {
accumulator = callback(accumulator, key, original[key]);
function shallowEqualObjects (objA, objB) {
if (objA === objB) {
return true
}
return accumulator
if (!objA || !objB) {
return false
}
var aKeys = Object.keys(objA);
var bKeys = Object.keys(objB);
var len = aKeys.length;
if (bKeys.length !== len) {
return false
}
for (var i = 0; i < len; i++) {
var key = aKeys[i];
if (objA[key] !== objB[key]) {
return false
}
}
return true
}
/**
@ -134,36 +151,45 @@
* render that element.
* @return {object}
*/
function context () {
return defineModel.call(this, Object.assign({
type: this.type,
value: this.value,
classification: this.classification,
component: this.component,
id: this.id,
label: this.label,
labelPosition: labelPosition.call(this),
attributes: attributeReducer.call(this, this.$attrs)
}, typeContext.call(this)))
}
var context = {
context: function context () {
if (this.debug) {
console.log(((this.type) + " re-context"));
}
return defineModel.call(this, Object.assign({}, {type: this.type,
value: this.value,
classification: this.classification,
component: this.component,
id: this.id || this.defaultId,
label: this.label,
labelPosition: this.logicalLabelPosition,
attributes: this.elementAttributes},
this.typeContext))
},
typeContext: typeContext,
elementAttributes: elementAttributes,
logicalLabelPosition: logicalLabelPosition
};
/**
* Given (this.type), return an object to merge with the context
* @return {object}
* @return {object}
*/
function typeContext () {
var this$1 = this;
switch (this.classification) {
case 'select':
return {
options: createOptionList(this.options),
optionGroups: this.optionGroups ? map(this.optionGroups, function (k, v) { return createOptionList(v); }) : false,
options: createOptionList.call(this, this.options),
optionGroups: this.optionGroups ? map(this.optionGroups, function (k, v) { return createOptionList.call(this$1, v); }) : false,
placeholder: this.$attrs.placeholder || false
}
case 'group':
if (this.options) {
return {
options: createOptionList(this.options),
component: 'FormulateInputGroup'
options: createOptionList.call(this, this.options)
}
}
break
@ -176,20 +202,21 @@
* Reducer for attributes that will be applied to each core input element.
* @return {object}
*/
function attributeReducer (attributes) {
if ( attributes === void 0 ) attributes = {};
function elementAttributes () {
var attrs = Object.assign({}, this.localAttributes);
if (this.id) {
attributes.id = this.id;
attrs.id = this.id;
} else {
attrs.id = this.defaultId;
}
return attributes
return attrs
}
/**
* Determine the a best-guess location for the label (before or after).
* @return {string} before|after
*/
function labelPosition () {
function logicalLabelPosition () {
if (this.labelPosition) {
return this.labelPosition
}
@ -208,17 +235,22 @@
* @return {array}
*/
function createOptionList (options) {
if (!Array.isArray(options)) {
return reduce(options, function (options, value, label) { return options.concat({ value: value, label: label, id: nanoid(15) }); }, [])
if (!Array.isArray(options) && options && typeof options === 'object') {
var optionList = [];
var that = this;
for (var value in options) {
optionList.push({ value: value, label: options[value], id: ((that.elementAttributes.id) + "_" + value) });
}
return optionList
} else if (Array.isArray(options) && !options.length) {
return [{ value: this.name, label: (this.label || this.name), id: nanoid(15) }]
return [{ value: this.value, label: (this.label || this.name), id: this.context.id || nanoid(9) }]
}
return options
}
/**
* Create a getter/setter model factory
* @return object
* Defines the model used throughout the existing context.
* @param {object} context
*/
function defineModel (context) {
return Object.defineProperty(context, 'model', {
@ -263,7 +295,7 @@
},
formulateValue: {
type: [String, Number, Object, Boolean, Array],
default: false
default: ''
},
value: {
type: [String, Number, Object, Boolean, Array],
@ -279,7 +311,7 @@
},
id: {
type: [String, Boolean, Number],
default: function () { return nanoid(9); }
default: false
},
label: {
type: [String, Boolean],
@ -292,16 +324,47 @@
help: {
type: [String, Boolean],
default: false
},
debug: {
type: Boolean,
default: false
}
},
computed: {
context: context,
classification: function classification () {
data: function data () {
return {
defaultId: nanoid(9),
localAttributes: {}
}
},
computed: Object.assign({}, context,
{classification: function classification () {
var classification = this.$formulate.classify(this.type);
return (classification === 'box' && this.options) ? 'group' : classification
},
component: function component () {
return this.$formulate.component(this.type)
return (this.classification === 'group') ? 'FormulateInputGroup' : this.$formulate.component(this.type)
}}),
watch: {
'$attrs': {
handler: function handler (value) {
this.updateLocalAttributes(value);
},
deep: true
}
},
created: function created () {
this.updateLocalAttributes(this.$attrs);
},
mounted: function mounted () {
if (this.debug) {
console.log('MOUNTED:' + this.$options.name + ':' + this.type);
}
},
methods: {
updateLocalAttributes: function updateLocalAttributes (value) {
if (!shallowEqualObjects(value, this.localAttributes)) {
this.localAttributes = value;
}
}
}
};
@ -420,7 +483,7 @@
_c("label", {
staticClass:
"formulate-input-label formulate-input-label--before",
attrs: { for: _vm.id },
attrs: { for: _vm.context.attributes.id },
domProps: { textContent: _vm._s(_vm.context.label) }
})
],
@ -448,7 +511,7 @@
_c("label", {
staticClass:
"formulate-input-label formulate-input-label--after",
attrs: { for: _vm.id },
attrs: { for: _vm.context.attributes.id },
domProps: { textContent: _vm._s(_vm.context.label) }
})
],
@ -568,7 +631,8 @@
var options = ref.options;
var labelPosition = ref.labelPosition;
var attributes = ref.attributes;
var rest = objectWithoutProperties( ref, ["options", "labelPosition", "attributes"] );
var classification = ref.classification;
var rest = objectWithoutProperties( ref, ["options", "labelPosition", "attributes", "classification"] );
var context = rest;
return this.options.map(function (option) { return this$1.groupItemContext(context, option); })
}
@ -653,10 +717,18 @@
* Default base for input components.
*/
var FormulateInputMixin = {
model: {
prop: 'formulateValue',
event: 'input'
},
props: {
context: {
type: Object,
required: true
},
formulateValue: {
type: [Object, Array, Boolean, String, Number],
default: ''
}
},
computed: {
@ -670,7 +742,7 @@
return this.context.attributes || {}
},
hasValue: function hasValue () {
return !!this.context.model
return !!this.model
}
}
};
@ -1011,7 +1083,7 @@
return this.context.optionGroups || false
},
placeholderSelected: function placeholderSelected () {
return !!(!this.hasValue && this.context.attributes.placeholder)
return !!(!this.hasValue && this.context.attributes && this.context.attributes.placeholder)
}
}
};
@ -1261,6 +1333,7 @@
for (var componentName in this.options.components) {
Vue.component(componentName, this.options.components[componentName]);
}
Object.freeze(this);
};
/**

View File

@ -12,7 +12,7 @@
>
<label
class="formulate-input-label formulate-input-label--before"
:for="id"
:for="context.attributes.id"
v-text="context.label"
/>
</slot>
@ -29,7 +29,7 @@
>
<label
class="formulate-input-label formulate-input-label--after"
:for="id"
:for="context.attributes.id"
v-text="context.label"
/>
</slot>
@ -44,7 +44,9 @@
<script>
import context from './libs/context'
import { shallowEqualObjects } from './libs/utils'
import nanoid from 'nanoid'
import library from './libs/library'
export default {
name: 'FormulateInput',
@ -60,7 +62,7 @@ export default {
},
formulateValue: {
type: [String, Number, Object, Boolean, Array],
default: false
default: ''
},
value: {
type: [String, Number, Object, Boolean, Array],
@ -76,7 +78,7 @@ export default {
},
id: {
type: [String, Boolean, Number],
default: () => nanoid(9)
default: false
},
label: {
type: [String, Boolean],
@ -89,16 +91,49 @@ export default {
help: {
type: [String, Boolean],
default: false
},
debug: {
type: Boolean,
default: false
}
},
data () {
return {
defaultId: nanoid(9),
localAttributes: {}
}
},
computed: {
context,
...context,
classification () {
const classification = this.$formulate.classify(this.type)
return (classification === 'box' && this.options) ? 'group' : classification
},
component () {
return this.$formulate.component(this.type)
return (this.classification === 'group') ? 'FormulateInputGroup' : this.$formulate.component(this.type)
}
},
watch: {
'$attrs': {
handler (value) {
this.updateLocalAttributes(value)
},
deep: true
}
},
created () {
this.updateLocalAttributes(this.$attrs)
},
mounted () {
if (this.debug) {
console.log('MOUNTED:' + this.$options.name + ':' + this.type)
}
},
methods: {
updateLocalAttributes (value) {
if (!shallowEqualObjects(value, this.localAttributes)) {
this.localAttributes = value
}
}
}
}

View File

@ -25,7 +25,7 @@ export default {
return this.context.options || []
},
optionsWithContext () {
const { options, labelPosition, attributes, ...context } = this.context
const { options, labelPosition, attributes, classification, ...context } = this.context
return this.options.map(option => this.groupItemContext(context, option))
}
},

View File

@ -2,10 +2,18 @@
* Default base for input components.
*/
export default {
model: {
prop: 'formulateValue',
event: 'input'
},
props: {
context: {
type: Object,
required: true
},
formulateValue: {
type: [Object, Array, Boolean, String, Number],
default: ''
}
},
computed: {
@ -19,7 +27,7 @@ export default {
return this.context.attributes || {}
},
hasValue () {
return !!this.context.model
return !!this.model
}
}
}

View File

@ -38,6 +38,7 @@ class Formulate {
for (var componentName in this.options.components) {
Vue.component(componentName, this.options.components[componentName])
}
Object.freeze(this)
}
/**

View File

@ -61,7 +61,7 @@ export default {
return this.context.optionGroups || false
},
placeholderSelected () {
return !!(!this.hasValue && this.context.attributes.placeholder)
return !!(!this.hasValue && this.context.attributes && this.context.attributes.placeholder)
}
}
}

View File

@ -6,36 +6,45 @@ import { reduce, map } from './utils'
* render that element.
* @return {object}
*/
export default function context () {
return defineModel.call(this, Object.assign({
type: this.type,
value: this.value,
classification: this.classification,
component: this.component,
id: this.id,
label: this.label,
labelPosition: labelPosition.call(this),
attributes: attributeReducer.call(this, this.$attrs)
}, typeContext.call(this)))
export default {
context () {
if (this.debug) {
console.log(`${this.type} re-context`)
}
return defineModel.call(this, {
type: this.type,
value: this.value,
classification: this.classification,
component: this.component,
id: this.id || this.defaultId,
label: this.label,
labelPosition: this.logicalLabelPosition,
attributes: this.elementAttributes,
...this.typeContext
})
},
typeContext,
elementAttributes,
logicalLabelPosition
}
/**
* Given (this.type), return an object to merge with the context
* @return {object}
* @return {object}
*/
function typeContext () {
switch (this.classification) {
case 'select':
return {
options: createOptionList(this.options),
optionGroups: this.optionGroups ? map(this.optionGroups, (k, v) => createOptionList(v)) : false,
options: createOptionList.call(this, this.options),
optionGroups: this.optionGroups ? map(this.optionGroups, (k, v) => createOptionList.call(this, v)) : false,
placeholder: this.$attrs.placeholder || false
}
case 'group':
if (this.options) {
return {
options: createOptionList(this.options),
component: 'FormulateInputGroup'
options: createOptionList.call(this, this.options)
}
}
break
@ -48,18 +57,21 @@ function typeContext () {
* Reducer for attributes that will be applied to each core input element.
* @return {object}
*/
function attributeReducer (attributes = {}) {
function elementAttributes () {
const attrs = Object.assign({}, this.localAttributes)
if (this.id) {
attributes.id = this.id
attrs.id = this.id
} else {
attrs.id = this.defaultId
}
return attributes
return attrs
}
/**
* Determine the a best-guess location for the label (before or after).
* @return {string} before|after
*/
function labelPosition () {
function logicalLabelPosition () {
if (this.labelPosition) {
return this.labelPosition
}
@ -78,17 +90,22 @@ function labelPosition () {
* @return {array}
*/
function createOptionList (options) {
if (!Array.isArray(options)) {
return reduce(options, (options, value, label) => options.concat({ value, label, id: nanoid(15) }), [])
if (!Array.isArray(options) && options && typeof options === 'object') {
const optionList = []
const that = this
for (const value in options) {
optionList.push({ value, label: options[value], id: `${that.elementAttributes.id}_${value}` })
}
return optionList
} else if (Array.isArray(options) && !options.length) {
return [{ value: this.name, label: (this.label || this.name), id: nanoid(15) }]
return [{ value: this.value, label: (this.label || this.name), id: this.context.id || nanoid(9) }]
}
return options
}
/**
* Create a getter/setter model factory
* @return object
* Defines the model used throughout the existing context.
* @param {object} context
*/
function defineModel (context) {
return Object.defineProperty(context, 'model', {

View File

@ -39,3 +39,33 @@ export function reduce (original, callback, accumulator) {
}
return accumulator
}
/**
* Shallow equal.
* @param {} objA
* @param {*} objB
*/
export function shallowEqualObjects (objA, objB) {
if (objA === objB) {
return true
}
if (!objA || !objB) {
return false
}
var aKeys = Object.keys(objA)
var bKeys = Object.keys(objB)
var len = aKeys.length
if (bKeys.length !== len) {
return false
}
for (var i = 0; i < len; i++) {
var key = aKeys[i]
if (objA[key] !== objB[key]) {
return false
}
}
return true
}

View File

@ -42,3 +42,37 @@ test('labelPosition of type "checkbox" with options defaults to before', () => {
const wrapper = mount(FormulateInput, { propsData: { type: 'checkbox', options: {a: '1', b: '2'}}})
expect(wrapper.vm.context.labelPosition).toBe('before')
})
test('type radio renders multiple inputs with options', () => {
const wrapper = mount(FormulateInput, { propsData: { type: 'radio', options: {a: '1', b: '2'} } })
expect(wrapper.findAll('input[type="radio"]').length).toBe(2)
})
test('type "radio" auto generate ids if not provided', () => {
const wrapper = mount(FormulateInput, { propsData: { type: 'radio', options: {a: '1', b: '2'} } })
expect(wrapper.findAll('input[type="radio"]').is('[id]')).toBe(true)
})
/**
* Test data binding
*/
test('type "checkbox" sets array of values via v-model', () => {
const wrapper = mount({
data () {
return {
checkboxValues: []
}
},
template: `
<div>
<FormulateInput type="checkbox" v-model="checkboxValues" :options="{foo: 'Foo', bar: 'Bar', fooey: 'Fooey'}" />
</div>
`
})
const fooInputs = wrapper.findAll('input[value^="foo"]')
expect(fooInputs.length).toBe(2)
fooInputs.setChecked(true)
expect(wrapper.vm.checkboxValues).toEqual(['foo', 'fooey'])
})

View File

@ -3,6 +3,7 @@ import { mount } from '@vue/test-utils'
import Formulate from '../dist/formulate.esm.js'
import FormulateInput from '../src/FormulateInput.vue'
import FormulateInputText from '../src/inputs/FormulateInputText.vue'
import { doesNotReject } from 'assert';
Vue.use(Formulate)
@ -76,13 +77,13 @@ test('type "week" renders a text input', () => {
})
/**
* Test functionality to text inputs
* Test rendering functionality to text inputs
*/
test('text inputs automatically have id assigned', () => {
const wrapper = mount(FormulateInput, { propsData: { type: 'text' } })
expect(wrapper.vm.context).toHaveProperty('id')
expect(wrapper.find(`input[id="${wrapper.vm.id}"]`).exists()).toBe(true)
expect(wrapper.find(`input[id="${wrapper.vm.context.attributes.id}"]`).exists()).toBe(true)
})
test('text inputs dont have labels', () => {
@ -92,7 +93,7 @@ test('text inputs dont have labels', () => {
test('text inputs can have labels', () => {
const wrapper = mount(FormulateInput, { propsData: { type: 'text', label: 'Field label' } })
expect(wrapper.find(`label[for="${wrapper.vm.id}"]`).exists()).toBe(true)
expect(wrapper.find(`label[for="${wrapper.vm.context.attributes.id}"]`).exists()).toBe(true)
})
test('text inputs dont have help text', () => {
@ -104,3 +105,39 @@ test('text inputs dont have help text', () => {
const wrapper = mount(FormulateInput, { propsData: { type: 'text', help: 'This is some help text' } })
expect(wrapper.find(`.formulate-input-help`).exists()).toBe(true)
})
/**
* Test data binding
*/
test('text inputs emit input (vmodel) event with value when edited', () => {
const wrapper = mount(FormulateInput, { propsData: { type: 'text' } })
wrapper.find('input').setValue('Updated Value')
expect(wrapper.emitted().input).toBeTruthy()
expect(wrapper.emitted().input[0]).toEqual(['Updated Value'])
})
test('test that inputs that arent updated dont re-context themselves', () => {
const wrapper = mount({
data () {
return {
valueA: 'first value',
valueB: 'second value'
}
},
template: `
<div>
<FormulateInput type="text" ref="first" v-model="valueA" :placeholder="valueA" />
<FormulateInput type="text" ref="second" v-model="valueB" :placeholder="valueB" />
</div>
`
})
const firstContext = wrapper.find({ref: "first"}).vm.context
const secondContext = wrapper.find({ref: "second"}).vm.context
wrapper.find('input').setValue('new value')
expect(firstContext).toBeTruthy()
expect(wrapper.vm.valueA === 'new value').toBe(true)
expect(wrapper.vm.valueB === 'second value').toBe(true)
expect(wrapper.find({ref: "first"}).vm.context === firstContext).toBe(false)
expect(wrapper.find({ref: "second"}).vm.context === secondContext).toBe(true)
})