Skip to content

Commit

Permalink
feat(select): new component select
Browse files Browse the repository at this point in the history
  • Loading branch information
bndynet committed Nov 17, 2019
1 parent 483e01b commit a2fbe71
Show file tree
Hide file tree
Showing 8 changed files with 258 additions and 1 deletion.
19 changes: 19 additions & 0 deletions site/Components/Select.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
<ui-select options='["Option 1", "Option 2"]' value="Option 2" placeholder="Choose the item..."></ui-select>
<ui-select label="Country" options='[
{"label": "China", "value": "China", "description": "Here is the country description."},
{"label": "Canada", "value": "Canada", "description": "Here is the country description."}
]' value="Canada" value-field="value"
value-template="<img src='https://www.printableworldflags.com/icon-flags/24/{label}.png'> {label} <small class='text-muted'>{description}</small>"
option-template="<img src='https://www.printableworldflags.com/icon-flags/24/{label}.png'> {label} <br /> <small class='text-muted'>{description}</small>"></ui-select>
<ui-select label="Country" options='[
{"label": "China", "value": "China", "description": "Here is the country description."},
{"label": "Canada", "value": "Canada", "description": "Here is the country description."},
{"label": "Chad", "value": "Chad", "description": "Here is the country description."},
{"label": "Cuba", "value": "Cuba", "description": "This option is disabled.", "disabled": true},
{"label": "Eritrea", "value": "Eritrea", "description": "Here is the country description."},
{"label": "Haiti", "value": "Haiti", "description": "Here is the country description."},
{"label": "Laos", "value": "Laos", "description": "Here is the country description."},
{"label": "Kosovo", "value": "Kosovo", "description": "Here is the country description."}
]' value="China" value-field="value"
disabled-field="disabled"
option-template="<img src='https://www.printableworldflags.com/icon-flags/24/{label}.png'> {label} <br /> <small class='text-muted'>{description}</small>" required></ui-select>
2 changes: 1 addition & 1 deletion site/script.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ function appendNav(navText) {

$(function() {
var navs = {
Components: ['Buttons', 'Loading', 'Indicators', 'Input'],
Components: ['Buttons', 'Loading', 'Indicators', 'Input', 'Select'],
};

setTimeout(function() {
Expand Down
34 changes: 34 additions & 0 deletions src/common/RootElement.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,4 +58,38 @@ export abstract class RootElement extends LitElement {
part.setValue(attrValue);
}
});

protected isOutsideEvent(evt: Event): boolean {
let targetElement = evt.target;

while (targetElement) {
if (targetElement == this) {
return false;
}
targetElement = (targetElement as HTMLElement).parentNode;
}
return true;
}

protected removeChildren(element: Element): void {
while (element.firstChild) {
element.removeChild(element.firstChild);
}
}

protected formatTemplate(template: string | undefined | null, value: any): string {
if (!template) {
return '';
}

if (typeof value !== 'object') {
return template.replace(/\{0\}/g, value);
} else {
let result = template;
Object.keys(value).forEach((key: string) => {
result = result!.replace(new RegExp(`{${key}}`, 'g'), value[key]);
});
return result;
}
}
}
137 changes: 137 additions & 0 deletions src/components/select/select-input.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
import { classMap } from 'lit-html/directives/class-map';
import { unsafeHTML } from 'lit-html/directives/unsafe-html';
import { html, customElement, TemplateResult, property } from 'lit-element';
import { RootElement } from '../../common/RootElement';

const TAG_NAME = 'ui-select-input';

@customElement(TAG_NAME)
export class SelectInput extends RootElement {
@property({ type: String, reflect: true }) value: string | undefined | null;
@property({ type: Array, reflect: true }) options = [];
@property({ type: String, attribute: 'option-template' }) optionTemplate: string | null | undefined;
@property({ type: String, attribute: 'value-template' }) valueTemplate: string | null | undefined;
@property({ type: String, attribute: 'value-field' }) valueField: string | undefined;
@property({ type: String, attribute: 'text-field' }) textField: string | undefined;
@property({ type: String, attribute: 'disabled-field' }) disabledField: string | undefined;
@property({ type: String }) placeholder = '';
@property({ type: Boolean }) required = false;

private valid = false;
private invalid = false;
private showOptions = false;
private selectedOptionIndex = -1;

public constructor() {
super();
}

public connectedCallback(): void {
document.addEventListener('click', evt => {
if (this.isOutsideEvent(evt)) {
this.showOptions = false;
this.requestUpdate();
}
});
super.connectedCallback();
}

protected firstUpdated(): void {
this.selectedOptionIndex = this.options.findIndex((option: any) => {
if (typeof option !== 'object') {
return option === this.value;
} else if (this.valueField) {
return option[this.valueField] === this.value;
}
return false;
});

const eleInput = this.querySelector('.form-control');
if (eleInput) {
eleInput.addEventListener('click', () => {
this.showOptions = !this.showOptions;
this.requestUpdate();
});
}

const eleOption = this.querySelectorAll('.select-option');
Array.from(eleOption).forEach((element: Element) => {
element.addEventListener('click', () => {
if (element.classList.contains('disabled')) {
return;
}
const val = element.getAttribute('value');
const idx = parseInt(element.getAttribute('data-index') || '-1');
this.value = val;
this.showOptions = false;
this.selectedOptionIndex = idx;
if (this.required) {
this.invalid = !this.value;
this.valid = !!this.value;
}
this.requestUpdate();
});
});
}

protected render(): TemplateResult {
const eleInputClass = { 'form-control': true, 'is-valid': this.valid, 'is-invalid': this.invalid };
const eleOptionsClass = { 'select-options': true, active: this.showOptions };
const valueView =
this.selectedOptionIndex > -1
? this.getHtmlFromTemplate(this.valueTemplate || this.optionTemplate, this.selectedOptionIndex)
: this.value
? html`
${this.value}
`
: html`
<span class="placeholder">${this.placeholder}</span>
`;
return html`
<div class="${classMap(eleInputClass)}" tabindex="0">
${valueView}
</div>
<div class="${classMap(eleOptionsClass)}">
${this.options.map((_option: any, index: number) => this.getHtmlFromTemplate(this.optionTemplate, index))}
</div>
`;
}

private getHtmlFromTemplate(template: string | null | undefined, index: number): TemplateResult {
const option = this.options[index];
if (!option) {
return html``;
}

const optionClass: any = {
'select-option': true,
disabled: this.disabledField && option[this.disabledField],
};
let text = this.formatTemplate(template, option);
let value = '';
if (typeof option !== 'object') {
value = option;
if (!text) {
text = option;
}
} else {
if (this.valueField) {
value = option[this.valueField];
} else {
throw new Error(`No value-field attribute found for ${TAG_NAME} element.`);
}

if (!text) {
if (this.textField) {
text = option[this.textField];
} else {
throw new Error(`No text-field attribute found for ${TAG_NAME} element.`);
}
}
}
optionClass['active'] = value === this.value;
return html`
<div class="${classMap(optionClass)}" value="${value}" data-index="${index}">${unsafeHTML(text)}</div>
`;
}
}
42 changes: 42 additions & 0 deletions src/components/select/select.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
ui-select {
position: relative;
display: block;

.form-control {
min-height: calc(1.5em + .75rem + 2px);
height: auto;
}

.placeholder {
color: #6c757d!important;
}

.select-options {
position: absolute;
z-index: 100;
border-radius: 4px;
border: solid 1px #ced4da;
width: 100%;
display: none;
background-color: #ffffff;

&.active {
display: block;
}

.select-option {
padding: .375rem .75rem;
cursor: pointer;
&:not(.disabled):hover {
background-color: var(--hover-bg-color);
}
&.active {
background-color: #dedede;
}
&.disabled {
cursor: default;
color: var(--gray);
}
}
}
}
20 changes: 20 additions & 0 deletions src/components/select/select.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { html, customElement, TemplateResult, property } from 'lit-element';
import { SelectInput } from './select-input';

import './select.scss';

const TAG_NAME = 'ui-select';

@customElement(TAG_NAME)
export class Select extends SelectInput {
@property({ type: String }) label = '';

protected render(): TemplateResult {
return html`
<div class="form-group">
<label>${this.label}</label>
${super.render()}
</div>
`;
}
}
4 changes: 4 additions & 0 deletions src/index.scss
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,7 @@ body {
margin: 0;
padding: 0;
}

:root {
--hover-bg-color: #efefef;
}
1 change: 1 addition & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,4 @@ export * from './components/loading/loading';
export * from './components/button/button';
export * from './components/indicator/indicator';
export * from './components/input/input';
export * from './components/select/select';

0 comments on commit a2fbe71

Please sign in to comment.