ui-dropdown
Version:
A custom dropdown element
213 lines (173 loc) • 5.98 kB
JavaScript
"use strict"
import view from "./view.js";
class DropdownViewController extends HTMLElement {
static get observedAttributes(){
return ["value"];
}
constructor(model){
super();
this.state = {};
this.state.connected = false;
this.model = model || {};
this.event = {};
this.view = {};
this.shadowRoot = this.attachShadow({mode: "open"});
this.shadowRoot.appendChild(view.content.cloneNode(true));
}
//Fires when the dropdown is inserted into the DOM. It's a good place to set
//the initial role, tabindex, internal state, and install event listeners.
//
//NOTE: A user may set a property on an instance of an dropdown, before its
//prototype has been connected to this class. The _upgradeProperty() method
//will check for any instance properties and run them through the proper
//class setters.
connectedCallback() {
//Set ARIA role, if necesary
if(!this.hasAttribute("role")){ this.setAttribute("role", "select"); }
//Wire views here
this.view.container = this.shadowRoot.querySelector("#container");
this.view.label = this.shadowRoot.querySelector("#label");
this.view.errorMessage = this.shadowRoot.querySelector("#errorMessage");
this.view.select = this.shadowRoot.querySelector("select");
this.view.options = this.shadowRoot.querySelectorAll("option");
//Reference events with bindings
this.event.click = this._onClick.bind(this);
this.event.change = this._onChange.bind(this);
this.view.container.addEventListener("click", this.event.click);
this.view.select.addEventListener("change", this.event.change)
this.state.connected = true;
this.state.startTime = performance.now();
this._updateView();
}
attributeChangedCallback(attrName, oldVal, newVal) {
switch(attrName){
case "value":
if(newVal !== this.value){ this.value = newVal; }
break;
default:
throw new Error(`Attribute ${attrName} is not handled, you should probably do that`);
}
}
get shadowRoot(){return this._shadowRoot;}
set shadowRoot(value){ this._shadowRoot = value}
get hasInvalidResponse(){
return !this.hasValidResponse;
}
get hasValidResponse(){
if(this.model.required){
return this.data[0].response !== null;
}
return true;
}
get data(){
this.datum = {};
this.datum.rt = performance.now() - this.state.startTime;
this.datum.name = this.model.name;
this.datum.question = this.model.label;
if(this.view && this.view.select && this.view.select.value && this.view.select.value !== this.model.placeholder){
this.datum.response = this.view.select.value;
} else{
this.datum.response = null;
}
return [this.datum];
}
//UPDATE ELEMENT TEMPALTE
get model(){ return this._model; }
set model(value){
value.type = "ui-dropdown";
this._model = value;
}
//UPDATE ELEMENT TEMPALTE
get value(){ return this.model; }
set value(value){
//Check if attribute matches property value, Sync the property with the
//attribute if they do not, skip this step if already sync
if(this.getAttribute("value") !== this._toString(value)){
//By setting the attribute, the attributeChangedCallback() function is
//called, which inturn calls this setter again.
this.setAttribute("value", this._toString(value));
//attributeChangeCallback() implicitly called
return;
}
//Ensure it is saved as an objet, and not a string
this.model = this._toJSON(value);
this._updateView();
}
_toString(value){
switch(value.constructor){
case Object:
value = JSON.stringify(value);
break;
}
return value;
}
_toJSON(value){
switch(value.constructor){
case String:
value = JSON.parse(value);
break;
}
return value;
}
_onChange(){
this.dispatchEvent(new CustomEvent("response", {detail: this.data}));
}
_onClick(){
this.dispatchEvent(new CustomEvent("update", {detail: this.data}));
}
_updateView(view) {
//No point in rendering if there isn't a model source, or a view on screen
if(!this.model || !this.state.connected){ return; }
switch(view){
case this.view.label:
this._updateLabelView();
break;
case this.view.select:
this._updateSelectView();
break;
default:
this._updateLabelView();
this._updateSelectView();
}
}
_updateLabelView(){
this.view.label.innerHTML = this.model.label;
}
_updateSelectView(){
this.view.select.innerHTML = "";
this.view.options = null;
this.model.placeholder = this.model.placeholder || "--- Select One ---";
let optionElement = this._newOptionElement(null, this.model.placeholder);
this.view.select.appendChild(optionElement);
if(this.model.options){
this.model.options.forEach(option => {
optionElement = this._newOptionElement(option,option);
this.view.select.appendChild(optionElement);
})
this.view.options = this.shadowRoot.querySelectorAll("option");
}
this.view.select.setAttribute("name", this.model.name);
}
_newOptionElement(value, innerHTML){
const temp = document.createElement("div");
temp.innerHTML = `<option ${value? "value="+value : ""}>${innerHTML}</option>`;
return temp.querySelector("*");
}
_removeEvents(){
this.view.container.removeEventListener("click", this.event.click);
}
disconnectedCallback() {
this._removeEvents()
this.state.connected = false;
}
showValidationError(error){
error = error || "Please provide a valid selection."
this.view.errorMessage.innerText = error;
this.view.select.classList.add("is-invalid");
}
hideValidationError(){
this.view.errorMessage.innerText = "";
this.view.select.classList.remove("is-invalid");
}
}
window.customElements.define("ui-dropdown", DropdownViewController);