使用 Aurelia 在视图中动态生成自定义组件

Dynamically generate custom component in view with Aurelia

我目前正在尝试为棋盘游戏构建 Web 应用程序,并决定在前端使用 Aurelia JavaScript 框架。我是 Aurelia 的新手,在尝试通过单击按钮创建自定义组件的新实例时遇到了 运行 麻烦。让我通过展示我想要完成的事情的例子来进一步解释。我尝试实现的游戏设置如下所示:

Game Setup Image

我整理了一个最简单的例子来进一步解释我遇到的问题。这更像是一个设计问题,源于我不熟悉 Aurelia 而不是其他任何事情。在这个最低限度的示例中,我有主 app.js 视图模型和 app.html 视图,Aurelia 将其视为主视图模型和视图。它们看起来像这样:

app.js

import { Card } from './card';

export class App {
    cards = [];

    newCard() {
        this.cards.push(new Card());
    }
}

app.html

<template>
    <require from="./card"></require>

    <button click.delegate="newCard()">New Card</button>

    <div>
        <li repeat.for="card of cards">
            <compose view-model="card"></compose>
        </li>
    </div>
</template>

然后我有一个卡片组件,它非常简单地表示一张扑克牌。这是它的视图模型和视图:

card.js

export class Card {
    cardValues = ['2','3','4','5','6','7','8','9','10',
        'J','Q','K','A'];
    cardSuits = ['Diamonds', 'Clubs', 'Hearts', 'Spades'];

    value;
    suit;

    activate() {
        this.value = this.pickRandomItem(this.cardValues);
        this.suit = this.pickRandomItem(this.cardSuits);
    }

    pickRandomItem(data) {
        let index = Math.floor(Math.random() * (data.length -1));
        return data[index];
    }
}

card.html

<template>
    <div style="border: 2px solid black;
                display: inline-block;
                margin-top: 10px;">
        <h3>Value: ${value}</h3>
        <h4>Suit: ${suit}</h4>
    </div>
</template>

目前,通过在应用视图模型的按钮单击事件处理程序中实例化一个新的 Card 对象,我能够在按下应用视图中的按钮时动态生成新卡片。我遇到的问题是我认为我不应该从应用程序视图模型中手动实例化 Card 对象。似乎应该有某种方式告诉 Aurelia 它需要创建一个新的 Card 对象,但我不知道那是什么方式。所以我的问题是:有没有更好的方法来动态创建自定义组件,不需要像我那样手动实例化它们?

为什么这看起来不是正确的方法,部分原因是因为在当前设置下,Card 对象的构造函数被调用两次,而构造函数只应被调用一次。此外,如果 Card class 需要依赖注入值,我将不得不手动将它们传递到新的 Card 对象中,这对我来说不合适。

非常感谢您的帮助!

这是最小工作回购的 link on GitHub

我认为方法可能在于在组合元素上使用模型 属性。模型 属性 允许您传递要在视图模型中使用的值对象,这些在 activate 方法中作为第一个参数可用。

所以你会像这样使用它:

<li repeat.for="cardObj of cards"> <compose view-model="card" model.bind="cardObj"></compose> </li>

所以卡片视图模型仍然存在,您移出生成逻辑并从卡片数组中传递卡片对象。这使事情变得更加清晰和简单,您不必每次都实例化这些沉重的对象。

在您的 app.js 文件中,您创建了一次新对象并调用了构造函数,因为那是 new 所做的。然后,一旦 compose 元素调用它,它就会再次实例化它。您正在实例化该对象两次。

而不是 new Card() 你要做的是将一个简单的对象推入你的卡片数组。将卡片生成逻辑移出 class 中,不会多次实例化。


编辑

在一些额外的反馈之后,我举了一个例子来说明它们是如何组合在一起的。如您所见,我们已将卡片创建逻辑移至一个函数中。我们调用这个函数来生成一个包含两个属性的对象:suit 和 value。然后我们将其作为数据传递给 compose 元素,然后在内部使用它。

app.js

export class App {
    cards = [];

    newCard() {
        this.cards.push(generateCard());
    }
}

function generateCard() {
    let cardValues = ['2','3','4','5','6','7','8','9','10',
        'J','Q','K','A'];

    let cardSuits = ['Diamonds', 'Clubs', 'Hearts', 'Spades'];

    function pickRandomItem(arr) {
        let index = Math.floor(Math.random() * (arr.length -1));
        return arr[index];
    }

    return {
        suit: pickRandomItem(cardSuits),
        value: pickRandomItem(cardValues)
    };
}

app.html

<template>
    <button click.delegate="newCard()">New Card</button>

    <div>
        <li repeat.for="cardObj of cards">
            <!-- cardObj will be our object {suit: 'suit', value: 'value'} -->
            <compose view-model="card" model.bind="cardObj"></compose>
        </li>
    </div>
</template>

card.js

export class Card {
    suit;
    value;

    activate(model) {
        if (model) {
            this.suit = model.suit;
            this.value = model.value;
        }
    }
}

您的 card.html 文件将保持不变。

另一种方法是将卡片 class 标记为瞬态并使用惰性解析器。请原谅ES6.

App.js

import { Card } from './card';
import {Lazy,inject} from 'aurelia-framework';

@inject(Lazy.of(Card))
export class App {
    constructor(cardFactory){
        this.cardFactory = cardFactory;
    }
    cards = [];

    newCard() {
        this.cards.push(this.cardFactory());
   }
}

Card.js

import {transient} from 'aurelia-framework';
@transient()
export class Card {
    //do stuff
}