My goal has been to learn Polymer
so I started developing a complex combo-box element.
I did my development on Plunker. Here is link to my plunk.
I'm happy with my .scss
file, but I wouldn't be opposed to some constructive criticism there. That being said what I'm mainly looking for though is your thoughts on the following three topics:
Am I structuring my
combo-box.html
template bindings in a bad way? If so how can I improve? I'm used to usingknockout.js
so some of my organization may come from my experience with it.Currently I'm using attributes to take in settings a consumer of
combo-box
can pass to the JavaScript logic. Is this the best way to achieve a method the user ofcombo-box
can decide whether they wantx
and noty
feature?Lastly I'm wondering if I could get some opinions on how I organized my code in
combo-box.js
. I started this learning experience with the idea that I didn't want any external dependencies besidespolymer.js
of course and theplatform.js
for polyfills thatpolymer.js
depends on. That being said I structured my code in this basic manner:(function(Polymer, undefined) { // any global to this closure vars and functions // polyfill functions (this goes along with my goal of no using other libs) // fn that returns an object (in my mind I think of this like my model/view model // call into Polymer function and declaration of the element life cycle methods }(Polymer));
The three main points examples have been included below:
combo-box.html
<polymer-element name="combo-box">
<template>
<link rel="stylesheet"
href="combo-box.css">
<template bind="{{ comboBox }}">
<div class="comboBoxWrap">
<template bind="{{ elementAttributes }}">
<template if="{{ leftalignedicon }}">
<img src="{{ leftalignedicon }}"
class="leftAlignedIcon"
alt="leftAlignedIcon">
<input type="text"
class="leftAlignedIconInput"
value="{{ value }}"
on-focus="{{ toggleOptionsVisibility }}"
on-blur="{{ toggleOptionsVisibility }}"
on-keydown="{{ optionsInputKeydown }}">
</template>
<template if="{{ rightalignedicon }}">
<img src="{{ rightalignedicon }}"
class="rightAlignedIcon"
alt="rightAlignedIcon">
<input type="text"
class="rightAlignedIconInput"
value="{{ value }}"
on-focus="{{ toggleOptionsVisibility }}"
on-blur="{{ toggleOptionsVisibility }}"
on-keydown="{{ optionsInputKeydown }}">
</template>
<template if="{{ !leftalignedicon && !rightalignedicon }}">
<input type="text"
on-focus="{{ toggleOptionsVisibility }}"
on-blur="{{ toggleOptionsVisibility }}"
on-keydown="{{ optionsInputKeydown }}"
value="{{ value }}">
</template>
</template>
<template if="{{ showOptions }}">
<ul>
<template repeat="{{ opt in options }}">
<li class="{{ { active: opt.active } | tokenList }}"
on-mousedown="{{ optionClick }}"
on-mouseover="{{ onHovered }}"
on-mouseout="{{ onUnhovered }}">{{ opt.text }}</li>
</template>
</ul>
</template>
</div>
</template>
</template>
<script src="comboBox.js"></script>
</polymer-element>
combo-box.js
(function(Polymer, undefined) {
var pxSuffix = 'px';
function getNodeIndex(node) {
var index = -1;
while ((node = node.previousSibling)) {
if (node.nodeType != 3 || !/^\s*$/.test(node.data)) {
index++;
}
}
return index;
}
function getJSON(url, callback) {
var request = new XMLHttpRequest();
request.open('GET', url, true);
request.onreadystatechange = function() {
if (this.readyState === 4){
if (this.status >= 200 && this.status < 400){
// Success!
callback(JSON.parse(this.responseText));
}
else {
// Error :(
callback(this);
}
}
};
request.send();
request = null;
}
// constructor function for a new ComboBox
function ComboBox(customElement) {
var comboBoxModel = {
value: '',
options: [],
showOptions: false,
elementAttributes: {}
};
var i = 0;
var currentAttr;
var request;
var historyprop = 'history';
// take the html attributes from customElement
// and augment them onto our comboBoxModel
for (i; i < customElement.attributes.length; i++) {
currentAttr = customElement.attributes[i];
comboBoxModel.elementAttributes[currentAttr.nodeName] = currentAttr.nodeValue;
}
// get the data for our comboBox
if(typeof comboBoxModel.elementAttributes.history !== 'undefined') {
if(typeof comboBoxModel.elementAttributes.historyprop !== 'undefined') {
historyprop = comboBoxModel.elementAttributes.historyprop;
}
getJSON(
comboBoxModel.elementAttributes.history,
function(responseObj) {
var iObj;
i = 0;
for (i; i < responseObj[historyprop].length; i++) {
iObj = responseObj[historyprop][i];
iObj.active = false;
comboBoxModel.options.push(iObj);
}
}
);
}
return comboBoxModel;
}
Polymer('combo-box', {
created: function() {
this.comboBox = ComboBox(this);
},
ready: function() {
// I don't love this solution for icons in the input box, but this is what I got
var shadowRoot = this.shadowRoot;
var inputEl = shadowRoot.querySelector("input[type='text'");
var imgEl = shadowRoot.querySelector('img');
var imgHeight;
var imgWidth;
var inputHeight;
var inputWidth;
var heightDiffInHalf;
if(inputEl !== null && imgEl !== null ) {
imgHeight = imgEl.clientHeight;
imgWidth = imgEl.clientWidth;
inputHeight = inputEl.offsetHeight;
inputWidth = inputEl.offsetWidth;
heightDiffInHalf = (inputHeight - imgHeight) / 2;
// set the img height to be the offsetTop of the input and center it within the input
imgEl.style.top = inputEl.offsetTop + heightDiffInHalf + pxSuffix;
// Depending on left vs right set the position of the img
if(this.comboBox.elementAttributes.leftalignedicon || this.comboBox.elementAttributes.rightalignedicon) {
if(this.comboBox.elementAttributes.leftalignedicon) {
imgEl.style.left = inputEl.offsetLeft + pxSuffix;
inputEl.style.paddingLeft = imgWidth + pxSuffix;
}
if(this.comboBox.elementAttributes.rightalignedicon) {
imgEl.style.left = (inputEl.offsetLeft + inputWidth) + pxSuffix;
inputEl.style.paddingRight = imgWidth + pxSuffix;
}
}
}
},
attributeChanged: function(attrName, oldVal, newVal) {
var _newVal = this.getAttribute(attrName);
console.log('attributeChanged');
console.log(attrName, 'old: ' + oldVal, 'newVal:', newVal);
console.log('_newVal: ' + _newVal)
},
toggleOptionsVisibility: function(e) {
// in the docs it says to get the model by doing:
// var model = e.target.templateInstance.model;
// BUT this seems to work
// using self for setTimeout
var self = this;
self.comboBox.showOptions = !this.comboBox.showOptions;
},
optionsInputKeydown: function(e) {
var keyCode = e.keyCode || e.which;
var i = 0;
var optionsLen = this.comboBox.options.length;
var activeIndex = -1;
// find the active option index
for(i; i < optionsLen; i++) {
if(this.comboBox.options[i].active === true){
activeIndex = i;
break;
}
}
if(keyCode === 13) {
// Enter
console.log('Option chosen (Enter)... execute user defined callback');
console.log(activeIndex);
}
else if(keyCode === 38) {
// Up arrow
if(activeIndex > 0 && activeIndex < optionsLen) {
this.comboBox.options[activeIndex].active = false;
this.comboBox.options[activeIndex - 1].active = true;
}
else {
if(activeIndex === 0) {
this.comboBox.options[activeIndex].active = false;
}
this.comboBox.options[optionsLen - 1].active = true;
}
}
else if(keyCode === 40){
// Down arrow
if(activeIndex > -1 && activeIndex < (optionsLen - 1)) {
this.comboBox.options[activeIndex].active = false;
this.comboBox.options[activeIndex + 1].active = true;
}
else {
if(activeIndex === (optionsLen - 1)) {
this.comboBox.options[activeIndex].active = false;
}
this.comboBox.options[0].active = true;
}
}
},
onHovered: function(event, detail, sender) {
var i = 0;
var hoveredIndex = getNodeIndex(sender);
for(i; i < this.comboBox.options.length; i++) {
this.comboBox.options[i].active = false;
}
this.comboBox.options[hoveredIndex].active = true;
},
onUnhovered: function(event, detail, sender) {
var hoveredIndex = getNodeIndex(sender);
this.comboBox.options[hoveredIndex].active = false;
},
optionClick: function(event, detail, sender) {
console.log('Option chosen (Click)... execute user defined callback');
console.log(getNodeIndex(sender));
}
});
}(Polymer));
Example of use/configuration
<!DOCTYPE html>
<html>
<head>
<title>combo-box</title>
</head>
<body>
<h1>combo-box</h1>
<!-- Polymer -->
<script src="//cdnjs.cloudflare.com/ajax/libs/polymer/0.3.4/platform.js"></script>
<script src="//cdnjs.cloudflare.com/ajax/libs/polymer/0.3.4/polymer.js"></script>
<!-- combo-box -->
<link rel="import" href="combo-box.html">
<!--
Attribute Documentation:
history: A path/url for an ajax request to return correctly formatted JSON
historyProp: Property name that corresponds to the array of history options
(Optional: Defaults to 'history')
leftAlignedIcon: Path/url for an image to use as an icon in the left side of the input box
rightAlignedIcon: Path/url for an image to use as an icon in the right side of the input box
-->
<combo-box history="history.json" historyProp="myHistory" ></combo-box>
<combo-box history="history.json"
historyProp="myHistory"
leftAlignedIcon="https://cdn1.iconfinder.com/data/icons/google_jfk_icons_by_carlosjj/16/search.png">
</combo-box>
<combo-box history="history.json"
historyProp="myHistory"
rightAlignedIcon="https://cdn2.iconfinder.com/data/icons/snipicons/500/search-16.png">
</combo-box>
</body>
</html>