Last active
November 1, 2019 16:35
-
-
Save bigopon/9dd32bc8a772526ae527f593e26b275b to your computer and use it in GitHub Desktop.
Aurelia - bindable - inheritance demo with form components
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<template> | |
<require from='./number-field'></require> | |
<require from='./select-field'></require> | |
<number-field value.bind='age' label="Age:"></number-field> | |
<div> | |
Age is ${age} | |
</div> | |
<hr/> | |
<select-field | |
items.bind='countries' | |
value.bind='country' | |
multiselect></select-field> | |
<div> | |
Country is ${country.text} | |
</div> | |
<hr/> | |
Hint: wheel to select faster | |
</template> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
export class App { | |
message = 'Hello World' | |
message2 = 'Bonjour' | |
countries = [ | |
{ value: 'US', text: 'USA' }, | |
{ value: 'UK', text: 'UK' }, | |
{ value: 'AUS', text: 'Australia' }, | |
{ value: 'SWE', text: 'Sweden' } | |
] | |
created(_, view) { | |
window.app = this; | |
this.view = view; | |
} | |
attached() { | |
this.country = this.countries[0]; | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import {bindable, bindingMode, observable, customElement, inlineView} from 'aurelia-framework' | |
export class Field { | |
static deferUpdateValue(fieldInstance, newRawValue) { | |
fieldInstance.value = fieldInstance.processRawValue(newRawValue); | |
} | |
/** | |
* Width of this field | |
* @type {number} | |
*/ | |
@bindable width | |
/** | |
* Height of this field | |
* @type {number} | |
*/ | |
@bindable height | |
/** | |
* Label of this field | |
* @type {string} | |
*/ | |
@bindable label | |
/** | |
* Value of this field | |
* @type {any} | |
*/ | |
@bindable({ defaultBindingMode: bindingMode.twoWay }) value | |
/** | |
* Raw value of this field, used to bind internally | |
* @type {string} | |
*/ | |
@observable rawValue | |
/** | |
* Change handler to process raw value into desired type | |
* | |
* @private | |
*/ | |
rawValueChanged(newValue) { | |
if (this._updateValueTO) clearTimeout(this._updateValueTO); | |
this._updateValueTO = setTimeout(Field.deferUpdateValue, 50, this, newValue); | |
} | |
/** | |
* Process the value to assign to value for desired value/ type | |
* | |
* @protected | |
*/ | |
processRawValue(rawValue) { | |
return rawValue; | |
} | |
/** | |
* Resolve width and height of this field input wrap | |
* As width / height can accept either string / number | |
*/ | |
resolveSize(size) { | |
if (typeof size === 'string') { | |
return size; | |
} | |
if (typeof size === 'number') { | |
return size <= 1 ? (size * 100) + '%' : (size + 'px'); | |
} | |
return ''; | |
} | |
valueChanged() { | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<!doctype html> | |
<html> | |
<head> | |
<title>Aurelia</title> | |
<meta name="viewport" content="width=device-width, initial-scale=1"> | |
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/meyer-reset/2.0/reset.min.css"> | |
<style> | |
body { | |
padding: 20px; | |
} | |
.form-component { | |
display: block; | |
margin-bottom: 20px; | |
} | |
</style> | |
</head> | |
<body aurelia-app> | |
<h1>Loading...</h1> | |
<script src="https://jdanyow.github.io/rjs-bundle/node_modules/requirejs/require.js"></script> | |
<script src="https://jdanyow.github.io/rjs-bundle/config.js"></script> | |
<script src="https://jdanyow.github.io/rjs-bundle/bundles/aurelia.js"></script> | |
<script src="https://jdanyow.github.io/rjs-bundle/bundles/babel.js"></script> | |
<script> | |
require(['aurelia-bootstrapper']); | |
</script> | |
</body> | |
</html> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import {bindable, useView, inject, TaskQueue} from 'aurelia-framework'; | |
import {TriggerField} from './trigger-field' | |
@inject(Element, TaskQueue) | |
@useView('./trigger-field.html') | |
export class NumberField extends TriggerField { | |
@bindable type = 'number' | |
constructor(element, taskQueue) { | |
super(); | |
this.element = element; | |
this.taskQueue = taskQueue; | |
this._setupSpinButtons = false; | |
this.triggers = [ | |
{ iconCls: 'spinner-ct', handler: '_spin' } | |
]; | |
} | |
attached() { | |
const el = this.element | |
const spinner = el.querySelector('.trigger-icon-spinner-ct'); | |
spinner.innerHTML = [ | |
'<i class="trigger-icon-spinner up-spinner">▲</i>', | |
'<i class="trigger-icon-spinner down-spinner">▼</i>' | |
].join(''); | |
this.upSpinner = spinner.firstElementChild; | |
this.downSpinner = spinner.lastElementChild; | |
this.inputEl.addEventListener('wheel', this); | |
} | |
valueChanged() { | |
} | |
/** | |
* @override Field.prototype.proecssRawValue | |
*/ | |
processRawValue(rawValue) { | |
return Number(rawValue) || 0; | |
} | |
_spin(trigger, e) { | |
if (e.target === this.upSpinner) { | |
this.spinUp(); | |
} else { | |
this.spinDown(); | |
} | |
} | |
spin(delta) { | |
// Coerce to 0 to avoid NaN blowing up | |
this.rawValue = (this.value || 0) + (delta || 0); | |
} | |
spinUp() { | |
this.spin(1); | |
} | |
spinDown() { | |
this.spin(-1); | |
} | |
handleEvent(e) { | |
if (e.type === 'keydown') { | |
if (e.target === this.inputEl) { | |
} | |
} else if (e.type === 'wheel') { | |
if (e.target === this.inputEl) { | |
this.handleInputElMouseWheel(e); | |
} | |
} | |
} | |
/** | |
* @override TriggerField.prototype.handleInputKeyDown | |
*/ | |
handleInputKeydown(e) { | |
const UP = 38; | |
const DOWN = 40; | |
const code = e.keyCode; | |
if (code === UP || code === DOWN) { | |
if (e.keyCode === UP) { | |
this.spin(-1); | |
} else if (e.keyCode === DOWN) { | |
this.spin(-1); | |
} | |
return false; | |
} else { | |
return true; | |
} | |
} | |
handleInputElMouseWheel(e) { | |
const delta = -e.deltaY; | |
this.spin(Math.ceil(delta / 100)); | |
this.taskQueue.flushMicroTaskQueue(); | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import {observable, bindable, useView, inject, TemplatingEngine, TaskQueue} from 'aurelia-framework'; | |
import {TriggerField} from './trigger-field' | |
import {isTrue} from './util' | |
@inject(Element, TemplatingEngine, TaskQueue) | |
@useView('./trigger-field.html') | |
export class SelectField extends TriggerField { | |
@bindable valueField = 'value' | |
@bindable displayField = 'text' | |
/** | |
* Items for select option | |
*/ | |
@bindable items | |
@bindable multiselect = false; | |
@bindable expanded = false; | |
@observable isExpanded = false; | |
constructor(element, templatingEngine, taskQueue) { | |
super(); | |
this.element = element; | |
this.templatingEngine = templatingEngine; | |
this.taskQueue = taskQueue; | |
this.triggers = [ | |
{ iconCls: 'arrow-down', handler: 'toggleExpand' } | |
]; | |
} | |
created(_, view) { | |
console.clear(); | |
this.view = view; | |
} | |
bind(bC, oBC) { | |
if (this._listView) { | |
this._listView.bind(bC, oBC); | |
} | |
} | |
attached() { | |
const { view, _listView, templatingEngine } = this; | |
if (!_listView) { | |
const enhanceListViewInstruction = { | |
container: view.container, | |
element: this._createListView(), | |
resource: view.resources, | |
bindingContext: this, | |
overrideContext: view.overrideContext | |
} | |
this._listView = templatingEngine.enhance(enhanceListViewInstruction); | |
} else { | |
_listView.appendNodesTo(document.body); | |
_listView.attached(); | |
} | |
this.element.addEventListener('wheel', this); | |
this._listViewEl.addEventListener('wheel', this); | |
} | |
detached() { | |
this._listView.detached(); | |
this._listView.removeNodes(); | |
this.element.removeEventListener('wheel', this); | |
this._listViewEl.removeEventListener('wheel', this); | |
} | |
unbind() { | |
if (this._listView) { | |
this._listView.unbind(); | |
} | |
} | |
rawValueChanged(newRawValue) { | |
if (this._ignoreUpdate) { | |
return; | |
} | |
super.rawValueChanged(newRawValue); | |
} | |
/** | |
* @override Field.prototype.processRawValue() | |
* | |
* As value in select is determined differently compared to normal field | |
*/ | |
processRawValue(rawValue) { | |
if (!this.items || !rawValue) { | |
return this.value; | |
} | |
const value = this.items | |
.find(i => i[this.displayField] === rawValue | |
|| i[this.valueField] === rawValue | |
); | |
return value; | |
} | |
/** | |
* Used to convert value to raw value (string) to represent in input | |
*/ | |
toRawValue(value) { | |
if (!value) { | |
return ''; | |
} | |
return value[this.displayField]; | |
} | |
valueChanged(value) { | |
this._ignoreUpdate = true; | |
this.rawValue = this.toRawValue(value); | |
const cursor = this.inputEl.selectionStart; | |
this.taskQueue.queueMicroTask(() => { | |
this.inputEl.setSelectionRange(cursor, this.rawValue ? this.rawValue.length : cursor); | |
}); | |
this._ignoreUpdate = false; | |
} | |
/** | |
* Override this to have your own template | |
* | |
* And use together with replacable/part/replace-part | |
*/ | |
_createListView() { | |
const parser = document.createElement('div'); | |
parser.innerHTML = [ | |
'<ul class="select-field-list-wrap" ', | |
'ref="_listViewEl" ', | |
'show.bind="expanded" ', | |
'css="top: ${_listTop}px; left: ${_listLeft}px; width: ${_listWidth}px;" ', | |
'mousedown.trigger="handleListMousedown()" ', | |
'tabindex="-1">', | |
'<li repeat.for="item of items" ', | |
'class="select-field-list-item ', | |
'${item === value ? \'selected\' : \'\' }" ', | |
'tabindex="-1" ', | |
'click.trigger="handleItemSelect(item)" ', | |
'', | |
'>${item[displayField]}</li>', | |
'</ul>' | |
].join(''); | |
return document.body.appendChild(parser.firstChild); | |
} | |
expandedChanged(expanded) { | |
this.isExpanded = expanded; | |
} | |
toggleExpand() { | |
this.expanded = this.isExpanded = !this.isExpanded; | |
} | |
isExpandedChanged(expanded) { | |
if (expanded) { | |
this.expand(); | |
} else { | |
this.collapse(); | |
} | |
} | |
expand() { | |
const rect = this.inputEl.parentNode.getBoundingClientRect(); | |
this._listTop = rect.top + rect.height; | |
this._listLeft = rect.left; | |
this._listWidth = rect.width; | |
this.inputEl.focus(); | |
} | |
collapse() { | |
// this.expanded = false; | |
} | |
moveSelectedValue(delta) { | |
const currentIndex = this.value ? this.items.findIndex(i => i === this.value) : -1 | |
let nextIndex = currentIndex + delta; | |
if (nextIndex > this.items.length - 1) { | |
nextIndex = 0; | |
} else if (nextIndex < 0) { | |
nextIndex = this.items.length - 1; | |
} | |
this.value = this.items[nextIndex]; | |
} | |
handleEvent(e) { | |
if (e.type === 'wheel') { | |
this.handleMouseWheel(e); | |
} | |
} | |
handleInputClick(e) { | |
if (document.activeElement === this.inputEl) { | |
this.expanded = true; | |
} | |
return true; | |
} | |
handleInputKeydown(e) { | |
const UP = 38; | |
const DOWN = 40; | |
const ESC = 27; | |
const code = e.keyCode; | |
if (code === ESC && this.expanded) { | |
this.expanded = false; | |
return false; | |
} | |
if (code === UP || code === DOWN) { | |
if (e.keyCode === UP) { | |
this.moveSelectedValue(-1); | |
} else if (e.keyCode === DOWN) { | |
this.moveSelectedValue(1); | |
} | |
return false; | |
} else { | |
return true; | |
} | |
} | |
handleInputFocus() { | |
this.expanded = true; | |
} | |
handleInputBlur() { | |
if (this._mousedownList) { | |
return true; | |
} else { | |
this.expanded = false; | |
} | |
} | |
handleListMousedown() { | |
this._mousedownList = true; | |
const mouseup = () => { | |
this._mousedownList = false; | |
document.removeEventListener('mouseup', mouseup); | |
}; | |
document.addEventListener('mouseup', mouseup); | |
} | |
handleItemSelect(item) { | |
this.value = item; | |
} | |
handleMouseWheel(e) { | |
if (!this.items) { | |
return; | |
} | |
const delta = Math.ceil(e.deltaY / 100); | |
this.moveSelectedValue(delta); | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
.field-wrap { | |
display: block; | |
} | |
.field-wrap, .field-wrap * { | |
box-sizing: border-box; | |
} | |
.field-input-wrap { | |
display: inline-flex; | |
width: 100%; | |
height: 22px; | |
flex-direction: row; | |
} | |
.field-input { | |
display: inline-block; | |
padding: 2px; | |
margin: 0; | |
flex: 1 1 auto; | |
} | |
.trigger-wrap { | |
display: inline-flex; | |
flex: 1 1 auto; | |
align-items: stretch; | |
justify-items: stretch; | |
} | |
.trigger-icon { | |
display: inline-block; | |
min-width: 20px; | |
margin: 0; | |
padding: 0; | |
border: 1px solid #a0a0a0; | |
background-position: center center; | |
background-size: 16px 16px; | |
background-repeat: no-repeat; | |
} | |
.trigger-icon:hover { | |
background-color: #e0e0e0; | |
} | |
.trigger-icon-spinner-ct { | |
display: flex; | |
flex-direction: column; | |
align-items: stretch; | |
justify-items: stretch; | |
} | |
.trigger-icon-spinner { | |
display: block; | |
flex: 1 0 auto; | |
font-size: 1vh; | |
} | |
.trigger-icon-spinner:hover { | |
background-color: lightblue; | |
cursor: pointer; | |
outline: 1px solid blue; | |
} | |
/** | |
* Select field css | |
*/ | |
.select-field-list-wrap { | |
position: absolute; | |
padding: 0; | |
margin: 0; | |
} | |
.select-field-list-item { | |
background-color: #f2f2f2; | |
border: 1px dotted #d0d0d0; | |
} | |
.select-field-list-item:hover { | |
background-color: #cecece; | |
border-color: #b0b0b0; | |
cursor: pointer; | |
} | |
.select-field-list-item.selected { | |
background-color: #909090; | |
} | |
.trigger-icon-arrow-down { | |
background-image: url('https://image.flaticon.com/icons/png/512/60/60995.png'); | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<template class="field-wrap"> | |
<require from="./trigger-field.css"></require> | |
<span>${label}</span> | |
<div | |
class="field-input-wrap" | |
css="width: ${resolveSize(width)}; height: ${resolveSize(height)}"> | |
<input | |
class="field-input" | |
type="text" | |
value.bind="rawValue" | |
ref="inputEl" | |
click.trigger="handleInputClick($event)" | |
focus.trigger="handleInputFocus($event)" | |
blur.trigger="handleInputBlur($event)" | |
keydown.delegate="handleInputKeydown($event)" /> | |
<div class="trigger-wrap"> | |
<button | |
repeat.for="trigger of triggers" | |
type="button" | |
class="trigger-icon ${trigger.iconCls ? 'trigger-icon-' + trigger.iconCls : '' }" | |
css="background-image: ${trigger.icon ? 'url(' + trigger.icon + ')' : ''};" | |
click.delegate="handleTriggerClick(trigger, $event)" | |
></button> | |
</div> | |
</div> | |
</template> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import {bindable} from 'aurelia-framework'; | |
import {Field} from './field'; | |
export class TriggerField extends Field { | |
@bindable triggers = []; | |
valueChanged(value) { | |
super.valueChange(value) | |
} | |
handleTriggerClick(trigger, e) { | |
const handler = trigger.handler | |
const fn = typeof handler === 'function' ? handler : this[handler]; | |
if (!fn) { | |
throw new Error('No handler'); | |
} | |
fn.call(trigger.scope || this, trigger, e); | |
} | |
handleInputFocus(e) { | |
return true; | |
} | |
handleInputBlur(e) { | |
return true; | |
} | |
handleInputKeydown(e) { | |
return true; | |
} | |
handleInputClick(e) { | |
return true; | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
export function isTrue(value) { | |
return value === '' || value === true; | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment