As part of my effort to learn JavaScript, I have started developing a small library for creating DOM widgets like modals, tooltips and so on from scratch (nothing but plain vanilla JS allowed in other words); much like jQuery UI. So far, I have only developed one widget, namely a modal, but the general structure of the library is there: a collection classes and mixins for keeping the code dry.
As somebody who is very much in the process of learning, I would appreciate general feedback on the code, but more specifically I am interested getting your views on the following points:
- Is the pattern of classes and mixins a good one for writing scaleable code?
- What is the best way to deal with user defined parameters and default values?
- Defensive code. When to throw errors, what to test for and so on. I want debugging the code to be easy for somebody that is not familliar with it.
- Instinctively, I feel that the Effect mixin is a royal repetitive mess. Any suggestions on how I could improve it? And in regard to the slide-down method, what is the best way to get the height of a hidden DOM element in JavaScript?
A working version of the code below can be found here: https://jsfiddle.net/9sewteLb/1/
var Koalified = {};
if (typeof define === 'function' && define.amd) {
define('koalified', Koalified);
} else if ('undefined' !== typeof exports && 'undefined' !== typeof module) {
module.exports = Koalified;
}
Koalified.Effect = function() {
this.show = function(el) {
el.style.display = 'block';
};
this.hide = function(el) {
el.style.display = 'none';
};
this.slideDown = function(el) {
el.style.display = 'block';
el.style.height = 'auto';
function getHeight() {
var height = el.clientHeight;
el.style.display = 'none';
return height;
}
var elHeight = getHeight();
var height = 0;
el.style.overflow = 'hidden';
el.style.display = 'block';
(function incrementHeight() {
el.style.height = height + 'px';
if ((height += 20) > elHeight) {
el.style.height = elHeight;
el.style.overflow = 'visible';
return;
} else {
requestAnimationFrame(incrementHeight);
}
}());
};
this.slideUp = function(el) {
var height = el.clientHeight;
el.style.overflow = 'hidden';
(function decrementHeight() {
el.style.height = height + 'px';
if ((height -= 20) < 0) {
el.style.display = 'none';
return;
} else {
requestAnimationFrame(decrementHeight);
}
}());
};
this.fadeIn = function(el) {
el.style.opacity = 0;
el.style.display = 'block';
(function fade() {
var opacity = parseFloat(el.style.opacity);
if (!((opacity += 0.07) > 1)) {
el.style.opacity = opacity;
requestAnimationFrame(fade);
}
}());
};
this.fadeOut = function(el) {
el.style.opacity = 1;
(function fade() {
if ((el.style.opacity -= 0.07) < 0) {
el.style.display = "none";
} else {
requestAnimationFrame(fade);
}
}());
};
this.open = function(el, effect) {
if (el === undefined) throw new Error('An element was not provided as an argument to the close method.');
switch (effect) {
case 'display':
this.show(el);
break;
case 'fade':
this.fadeIn(el);
break;
case 'slide':
this.slideDown(el);
break;
}
};
this.close = function(el, effect) {
if (el === undefined) throw new Error('An element was not provided as an argument to the close method.');
switch (effect) {
case 'display':
this.hide(el);
break;
case 'fade':
this.fadeOut(el);
break;
case 'slide':
this.slideUp(el);
break;
}
};
return this;
};
Koalified.MicroComponents = function() {
this.createCloseButton = function(el, closeButtonClassName, closeButtonText) {
var button = document.createElement('button');
closeButtonClassName && button.setAttribute('class', closeButtonClassName);
button.addEventListener('click', this.close.bind(this, this.element, this.animation), false);
if (closeButtonText !== undefined) button.textContent = closeButtonText;
return button;
};
return this;
};
Koalified.Modal = function(params) {
this.init(params);
};
Koalified.Effect.call(Koalified.Modal.prototype);
Koalified.MicroComponents.call(Koalified.Modal.prototype);
Koalified.Modal.prototype.constructor = Koalified.Modal;
Koalified.Modal.prototype.init = function(params) {
if (!params["element"]) throw new Error('A dom element was not passed to the Modal constructor.');
this.element = document.getElementById(params["element"]);
this.modalClassName = params["modalClassName"] !== undefined ? params["modalClassName"] : null;
this.trigger = params["trigger"] !== undefined ? document.getElementById(params["trigger"]) : false;
this.closeButton = params["closeButton"] !== undefined ? params["closeButton"] : true;
this.closeButtonClassName = params["closeButtonClassName"] !== undefined ? params["closeButtonClassName"] : null;
this.closeButtonText = params["closeButtonText"] !== undefined ? params["closeButtonText"] : null;
this.animation = params["animation"] !== undefined ? params["animation"] : 'display';
this.setup();
};
Koalified.Modal.prototype.setup = function() {
if (this.closeButton !== false) {
var closeButton = this.createCloseButton(this.element, this.closeButtonClassName, this.closeButtonText);
this.element.insertBefore(closeButton, this.element.childNodes[0]);
}
this.trigger !== false && this.trigger.addEventListener('click', this.open.bind(this, this.element, this.animation), false);
};
Koalified.Modal.prototype.openModal = function() {
this.open(this.element, this.animation);
};
Koalified.Modal.prototype.closeModal = function() {
this.close(this.element, this.animation);
};