diff --git a/Gruntfile.js b/Gruntfile.js
index 87d78e9e..70def36a 100644
--- a/Gruntfile.js
+++ b/Gruntfile.js
@@ -103,9 +103,9 @@ module.exports = function (grunt) {
'dist/js/babel/util.js': 'js/src/util.js',
'dist/js/babel/ripples.js': 'js/src/ripples.js',
'dist/js/babel/autofill.js': 'js/src/autofill.js',
- 'dist/js/babel/input.js': 'js/src/input.js',
+ 'dist/js/babel/input.js': 'js/src/textInput.js',
'dist/js/babel/checkbox.js': 'js/src/checkbox.js',
- 'dist/js/babel/togglebutton.js': 'js/src/togglebutton.js',
+ 'dist/js/babel/togglebutton.js': 'js/src/switch.js',
'dist/js/babel/radio.js': 'js/src/radio.js',
'dist/js/babel/fileInput.js': 'js/src/fileInput.js',
'dist/js/babel/bootstrapMaterialDesign.js': 'js/src/bootstrapMaterialDesign.js',
@@ -124,9 +124,9 @@ module.exports = function (grunt) {
'docs/dist/js/babel/util.js': 'js/src/util.js',
'docs/dist/js/babel/ripples.js': 'js/src/ripples.js',
'docs/dist/js/babel/autofill.js': 'js/src/autofill.js',
- 'docs/dist/js/babel/input.js': 'js/src/input.js',
+ 'docs/dist/js/babel/input.js': 'js/src/textInput.js',
'docs/dist/js/babel/checkbox.js': 'js/src/checkbox.js',
- 'docs/dist/js/babel/togglebutton.js': 'js/src/togglebutton.js',
+ 'docs/dist/js/babel/togglebutton.js': 'js/src/switch.js',
'docs/dist/js/babel/radio.js': 'js/src/radio.js',
'docs/dist/js/babel/fileInput.js': 'js/src/fileInput.js',
'docs/dist/js/babel/bootstrapMaterialDesign.js': 'js/src/bootstrapMaterialDesign.js',
@@ -148,9 +148,9 @@ module.exports = function (grunt) {
'dist/js/umd/util.js': 'js/src/util.js',
'dist/js/umd/ripples.js': 'js/src/ripples.js',
'dist/js/umd/autofill.js': 'js/src/autofill.js',
- 'dist/js/umd/input.js': 'js/src/input.js',
+ 'dist/js/umd/input.js': 'js/src/textInput.js',
'dist/js/umd/checkbox.js': 'js/src/checkbox.js',
- 'dist/js/umd/togglebutton.js': 'js/src/togglebutton.js',
+ 'dist/js/umd/togglebutton.js': 'js/src/switch.js',
'dist/js/umd/radio.js': 'js/src/radio.js',
'dist/js/umd/fileInput.js': 'js/src/fileInput.js',
'dist/js/umd/bootstrapMaterialDesign.js': 'js/src/bootstrapMaterialDesign.js',
@@ -208,9 +208,9 @@ module.exports = function (grunt) {
'dist/js/babel/util.js',
'dist/js/babel/ripples.js',
'dist/js/babel/autofill.js',
- 'dist/js/babel/input.js',
+ 'dist/js/babel/textInput.js',
'dist/js/babel/checkbox.js',
- 'dist/js/babel/togglebutton.js',
+ 'dist/js/babel/switch.js',
'dist/js/babel/radio.js',
'dist/js/babel/fileInput.js',
'dist/js/babel/bootstrapMaterialDesign.js',
diff --git a/grunt/configBridge.json b/grunt/configBridge.json
index b63fd5ac..2f0bdd94 100644
--- a/grunt/configBridge.json
+++ b/grunt/configBridge.json
@@ -9,6 +9,7 @@
],
"coreJs": [
+ "../dist/js/babel/baseInput.js",
"../dist/js/babel/autofill.js",
"../dist/js/babel/bootstrapMaterialDesign.js",
"../dist/js/babel/checkbox.js",
diff --git a/js/src/baseInput.js b/js/src/baseInput.js
new file mode 100644
index 00000000..c8d61294
--- /dev/null
+++ b/js/src/baseInput.js
@@ -0,0 +1,164 @@
+import Util from './util'
+
+const BaseInput = (($) => {
+
+ const Default = {
+ formGroup: {
+ template: `
`,
+ required: true,
+ autoCreate: false
+ },
+ requiredClasses: ['form-control'],
+ invalidComponentMatches: []
+ }
+
+ const ClassName = {
+ FORM_GROUP: 'form-group',
+ HAS_ERROR: 'has-error',
+ IS_EMPTY: 'is-empty'
+ }
+
+ const Selector = {
+ FORM_GROUP: `.${ClassName.FORM_GROUP}` //,
+ }
+
+ /**
+ * ------------------------------------------------------------------------
+ * Class Definition
+ * ------------------------------------------------------------------------
+ */
+ class BaseInput {
+
+ constructor(element, defaultConfig, config) {
+ this.$element = $(element)
+ this.config = $.extend({}, Default, defaultConfig, config)
+
+ // Enforce no overlap between components to prevent side effects
+ this._rejectInvalidComponentMatches()
+
+ // Enforce required classes for a consistent rendering
+ this._rejectWithoutRequiredClasses()
+
+ // Enforce expected structure (if any)
+ this.rejectWithoutRequiredStructure()
+
+ if(this.config.formGroup.autoCreate) {
+ // Will create form-group if necessary
+ this.autoCreateFormGroup()
+ }
+
+ // different components have different rules, always run this separately
+ this.$formGroup = this.findFormGroup(this.config.formGroup.required)
+
+ this.addFocusListener()
+ this.addChangeListener()
+ }
+
+ dispose(dataKey) {
+ $.removeData(this.$element, dataKey)
+ this.$element = null
+ this.$formGroup = null
+ this.config = null
+ }
+
+ // ------------------------------------------------------------------------
+ // protected
+
+ rejectWithoutRequiredStructure(){
+ // implement
+ }
+
+ addFocusListener() {
+ // implement
+ }
+
+ addChangeListener() {
+ // implement
+ }
+
+ addFormGroupFocus(formGroup) {
+ formGroup.addClass(ClassName.IS_FOCUSED)
+ }
+
+ removeFormGroupFocus(formGroup) {
+ formGroup.removeClass(ClassName.IS_FOCUSED)
+ }
+
+ addHasError() {
+ this.$formGroup.addClass(ClassName.HAS_ERROR)
+ }
+
+ removeHasError() {
+ this.$formGroup.removeClass(ClassName.HAS_ERROR)
+ }
+
+ addIsEmpty() {
+ this.$formGroup.addClass(ClassName.IS_EMPTY)
+ }
+
+ removeIsEmpty() {
+ this.$formGroup.removeClass(ClassName.IS_EMPTY)
+ }
+
+ isEmpty() {
+ return (this.$element.val() === null || this.$element.val() === undefined || this.$element.val() === '')
+ }
+
+ // Find or create a form-group if necessary
+ autoCreateFormGroup() {
+ let fg = this.findFormGroup(false)
+ if (fg === null || fg.length === 0) {
+ this.outerElement().wrap(this.config.formGroup.template)
+ }
+ }
+
+ // Demarcation element (e.g. first child of a form-group)
+ // Subclasses such as file inputs may have different structures
+ outerElement(){
+ return this.$element
+ }
+
+ // Find expected form-group
+ findFormGroup(raiseError = true) {
+ let fg = this.$element.closest(Selector.FORM_GROUP) // note that form-group may be grandparent in the case of an input-group
+ if (fg.length === 0 && raiseError) {
+ $.error(`Failed to find form-group for ${$element}`)
+ }
+ return fg
+ }
+
+ findOrCreateFormGroup() {
+ let fg = this.$element.closest(Selector.FORM_GROUP) // note that form-group may be grandparent in the case of an baseInput-group
+ if (fg === null || fg.length === 0) {
+ this.$element.wrap(this.config.formGroup.template)
+ fg = this.$element.closest(Selector.FORM_GROUP) // find node after attached (otherwise additional attachments don't work)
+ }
+ return fg
+ }
+
+ // ------------------------------------------------------------------------
+ // private
+ _rejectInvalidComponentMatches(){
+ for(let otherComponent in this.config.invalidComponentMatches){
+ otherComponent.rejectMatch(this.constructor.name, $element)
+ }
+ }
+
+ _rejectWithoutRequiredClasses(){
+ for(let requiredClass in this.config.requiredClasses){
+ if(!$element.hasClass(requiredClass)){
+ $.error(`${this.constructor.name} elements require class: ${requiredClass}`)
+ }
+ }
+ }
+
+ // ------------------------------------------------------------------------
+ // static
+
+ }
+
+ return BaseInput
+
+})(jQuery)
+
+export default BaseInput
diff --git a/js/src/bootstrapMaterialDesign.js b/js/src/bootstrapMaterialDesign.js
index 21925d6c..231c5ecc 100644
--- a/js/src/bootstrapMaterialDesign.js
+++ b/js/src/bootstrapMaterialDesign.js
@@ -8,7 +8,7 @@
*
* NOTE: If omitting use of this class, please note that the Input component must be
* initialized prior to other decorating components such as radio, checkbox,
- * togglebutton, fileInput.
+ * switch, fileInput.
*
*/
const BootstrapMaterialDesign = (($) => {
@@ -42,18 +42,18 @@ const BootstrapMaterialDesign = (($) => {
'.ripple' // generic marker class to add ripple to elements
]
},
- input: {
+ textInput: {
selector: [
- 'input.form-control',
- 'textarea.form-control',
- 'select.form-control'
+ 'input[type=text]',
+ 'textarea',
+ 'select'
]
},
checkbox: {
selector: '.checkbox > label > input[type=checkbox]'
},
- togglebutton: {
- selector: '.togglebutton > label > input[type=checkbox]'
+ switch: {
+ selector: '.switch > label > input[type=checkbox]'
},
radio: {
selector: '.radio > label > input[type=radio]'
@@ -68,9 +68,9 @@ const BootstrapMaterialDesign = (($) => {
// create an ordered component list for instantiation
instantiation: [
'ripples',
- 'input',
+ 'textInput',
'checkbox',
- 'togglebutton',
+ 'switch',
'radio',
'fileInput',
'autofill'
diff --git a/js/src/checkbox.js b/js/src/checkbox.js
index 647a06e0..583c3156 100644
--- a/js/src/checkbox.js
+++ b/js/src/checkbox.js
@@ -1,6 +1,10 @@
+import BaseInput from './baseInput'
+import TextInput from './textInput'
+import FileInput from './fileInput'
+import Radio from './radio'
+import Switch from './switch'
import Util from './util'
-// Checkbox decorator, to be called after Input
const Checkbox = (($) => {
/**
@@ -13,7 +17,11 @@ const Checkbox = (($) => {
const JQUERY_NO_CONFLICT = $.fn[NAME]
const Default = {
- template: ``
+ template: ``,
+ formGroup: {
+ autoCreate: true
+ },
+ invalidComponentMatches: [TextInput, FileInput, Radio, Switch]
}
/**
@@ -21,40 +29,66 @@ const Checkbox = (($) => {
* Class Definition
* ------------------------------------------------------------------------
*/
- class Checkbox {
+ class Checkbox extends BaseInput {
constructor(element, config) {
- this.$element = $(element)
- this.config = $.extend({}, Default, config)
-
+ super(element, Default, config)
this.$element.after(this.config.template)
- this.$formGroup = Util.findFormGroup(this.$element, false)
-
- this._bindEventListeners()
}
dispose() {
- $.removeData(this.$element, DATA_KEY)
- this.$element = null
- this.$formGroup = null
- this.config = null
+ super.dispose(DATA_KEY)
+ }
+
+ static matches($element) {
+ // '.checkbox > label > input[type=checkbox]'
+ if ($element.attr('type') === 'checkbox') {
+ return true
+ }
+ return false
+ }
+
+ static rejectMatch(component, $element) {
+ Util.assert(this.matches($element), `${component} component is invalid for type='checkbox'.`)
}
// ------------------------------------------------------------------------
- // private
- _bindEventListeners() {
+ // protected
+
+ // Demarcation element (e.g. first child of a form-group)
+ // Subclasses such as file inputs may have different structures
+ outerElement() {
+ // '.checkbox > label > input[type=checkbox]'
+ return this.$element.parent().parent()
+ }
+
+ rejectWithoutRequiredStructure() {
+ // '.checkbox > label > input[type=checkbox]'
+ Util.assert(this.$element.parent().prop('tagName') === 'label', `${component} parent element should be