Aurelia动态创建自定义元素,无需撰写

问题描述:

我了解Aurelia自定义元素与<compose>的优缺点; Jeremy Danyow的blog post有帮助。但是,我想have my cake and eat it tooAurelia动态创建自定义元素,无需撰写

我想创建自定义元素,我也可以动态编写。因为<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>静态和动态。自定义元素绝对看起来是最好的方法。然而,我没有看到如何在不创建两个并行组件的情况下实现这一点 - 一个设计为自定义元素,另一个设计为可组合视图/视图模型。

+0

'无容器'*摇头和拍打LStarky在手腕上* maaaan多少次我得告诉你不要使用无容器,除非它是绝对必要的B/C的好理由(例如传统的CSS) –

+0

如果我不' t使用无容器,Bootstrap在''周围显示一个可见的框。我不认为它会影响绑定等。但无论如何,我真正的兴趣在于可以静态和动态实例化(并且与Aurelia验证一起工作)的构建良好的自定义控件。 – LStarky

+0

那么,你去了,你给了一个有理由使用'无容器':-) –

这对于解决方案如何?在我的解决方案中,两个控件基本上都是相同的,但是在一个真实的解决方案中,它们会有不同的行为,但这是一个很好的起点。

下面是一个例子: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; 
    } 
    } 
} 

文本框。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; 
    } 
    } 
} 
+0

我正在通过这个建议,基本上似乎是创建混合组件作为正常的自定义元素工作,但也可以通过撰写来激活。我正确地认为'activate(model)'只会在由''实例化时才被调用? – LStarky

+0

是的。这种策略的唯一真正限制是这些自定义元素不能也是路由器加载的页面。尽管通过检查第一个参数的类型来激活,也可以解决这个问题。 –

+0

@AshleyGrant对我的回答有什么想法? –

为了实现动态创建自定义元素,我已经实现了一个使用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>,尤其是因为自定义元素控件也可以在独立设置(静态实例化)中使用。

+0

这看起来效率很低,乍一看,IMO –

+0

是的,我同意。如何改进?从控制对象数组动态构建表单有什么更好的方法? – LStarky

+0

我会发表我的想法作为对这个问题的答案 –

还有另一种策略,不知道这是否是好是坏。您可以创建一个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。可能有更好的方法来做到这一点。

Runnable的例子https://gist.run/?id=827c72ec2062ec61adbfb0a72b4dac7d

你觉得呢?

+0

我在这里遇到的问题是这可能会导致内存泄漏,因为实际上没有API来拆卸“增强“元素。 –

+0

嗯......这非常重要。所以,在父元素分离之后,增强元素是否仍然在内存中?我们能做些什么来清洁它吗? –

+0

我记得看到一个关于我们没有在“增强”元素上调用“分离”的问题。你必须自己调用这个方法。我会看看我能否找到它。 –