To offer a simple select alternative to radio buttons, mirroring the same attributes as other form controls. Using attributes such as value, disabled to offer a drop in control for configuring something off or on. Bootstraped from a bootstrap file, by importing into a bigger application or direct as html script (ensure you build fallback for IE support).
<my-box-select></my-box-select>
<!-- Polyfill -->
<script src="/node_modules/promise-polyfill/dist/polyfill.min.js"></script>
<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-loader.js"></script>
<!-- Bootstrap component only, ensure you have built fallback for IE support too (.js)! -->
<script type="module" src="./your/path/my-box-select.mjs"></script>
<script nomodule src="./your/path/my-box-select.js"></script>
<!-- Or add your switch import to a bootstrap js file/app using ES6 import and use that instead of the component directly... -->
<script type="module" src="./your/path/index.mjs"></script>
<script nomodule src="./your/path/index.js"></script>
<!-- example basic fouc resolution -->
<style>
[fouc] { opacity: 0; transition: opacity 200ms ease-in-out; }
[fouc="loaded"] { opacity: 1; }
</style>
...
<div class="info">
<div class="class">
<div class="row" fouc>
<div class="col-sm-4">
<my-box-select label="Hello" value="1">
<option value="1">One</option>
<option value="2">Two</option>
<option value="3">Three</option>
<option value="4">Four</option>
</my-box-select>
</div>
<div class="col-sm-4">
<my-box-select label="Hello" value="2">
<option value="1">One</option>
<option value="2">Two</option>
<option value="3">Three</option>
<option value="4">Four</option>
</my-box-select>
</div>
<div class="col-sm-4">
<my-box-select label="Hello" value="3" disabled>
<option value="1">One</option>
<option value="2">Two</option>
<option value="3">Three</option>
<option value="4">Four</option>
</my-box-select>
</div>
</div>
</div>
</div>
...
<!-- fouc for individual components -->
<script> setTimeout(() => document.querySelector('[fouc]').setAttribute('fouc', 'loaded'), 1000); </script>
import { CustomHTMLElement, html } from '../../../node_modules/custom-web-component/index.js';
/**
* @public @name MyBoxSelect
* @extends CustomHTMLElement
* @description Box select control, kinda like a radio button, but boxes instead!
*/
class MyBoxSelect extends CustomHTMLElement {
/**
* @public @constructor @name constructor
* @description Triggered when component is instantiated (but not ready or in DOM, must call super() first)
*/
constructor() {
super();
this.value;
this.invalid;
this.options = [];
}
/**
* @public @static @name template
* @description Template function to return web component UI
* @return {TemplateResult} HTML template result
*/
static template() {
return html`
<style>
:host {
display: inline-block;
width: 100%;
min-height: 62px;
box-sizing: border-box;
}
.container {
display: inline-block;
width: 100%;
height: inherit;
padding: 6px 0 6px 0;
box-sizing: border-box;
position: relative;
}
.container [invisible] { opacity: 0; }
.container label {
display: block;
min-height: 20px;
color: var(--my-box-select--label--color, #222);
font-weight: var(--my-box-select--label--font-weight, normal);
text-align: var(--my-box-select--label--text-align, left);
font-size: 14px;
overflow: hidden;
}
.container .boxer-buttons {
display: flex;
padding: 1px;
width: 100%;
flex-flow: row;
border: var(--my-box-select--border, 1px solid #222);
border-radius: var(--my-box-select--border-radius, 0);
box-sizing: border-box;
}
.container .boxer-buttons .option {
display: block;
padding: 0 5px;
height: 28px;
flex: 1 1;
background: var(--my-box-select--option--background, white);
color: var(--my-box-select--option--color, #222);
font-size: var(--my-box-select--option--font-size, 13px);
font-family: var(--my-box-select--option--font-family, inherit);
cursor: var(--my-box-select--option--cursor, default);
text-align: center;
line-height: 30px;
cursor: default;
white-space: nowrap;
border-radius: var(--my-box-select--option--border-radius, 0);
}
.container .boxer-buttons .option[selected] { background: var(--my-box-select--option--background--selected, #222); color: var(--my-box-select--option--color--selected, white); cursor: default; }
:host([disabled]) .container { pointer-events: none; cursor: not-allowed; opacity: var(--my-box-select--disabled--opacity, 0.6); }
</style>
<div class="container" ?invalid="${this.invalid}">
<label ?invisible="${!this.hasAttribute('label')}">${this.getAttribute('label')}</label>
<div id="boxer-buttons" class="boxer-buttons" @change="${this._changeEvent.bind(this)}">
${this.options.length > 0 ? this.options.map((option) => html`
<span class="option" tabindex="0" value="${option.value}" ?selected="${option.selected}" @click="${this._changeEvent.bind(this)}">${option.label}</span>
`) : ''}
</div>
</div>
`;
}
/**
* @public @static @get @name observedProperties
* @description Provide properties to watch for changes
* @return {Array} Array of property names as strings
*/
static get observedProperties() { return ['value'] }
/**
* @public @name propertyChanged
* @description Callback run when a custom elements properties change
* @param {String} property The property name
* @param {Mixed} oldValue The old value
* @param {Mixed} newValue The new value
*/
propertyChanged(property, oldValue, newValue) {
this.updateTemplate();
}
/**
* @public @static @get @name observedAttributes
* @description Provide attributes to watch for changes
* @return {Array} Array of attribute names as strings
*/
static get observedAttributes() { return ['name', 'label', 'disabled'] }
/**
* @public @name attributeChanged
* @description Callback run when a custom elements attributes change
* @param {String} attribute The attribute name
* @param {Mixed} oldValue The old value
* @param {Mixed} newValue The new value
*/
attributeChanged(attribute, oldValue, newValue) {
this.updateTemplate();
}
/**
* @public @name constructed
* @description Lifecycle hook that gets called when the element is finished contructing, perfect for setting up properties
*/
constructed() {
this.value = this.hasAttribute('value') ? this.getAttribute('value') : this.value;
}
/**
* @public @name connected
* @description Callback run once the custom element has been added to the DOM and template is rendered
*/
connected() { }
/**
* @public @name templateUpdated
* @description Callback run once the template has complete re-render
*/
templateUpdated() {
this.rerender();
}
/**
* @public @name templateUpdated
* @description Callback run once the custom element has complete a re-render
*/
rerender() {
setTimeout(() => {
this.options = [];
let options = this.querySelectorAll('option');
for (let i = 0; i < options.length; i++) {
let value = options[i].hasAttribute('value') ? options[i].getAttribute('value') : options[i].innerText;
this.options.push({
label: options[i].innerText,
value: value,
selected: value === this.value
});
}
this.updateTemplate();
}, 1);
}
/**
* @private @name _event
* @description Detect an event, update a property and dispatch an event
* @param {Event} ev Any event that kicks the function
*/
_changeEvent(ev) {
if (this.hasAttribute('disabled')) return;
this.value = ev.target.hasAttribute('value') ? ev.target.getAttribute('value') : ev.target.innerText;
this._select(this.value);
this.updateTemplate();
this.dispatchEvent(new CustomEvent('change', { detail: ev }));
ev.stopPropagation();
}
/**
* @private @name _select
* @description Select a value
* @param {Event} ev Any event that kicks the function
*/
_select(value) {
for (let i = 0; i < this.options.length; i++) this.options[i].selected = this.options[i].value === this.value;
this.updateTemplate();
}
}
// bootstrap the class as a new web component
customElements.define('my-box-select', MyBoxSelect);