2
votes

I am currently trying to build a web application for a board game and have decided to use the Aurelia JavaScript framework for the front-end. I am new to Aurelia and have run into trouble when trying to create a new instance of a custom component at the click of a button. Let me explain further by showing an example of what I am trying to accomplish. The setup for the game I am trying to implement looks as follows:

Game Setup Image

I have put together a bare-minimum example to further explain what I am having trouble with. It is more of a design problem that stems from me not being familiar with Aurelia than anything else. In this bare-minimum example I have the main app.js viewmodel and app.html view that Aurelia treats as the main viewmodel and view. They look like this:

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>

I then have a card component which very simply represents a playing card. Here is its viewmodel and view:

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>

Currently, I am able to dynamically generate new cards at the press of the button in the app view by instantiating a new Card object in the app viewmodel's button click event handler. The issue I have with this is that I do not believe I should have to instantiate the Card objects manually from the app viewmodel. It seems that there should be some way to tell Aurelia that it needs to create a new Card object, but I could not figure out what that way would be. So my question is this: Is there a better way to dynamically create custom components which does not require manually instantiating them as I have done?

Part of my reasoning for why this does not seem like the correct approach is because with this current setup, the constructor for a Card object is called twice when the constructor should only be called once. Also, if the Card class were to require dependency injected values, I would have to manually pass those into the new Card objects, and that does not feel right to me.

Thank you so much for your help!

Here's a link to the minimal working repo on GitHub

2
You should try to keep your question small and focused, so that others could easily understand the question and could reuse the answer for their own problem. Please spend some time to reduce your question to the smallest repro possible.Mikhail Shilkov
Okay, I'll rewrite it to be simpler and make a new bare-minimum Aurelia project and post that. I was caught up in thinking about everything in the context of my application, but that makes sense, thanks!KevinM
Looks like Splendor :)Miroslav Popovic
You are right @MiroslavPopovic, it is going to be a Splendor implementation :)KevinM

2 Answers

4
votes

I think the approach might lie in the use of the model property on the compose element. The model property allows you to pass through an object of values to be used inside of your view model, these are available on the activate method as the first argument.

So you would use it like this:

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

So the card viewmodel remains, you move out the generation logic and you are passing through the card object from your cards array. This makes things a whole lot more cleaner and simpler, you're not instantiating these heavy objects each time.

Inside of your app.js file, you're creating a new object once and the constructor is being called because that's what new does. Then once the compose element calls it, it instantiates it again. You're instantiating the object twice.

Instead of new Card() what you will do is push a simple object into your cards array. Move the card generation logic out in a class that doesn't get instantiated multiple times.


Edit

After some additional feedback, I have knocked up an example of how it could all fit together. As you can see, we have moved card creation logic into a function. We call this function to generate an object which contains two properties: suit and value. Then we pass this through to the compose element as data and then use it inside.

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;
        }
    }
}

Your card.html file will remain untouched.

0
votes

Another approach would be to mark the Card class as transient and use the Lazy resolver. Please excuse the 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
}