SELECT-REPLACE (Sonstige Elemente )
src/app/shared/utilities/select-replace/templates
Demo Section
Each variation will be presented in the following section.
Readme
select-replace (utility
)
Description
Utility that hides the native select and adds a custom UI which can be styled easily. Actions and the whole state of the ui are mapped to the native select. It has also multiselect support.
Requirements
- @veams/core - Veams Core Framework.
- @veams/query or
jquery
- Veams Query or jQuery. - @veams/component - Veams Component.
- @veams/component-form - Form component in Veams.
Installation
Installation with Veams from local machine
veams install bp absolute/filepath/to/select-replace
JavaScript Options
The module gives you the possibility to override default JS options:
Option | Type | Default | Description |
---|---|---|---|
classes.active | String | 'is-active' |
Class to be set on button if at least one item is selected |
classes.disabled | String | 'is-disabled' |
Class to be set on item that should be disabled |
classes.open | String | 'is-open' |
Class for open state of button and list |
classes.uiReplaced | String | 'is-ui-replaced' |
Class to be set on native select after ui has been been created |
i18n.placeholder | String | 'Please select' |
Fallback text for placeholder (should be overwritten in other languages) |
i18n.selected | String | 'Selected' |
Fallback screenreader-only label for selected element (should be overwritten in other languages) |
i18n.selectMenu | String | 'Select Menu' |
Fallback screenreader-only headline text for select element (should be overwritten in other languages) |
selectors.btn | String | '[data-js-item="btn"]' |
Element selector for button |
selectors.btnWrapper | String | '[data-js-item="btn-wrapper"]' |
Element selector for button wrapper |
selectors.listItem | String | '[data-js-item="list-item"]' |
Element selector for list item |
selectors.listWrapper | String | '[data-js-item="list-wrapper"]' |
Element selector for list wrapper |
selectors.listWrapperInner | String | '[data-js-item="list-wrapper-inner"]' |
Element selector for list wrapper inner |
templates.btn | String | 'SELECTREPLACE__BTN' |
Client side template for button (which opens the list) |
templates.list | String | 'SELECTREPLACE__LIST' |
Client side template for list |
templates.preRender | String | 'SELECTREPLACE__PRERENDER' |
Client side template for prerendering custom select UI |
useSelectWidth | Boolean | true |
Sync width of replacement element with width of native select |
validateSelectEvent | String | 'formValidation:validateSelect' |
Event triggered as soon as select is validated (domAction and errorClass as event obj props) |
Templates
select-replace.hbs
{{#wrapWith "form"}}
{{> form__select }}
{{/wrapWith}}
Data File
select-replace.hjson
{
"variations": {
"simple": {
"docs": {
"variationName": "Simple Select"
},
"settings": {
"id": "select-1",
"labelClasses": "is-aural",
"required": false,
"jsModule": true,
"jsOptions": {
"placeholder": "Land"
}
},
"content": {
"label": "Bitte Land auswählen",
"options": [
{
"settings": {
"disabled": true,
"selected": true
},
"content": {
"val": "",
"name": "Land"
}
},
{
"content": {
"val": "Lorem-1",
"name": "Lorem-2"
}
},
{
"content": {
"val": "Lorem-2",
"name": "Lorem-2"
}
},
{
"content": {
"val": "Lorem-3",
"name": "Lorem-3"
}
},
{
"content": {
"val": "Lorem-4",
"name": "Lorem-4"
}
}
]
}
},
"multiple": {
"docs": {
"variationName": "Multiple Select"
},
"settings": {
"id": "select-2",
"labelClasses": "is-aural",
"required": false,
"jsModule": true,
"jsOptions": {
"placeholder": "Land"
},
"attributes": [
{
"attrKey": "multiple",
"attrValue": "multiple"
}
]
},
"content": {
"label": "Bitte Länder auswählen",
"options": [
{
"content": {
"val": "Lorem-1",
"name": "Lorem-1"
}
},
{
"content": {
"val": "Lorem-2",
"name": "Lorem-2"
}
},
{
"content": {
"val": "Lorem-3",
"name": "Lorem-3"
}
},
{
"content": {
"val": "Lorem-4",
"name": "Lorem-4"
}
}
]
}
}
}
}
Scripts
select-replace.js
/**
* This utility replaces the UI of native select for styling purposes.
*
* @module SelectReplace
* @version v1.0.0
*
* @author Andy Gutsche
*/
// Imports
import $ from 'jquery';
import Component from '@veams/component';
// Variables
class SelectReplace extends Component {
/**
* Class Properties
*/
$el = $(this.el);
isMultiSelect = this.el.multiple;
isOpen = false;
modifierClasses = [];
/**
* Constructor for our class
*
* @see module.js
*
* @param {Object} obj - Object which is passed to our class
* @param {Object} obj.el - element which will be saved in this.el
* @param {Object} obj.options - options which will be passed in as JSON object
*/
constructor(obj) {
let options = {
classes: {
active: 'is-active',
disabled: 'is-disabled',
open: 'is-open',
uiReplaced: 'is-ui-replaced'
},
i18n: {
placeholder: 'Please select',
selected: 'Selected',
selectMenu: 'Select Menu'
},
selectors: {
btn: '[data-js-item="btn"]',
btnWrapper: '[data-js-item="btn-wrapper"]',
listItem: '[data-js-item="list-item"]',
listWrapper: '[data-js-item="list-wrapper"]',
listWrapperInner: '[data-js-item="list-wrapper-inner"]'
},
templates: {
btn: 'SELECTREPLACE__BTN',
list: 'SELECTREPLACE__LIST',
preRender: 'SELECTREPLACE__PRERENDER'
},
useSelectWidth: true,
validateSelectEvent: 'formValidation:validateSelect'
};
super(obj, options);
}
/**
* Get module information
*
*/
static get info() {
return {
version: '1.0.0'
};
}
/**
* Event handling
*
*/
get events() {
return {
'{{this.context.EVENTS.change}}': 'renderAll',
'{{this.options.validateSelectEvent}}': 'onValidateSelect'
};
}
/**
* Subscribe handling
*
*/
get subscribe() {
return {
'{{this.context.EVENTS.selectReplace.closeAll}}': 'closeList'
};
}
/**
* Get data from native select
*
*/
get data() {
let $options = this.$el.find('option');
let data = {};
let optionsArr = [];
let i = 0;
for (i; i < $options.length; i++) {
let $option = $($options[i]);
optionsArr[i] = {};
optionsArr[i].text = $option.text();
optionsArr[i].value = $option[0].value;
optionsArr[i].selected = $option[0].selected;
optionsArr[i].disabled = $option[0].disabled;
}
data.items = optionsArr;
data.i18n = this.options.i18n;
data.disabled = this.el.disabled;
return data;
}
/**
* Initialize the view
*
*/
didMount() {}
/**
* Bind events
*
*/
bindEvents() {
let fnDefaultClickHandler = this.defaultClickHandler.bind(this);
let fnButtonHandler = this.buttonHandler.bind(this);
let fnSelectItemHandler = this.selectItemHandler.bind(this);
// Global events
if (this.options.useSelectWidth) {
this.registerEvent('{{this.context.EVENTS.resize}}', 'setWidth', true);
}
// Local events
$(document.body).on(this.context.EVENTS.click, fnDefaultClickHandler);
this.$btnWrapper.on(this.context.EVENTS.click, fnButtonHandler);
this.$listWrapper.on(
this.context.EVENTS.click,
this.options.selectors.listItem,
fnSelectItemHandler
);
}
/**
* Handle select validation state
*
* @param {Object} e - Event object
* @param {Object} obj - Additional event data
* @param {String} obj.domAction - DOM action ('addClass' | 'removeClass')
* @param {String} obj.errorClass - Error class (eg. 'is-error')
*/
onValidateSelect(e, obj) {
this.$btnWrapper[obj.domAction](obj.errorClass);
}
/**
* Handle click outside button or list
*
* @param {Object} [e] - Event object
*/
defaultClickHandler(e) {
this.closeList();
}
/**
* Handle click on button
*
* @param {Object} e - Event object
*/
buttonHandler(e) {
e.stopPropagation();
this.isOpen ? this.closeList() : this.openList();
}
/**
* Handle click on list item
*
* @param {Object} e - Event object
*/
selectItemHandler(e) {
let $clickedEl = $(e.target);
let $option = $('option[value="' + $clickedEl.attr('data-value') + '"]', this.el);
e.stopPropagation();
if ($clickedEl.hasClass(this.options.classes.disabled)) {
return;
}
if ($option[0].selected) {
$option[0].selected = false;
} else {
$option[0].selected = true;
}
this.$el.trigger(this.context.EVENTS.change);
if (!this.isMultiSelect) {
this.closeList();
}
this.lastScrollOffset = this.$listWrapperInner[0].scrollTop;
this.renderAll();
}
/**
* Open list
*
*/
openList() {
this.context.Vent.trigger(this.context.EVENTS.selectReplace.closeAll);
this.$btn.attr('aria-expanded', true);
this.$btnWrapper.addClass(this.options.classes.open);
this.$listWrapper.addClass(this.options.classes.open);
this.$listWrapper.removeAttr('disabled');
this.isOpen = true;
}
/**
* Close list
*
*/
closeList() {
this.$listWrapperInner[0].scrollTop = 0;
this.$btn.attr('aria-expanded', false);
this.$btnWrapper.removeClass(this.options.classes.open);
this.$listWrapper.removeClass(this.options.classes.open);
this.$listWrapper.attr('disabled', 'disabled');
this.isOpen = false;
}
/**
* Set width of rendered ui element to width of native select element
*
*/
setWidth() {
this.$replaceEl.css({
width: this.$el.outerWidth()
});
}
/**
* Render container for button and list
*
*/
preRender() {
let preRenderData = {
isMultiSelect: this.isMultiSelect,
instanceId: this.instanceId
};
let classes = [].slice.call(this.$el[0].classList);
this.$replaceEl = $(this.renderTemplate(this.options.templates.preRender, preRenderData));
for (let i = 0; i < classes.length; i++) {
if (classes[i].indexOf('is-') === 0) {
this.modifierClasses.push(classes[i]);
}
}
this.$btnWrapper = this.$replaceEl.find(this.options.selectors.btnWrapper);
this.$listWrapper = this.$replaceEl.find(this.options.selectors.listWrapper);
this.el.parentNode.insertBefore(this.$replaceEl[0], this.el.nextSibling);
if (this.options.useSelectWidth) {
this.setWidth();
}
this.$el.addClass(this.options.classes.uiReplaced);
this.renderAll();
}
/**
* Render button and list
*
*/
renderAll() {
this.renderBtn(this.data);
this.renderList(this.data);
}
/**
* Render button
*
* @param {Object} data - Object containing data from native select
*/
renderBtn(data) {
let i = 0;
let text = '';
let btnData = {
instanceId: this.instanceId,
modifierClasses: this.modifierClasses.join(' '),
ariaExpanded: this.isOpen,
disabled: data.disabled,
tabIndex: this.el.tabIndex
};
this.$btn = $(this.renderTemplate(this.options.templates.btn, btnData));
let selectedCount = 0;
for (i; i < data.items.length; i++) {
if (data.items[i].selected) {
text = data.items[i].text;
selectedCount++;
}
}
if (selectedCount === 0) {
text = this.options.i18n.placeholder;
this.$btnWrapper.removeClass(this.options.classes.active);
} else {
this.$btnWrapper.addClass(this.options.classes.active);
}
if (selectedCount > 1) {
text = this.options.i18n.placeholder + ' (' + selectedCount + ')';
}
this.$btn
.text(text)
.attr(
'aria-label',
this.options.i18n.selectMenu + ': ' + this.options.i18n.placeholder
);
this.$btnWrapper.empty();
this.$btnWrapper.append(this.$btn);
}
/**
* Render list
*
* @param {Object} data - Object containing data from native select
*/
renderList(data) {
this.$listWrapperInner = $(this.renderTemplate(this.options.templates.list, data));
this.$listWrapperInner[0].scrollTop = this.lastScrollOffset;
this.$listWrapper.empty();
this.$listWrapper.append(this.$listWrapperInner);
}
}
export default SelectReplace;
HTML Output
Simple Select
<form class="c-form--default
" action="" method="" data-css="c-form">
<div class="form__select">
<div class="form__label-wrapper">
<label for="" class="form__select-label">
Bitte Land auswählen
</label>
</div>
<div class="form__select-wrapper">
<!--
data-placeholder="Bitte Wählen"
data-reset-text=" - "
data-has-reset-option="true|false"
-->
<select id="select-1" data-placeholder="Bitte Wählen" data-js-module="form-dropdown" name="">
<option value="" disabled selected>Land</option>
<option value="Lorem-1">Lorem-2</option>
<option value="Lorem-2">Lorem-2</option>
<option value="Lorem-3">Lorem-3</option>
<option value="Lorem-4">Lorem-4</option>
</select>
</div>
</div>
</form>
Multiple Select
<form class="c-form--default
" action="" method="" data-css="c-form">
<div class="form__select">
<div class="form__label-wrapper">
<label for="" class="form__select-label">
Bitte Länder auswählen
</label>
</div>
<div class="form__select-wrapper">
<!--
data-placeholder="Bitte Wählen"
data-reset-text=" - "
data-has-reset-option="true|false"
-->
<select id="select-2" data-placeholder="Bitte Wählen" data-js-module="form-dropdown" name="" multiple="multiple">
<option value="Lorem-1">Lorem-1</option>
<option value="Lorem-2">Lorem-2</option>
<option value="Lorem-3">Lorem-3</option>
<option value="Lorem-4">Lorem-4</option>
</select>
</div>
</div>
</form>