Aurelia 无需 compose 即可动态创建自定义元素
Aurelia dynamically created custom elements without compose
我了解 Aurelia 的自定义元素与 <compose>
的优缺点; Jeremy Danyow 的 blog post helps. But, I would like to have my cake and eat it too.
我想创建我也可以动态组合的自定义元素。由于 <compose>
需要不同的实例化,使用它意味着我需要为每个元素创建两个并行版本——一个用于 <compose>
,一个用于静态调用。例如,考虑以下用例:
<template>
<h1>Welcome to the Data Entry Screen</h1>
<!-- Static controls -->
<my-textbox label="Your name:" value.bind="entry_name"></my-textbox>
<my-datepicker label="Current date:" value.bind="entry_date"></my-datepicker>
<!-- Loop through dynamic form controls -->
<div class="form-group" repeat.for="control of controls" if.bind="control.type !== 'hidden'">
<label class="control-label">${control.label}</label>
<div>
<compose containerless class="form-control"
view-model="resources/elements/${control.type}/${control.type}"
model.bind="{'control': control, 'model': model, 'readonly': readonly}">
</compose>
</div>
</div>
</template>
具有以下控制数据:
controls = [
{label: 'Entry Date', type: 'my-datepicker', bind: 'acc_entry_date'},
{label: 'Code', type: 'my-textbox', bind: 'acc_entry_code'},
{label: 'Ref', type: 'my-textbox', bind: 'acc_entry_ref'},
{label: 'Description', type: 'my-textarea', rows: '3', bind: 'acc_entry_description'},
{label: 'Status', type: 'my-dropdown', bind: 'acc_entry_status', enum: 'AccountEntryStatus'},
{type: 'hidden', bind: 'acc_entry_period_id'}];
如您所见,我想静态和动态地使用 <my-textbox>
和 <my-datepicker>
。自定义元素绝对是最好的方法。但是,我不知道如何在不创建两个并行组件的情况下实现这一目标——一个设计为自定义元素,另一个设计为可组合元素 view/viewmodel.
为了实现自定义元素的动态创建,我实现了一个元自定义元素,它使用 if.bind
来动态实例化正确的自定义元素(下面的总体思路)。
元视图模型:
import {bindable} from 'aurelia-framework';
export class MyMetaElement {
@bindable control; // control definition object
@bindable model; // data for binding
@bindable readonly = false; // flag to make controls view-only
}
元视图:
<template>
<my-textbox if.bind="control.type == 'my-textbox" label.bind="control.label" value.bind="model[control.bind]" readonly.bind="readonly"></my-textbox>
<my-datepicker if.bind="control.type == 'my-datepicker" label.bind="control.label" value.bind="model[control.bind]" readonly.bind="readonly"></my-datepicker>
<my-textarea if.bind="control.type == 'my-textarea" label.bind="control.label" value.bind="model[control.bind]" rows.bind="control.rows" readonly.bind="readonly"></my-textarea>
<my-dropdown if.bind="control.type == 'my-dropdown" label.bind="control.label" value.bind="model[control.bind]" enum.bind="control.enum" readonly.bind="readonly"></my-dropdown>
</template>
虽然动态创建控件看起来需要做很多额外的工作,但它比 <compose>
的使用有很多优势,特别是因为自定义元素控件也可以在独立设置中使用 (静态实例化)。
这个解决方案怎么样?在我的解决方案中,两个控件基本上是相同的,但在实际解决方案中,它们会有不同的行为,但这是一个很好的起点。
举个例子:https://gist.run?id=e6e980a88d7e33aba130ef91f55df9dd
app.html
<template>
<require from="./text-box"></require>
<require from="./date-picker"></require>
<div>
Text Box
<text-box value.bind="text"></text-box>
</div>
<div>
Date Picker
<date-picker value.bind="date"></date-picker>
</div>
<button click.trigger="reset()">Reset controls</button>
<div>
Dynamic controls:
<div repeat.for="control of controls">
${control.label}
<compose view-model="./${control.type}" model.bind="control.model" ></compose>
<div>
control.model.value = ${control.model.value}
</div>
</div>
</div>
<button click.trigger="changeModelDotValueOnTextBox()">Change model.value on text box</button>
<button click.trigger="changeModelOnTextBox()">Change model.value on text box and then make a copy of the model</button>
</template>
app.js
export class App {
text = 'This is some text';
date = '2017-02-28';
controls = getDefaultControls();
reset() {
this.controls = getDefaultControls();
}
changeModelOnTextBox() {
this.controls[1].model = {
value: 'I changed the model to something else!'
};
}
changeModelDotValueOnTextBox() {
this.controls[1].model.value = 'I changed the model!';
}
}
function getDefaultControls(){
return[
{label: 'Entry Date', type: 'date-picker', model: { value: '2017-01-01' }},
{label: 'Code', type: 'text-box', model: { value: 'This is some other text'}}
];
}
日期-picker.html
<template>
<input type="date" value.bind="value" />
</template>
日期-picker.js
import { inject, bindable, bindingMode, TaskQueue } from 'aurelia-framework';
import { ObserverLocator } from 'aurelia-binding';
@inject(Element, TaskQueue, ObserverLocator)
export class DatePicker {
@bindable({ defaultBindingMode: bindingMode.twoWay }) value;
model = null;
observerSubscription = null;
constructor(el, taskQueue, observerLocator) {
this.el = el;
this.taskQueue = taskQueue;
this.observerLocator = observerLocator;
}
activate(model) {
if(this.observerSubscription) {
this.observerSubscription.dispose();
}
this.model = model;
this.observerSubscription = this.observerLocator.getObserver(this.model, 'value')
.subscribe(() => this.modelValueChanged());
this.hasModel = true;
this.modelValueChanged();
}
detached() {
if(this.observerSubscription) {
this.observerSubscription.dispose();
}
}
modelValueChanged() {
this.guard = true;
this.value = this.model.value;
this.taskQueue.queueMicroTask(() => this.guard = false)
}
valueChanged() {
if(this.guard == false && this.hasModel) {
this.model.value = this.value;
}
}
}
正文-box.html
<template>
<input type="text" value.bind="value" />
</template>
正文-box.js
import { inject, bindable, bindingMode, TaskQueue } from 'aurelia-framework';
import { ObserverLocator } from 'aurelia-binding';
@inject(Element, TaskQueue, ObserverLocator)
export class TextBox {
@bindable({ defaultBindingMode: bindingMode.twoWay }) value;
model = null;
observerSubscription = null;
constructor(el, taskQueue, observerLocator) {
this.el = el;
this.taskQueue = taskQueue;
this.observerLocator = observerLocator;
}
activate(model) {
if(this.observerSubscription) {
this.observerSubscription.dispose();
}
this.model = model;
this.observerSubscription = this.observerLocator.getObserver(this.model, 'value')
.subscribe(() => this.modelValueChanged());
this.hasModel = true;
this.modelValueChanged();
}
detached() {
if(this.observerSubscription) {
this.observerSubscription.dispose();
}
}
modelValueChanged() {
this.guard = true;
this.value = this.model.value;
this.taskQueue.queueMicroTask(() => this.guard = false)
}
valueChanged() {
if(this.guard == false && this.hasModel) {
this.model.value = this.value;
}
}
}
还有一个攻略,不知道好不好。您可以创建一个按您想要的方式运行的 custom-compose
。例如:
import {
bindable,
inlineView,
noView,
inject,
TemplatingEngine,
bindingMode } from 'aurelia-framework';
@noView
@inject(Element, TemplatingEngine)
export class DynamicElement {
@bindable type;
@bindable({ defaultBindingMode: bindingMode.twoWay }) model;
constructor(element, templatingEngine) {
this.element = element;
this.templatingEngine = templatingEngine;
}
bind(bindingContext, overrideContext) {
this.element.innerHTML = `<${this.type} value.bind="model"></${this.type}>`;
this.templatingEngine.enhance({ element: this.element, bindingContext: this });
}
detached() {
this.element.firstChild.remove();
this.view.detached();
this.view.unbind();
this.view = null;
}
}
用法:
<div repeat.for="control of controls">
${control.label}
<dynamic-element type.bind="control.type" model.bind="control.value"></dynamic-element>
<div>
control.value = ${control.value}
</div>
</div>
我对 bindingContext: this
不太满意。可能有更好的方法来做到这一点。
可运行示例https://gist.run/?id=827c72ec2062ec61adbfb0a72b4dac7d
你怎么看?
我了解 Aurelia 的自定义元素与 <compose>
的优缺点; Jeremy Danyow 的 blog post helps. But, I would like to have my cake and eat it too.
我想创建我也可以动态组合的自定义元素。由于 <compose>
需要不同的实例化,使用它意味着我需要为每个元素创建两个并行版本——一个用于 <compose>
,一个用于静态调用。例如,考虑以下用例:
<template>
<h1>Welcome to the Data Entry Screen</h1>
<!-- Static controls -->
<my-textbox label="Your name:" value.bind="entry_name"></my-textbox>
<my-datepicker label="Current date:" value.bind="entry_date"></my-datepicker>
<!-- Loop through dynamic form controls -->
<div class="form-group" repeat.for="control of controls" if.bind="control.type !== 'hidden'">
<label class="control-label">${control.label}</label>
<div>
<compose containerless class="form-control"
view-model="resources/elements/${control.type}/${control.type}"
model.bind="{'control': control, 'model': model, 'readonly': readonly}">
</compose>
</div>
</div>
</template>
具有以下控制数据:
controls = [
{label: 'Entry Date', type: 'my-datepicker', bind: 'acc_entry_date'},
{label: 'Code', type: 'my-textbox', bind: 'acc_entry_code'},
{label: 'Ref', type: 'my-textbox', bind: 'acc_entry_ref'},
{label: 'Description', type: 'my-textarea', rows: '3', bind: 'acc_entry_description'},
{label: 'Status', type: 'my-dropdown', bind: 'acc_entry_status', enum: 'AccountEntryStatus'},
{type: 'hidden', bind: 'acc_entry_period_id'}];
如您所见,我想静态和动态地使用 <my-textbox>
和 <my-datepicker>
。自定义元素绝对是最好的方法。但是,我不知道如何在不创建两个并行组件的情况下实现这一目标——一个设计为自定义元素,另一个设计为可组合元素 view/viewmodel.
为了实现自定义元素的动态创建,我实现了一个元自定义元素,它使用 if.bind
来动态实例化正确的自定义元素(下面的总体思路)。
元视图模型:
import {bindable} from 'aurelia-framework';
export class MyMetaElement {
@bindable control; // control definition object
@bindable model; // data for binding
@bindable readonly = false; // flag to make controls view-only
}
元视图:
<template>
<my-textbox if.bind="control.type == 'my-textbox" label.bind="control.label" value.bind="model[control.bind]" readonly.bind="readonly"></my-textbox>
<my-datepicker if.bind="control.type == 'my-datepicker" label.bind="control.label" value.bind="model[control.bind]" readonly.bind="readonly"></my-datepicker>
<my-textarea if.bind="control.type == 'my-textarea" label.bind="control.label" value.bind="model[control.bind]" rows.bind="control.rows" readonly.bind="readonly"></my-textarea>
<my-dropdown if.bind="control.type == 'my-dropdown" label.bind="control.label" value.bind="model[control.bind]" enum.bind="control.enum" readonly.bind="readonly"></my-dropdown>
</template>
虽然动态创建控件看起来需要做很多额外的工作,但它比 <compose>
的使用有很多优势,特别是因为自定义元素控件也可以在独立设置中使用 (静态实例化)。
这个解决方案怎么样?在我的解决方案中,两个控件基本上是相同的,但在实际解决方案中,它们会有不同的行为,但这是一个很好的起点。
举个例子:https://gist.run?id=e6e980a88d7e33aba130ef91f55df9dd
app.html
<template>
<require from="./text-box"></require>
<require from="./date-picker"></require>
<div>
Text Box
<text-box value.bind="text"></text-box>
</div>
<div>
Date Picker
<date-picker value.bind="date"></date-picker>
</div>
<button click.trigger="reset()">Reset controls</button>
<div>
Dynamic controls:
<div repeat.for="control of controls">
${control.label}
<compose view-model="./${control.type}" model.bind="control.model" ></compose>
<div>
control.model.value = ${control.model.value}
</div>
</div>
</div>
<button click.trigger="changeModelDotValueOnTextBox()">Change model.value on text box</button>
<button click.trigger="changeModelOnTextBox()">Change model.value on text box and then make a copy of the model</button>
</template>
app.js
export class App {
text = 'This is some text';
date = '2017-02-28';
controls = getDefaultControls();
reset() {
this.controls = getDefaultControls();
}
changeModelOnTextBox() {
this.controls[1].model = {
value: 'I changed the model to something else!'
};
}
changeModelDotValueOnTextBox() {
this.controls[1].model.value = 'I changed the model!';
}
}
function getDefaultControls(){
return[
{label: 'Entry Date', type: 'date-picker', model: { value: '2017-01-01' }},
{label: 'Code', type: 'text-box', model: { value: 'This is some other text'}}
];
}
日期-picker.html
<template>
<input type="date" value.bind="value" />
</template>
日期-picker.js
import { inject, bindable, bindingMode, TaskQueue } from 'aurelia-framework';
import { ObserverLocator } from 'aurelia-binding';
@inject(Element, TaskQueue, ObserverLocator)
export class DatePicker {
@bindable({ defaultBindingMode: bindingMode.twoWay }) value;
model = null;
observerSubscription = null;
constructor(el, taskQueue, observerLocator) {
this.el = el;
this.taskQueue = taskQueue;
this.observerLocator = observerLocator;
}
activate(model) {
if(this.observerSubscription) {
this.observerSubscription.dispose();
}
this.model = model;
this.observerSubscription = this.observerLocator.getObserver(this.model, 'value')
.subscribe(() => this.modelValueChanged());
this.hasModel = true;
this.modelValueChanged();
}
detached() {
if(this.observerSubscription) {
this.observerSubscription.dispose();
}
}
modelValueChanged() {
this.guard = true;
this.value = this.model.value;
this.taskQueue.queueMicroTask(() => this.guard = false)
}
valueChanged() {
if(this.guard == false && this.hasModel) {
this.model.value = this.value;
}
}
}
正文-box.html
<template>
<input type="text" value.bind="value" />
</template>
正文-box.js
import { inject, bindable, bindingMode, TaskQueue } from 'aurelia-framework';
import { ObserverLocator } from 'aurelia-binding';
@inject(Element, TaskQueue, ObserverLocator)
export class TextBox {
@bindable({ defaultBindingMode: bindingMode.twoWay }) value;
model = null;
observerSubscription = null;
constructor(el, taskQueue, observerLocator) {
this.el = el;
this.taskQueue = taskQueue;
this.observerLocator = observerLocator;
}
activate(model) {
if(this.observerSubscription) {
this.observerSubscription.dispose();
}
this.model = model;
this.observerSubscription = this.observerLocator.getObserver(this.model, 'value')
.subscribe(() => this.modelValueChanged());
this.hasModel = true;
this.modelValueChanged();
}
detached() {
if(this.observerSubscription) {
this.observerSubscription.dispose();
}
}
modelValueChanged() {
this.guard = true;
this.value = this.model.value;
this.taskQueue.queueMicroTask(() => this.guard = false)
}
valueChanged() {
if(this.guard == false && this.hasModel) {
this.model.value = this.value;
}
}
}
还有一个攻略,不知道好不好。您可以创建一个按您想要的方式运行的 custom-compose
。例如:
import {
bindable,
inlineView,
noView,
inject,
TemplatingEngine,
bindingMode } from 'aurelia-framework';
@noView
@inject(Element, TemplatingEngine)
export class DynamicElement {
@bindable type;
@bindable({ defaultBindingMode: bindingMode.twoWay }) model;
constructor(element, templatingEngine) {
this.element = element;
this.templatingEngine = templatingEngine;
}
bind(bindingContext, overrideContext) {
this.element.innerHTML = `<${this.type} value.bind="model"></${this.type}>`;
this.templatingEngine.enhance({ element: this.element, bindingContext: this });
}
detached() {
this.element.firstChild.remove();
this.view.detached();
this.view.unbind();
this.view = null;
}
}
用法:
<div repeat.for="control of controls">
${control.label}
<dynamic-element type.bind="control.type" model.bind="control.value"></dynamic-element>
<div>
control.value = ${control.value}
</div>
</div>
我对 bindingContext: this
不太满意。可能有更好的方法来做到这一点。
可运行示例https://gist.run/?id=827c72ec2062ec61adbfb0a72b4dac7d
你怎么看?