Skip to content

Instantly share code, notes, and snippets.

@bigopon
Last active November 1, 2019 16:35
Show Gist options
  • Save bigopon/9dd32bc8a772526ae527f593e26b275b to your computer and use it in GitHub Desktop.
Save bigopon/9dd32bc8a772526ae527f593e26b275b to your computer and use it in GitHub Desktop.
Aurelia - bindable - inheritance demo with form components
<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>
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];
}
}
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() {
}
}
<!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>
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">&#9650;</i>',
'<i class="trigger-icon-spinner down-spinner">&#9660;</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();
}
}
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);
}
}
.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');
}
<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>
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;
}
}
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