2
votes

Angular2/Typescript - Parent/Child Directive(?)

I'm new to, and very much still learning, Angular2/Typescript/javascript. As a result, I'm not entirely sure how to title my question. The basis of my app is a card game. The premise (relative to my struggle) is that the game has 2 players and each player has a hand of 5 cards. I have API calls to build/return the hand of cards.

In my app.component template, I have 2 div blocks; one for each players' hand of cards. Currently, I have it working by building two distinct arrays of cards (named p1cards and p2cards). Here is the relative code for that:

<div class="player1Cards" id="player1Cards">
  <ul class="list-group">
    <div draggable *ngFor="let card of p1cards" [dragData]="card" 
class="list-group-item">
      <img src="{{cardBluePath + card.fileName}}">
    </div>
  </ul>
</div>

<div class="player2Cards" id="player2Cards">
  <ul class="list-group">
    <div draggable *ngFor="let card of p2cards" [dragData]="card" 
class="list-group-item">
      <img src="{{cardBluePath + card.fileName}}">
    </div>
  </ul>
</div>

And here is the actual export class of the entire AppComponent:

@Injectable()
export class AppComponent implements OnInit
{
  @ViewChild(ModalComponent) errorMsg: ModalComponent;

  errorMessage: string;
  gameBoard: GameBoard[];
  name: {};
  mode = 'Observable';

  //we need a gameboard (maybe not)

  //we need an array of players
  player:Player;
  players:Player[] = [];

  p1cards:Card[] = [];
  p2cards:Card[] = [];
  droppedItems = [];

  //This tells us where the card images can be found
  cardBluePath = "/assets/deck/Blue/";
  cardRedPath = "/assets/deck/Red/";

  //The boardService will handle our API calls
  boardService;

  //Initialize the API service
  constructor(boardService:BoardService) {
    this.boardService = boardService;
  }


  //On load...
  ngOnInit()
  {
    //Create the game
    this.boardService.createGame()
        .subscribe(
         error =>  this.errorMessage = <any>error);

   //Create the players
   this.createPlayer(0);
   this.createPlayer(1);
 }


  createPlayer(player: number)
  {
    var playerName;
    if (player == 0) {playerName = "Player1"} else {playerName = "Player2"};

    //We'll make a call to the API to build the hand of cards
    this.boardService.buildHand(player)
        .subscribe(
         cardList =>
         {
           var cardData = [];
           cardData = JSON.parse(cardList.toString());

           var i, itemLength, card
           itemLength = cardData.length;
           for(i=0;i<itemLength;i++)
           {
             let card = new Card();
             Object.assign(card,
             {
               "cardNum":i,
               "id": cardData[i].id,
               "displayName": cardData[i].displayName,
               "fileName": cardData[i].fileName,
               "left": cardData[i].left,
               "top": cardData[i].top,
               "right": cardData[i].right,
               "bottom": cardData[i].bottom,
               "level": cardData[i].level,
               "native": cardData[i].native
             });

             if (player == 0) {this.p1cards.push(card)} else {this.p2cards.push(card)};
             ////this.cards.push(card);
           }

           //Now we will create the player and feed it the hand
           this.player = new Player(playerName);
           if (player ==0) {this.player.cardHand = this.p1cards} else {this.player.cardHand = this.p2cards};
           this.players.push(this.player);
         }
       );
  }


  //When a card is dropped...
  onItemDrop(e: any, slot: any)
  {
      e.dragData.slot = slot;

      //Update the object
      this.boardService.playCard(slot, e.dragData.card)
          .subscribe(result => {
            //If the slot is open and the card is played, physically move the item
            if (result == "true" )
            {
              this.droppedItems.push(e.dragData);
              this.removeItem(e.dragData, this.p1cards);
            }
            else{
              window.alert("Slot already occupied.");
              //this.modalWindow.show()
              //this.errorMsg.showErrorMessage("Slot already occupied.");
              //this.errorMsg.show();
            }
          });
  }


  //Remove the card from the hand
  removeItem(item: any, list: Array<any>)
  {
      let index = list.map((e) => {
          return e.cardNum
      }).indexOf(item.cardNum);
      list.splice(index, 1);
  }
}

The createPlayer function is really where the question begins. Currently, it will make the API call and parse the JSON back into an array of cards. Right now, the array of cards lives locally in the AppComponent (as p1cards or p2cards).

What I want to do instead is create a player objects (component) for each player, assign their respective hand of cards, and then put those players in an array. I had that part working (pieces of the code still exist above, but not all of it), but I hit a wall in my *ngFor to display the cards. In pseudocode, I understood what I needed to do, but in practice I couldn't figure it out.

I knew that div class player1Cards needed to be something like "let player of Players where name = player1", and then I needed to iterate over the player.cardHand[] array to display each of the cards. I tried quite a few things, but nothing worked.

So then, after a few hours of Google searching, I came to the conclusion that I needed a child view for the player to handle it. I currently have the following for that:

My player.html is:

<div draggable *ngFor="let card of cardHand" [dragData]="card" class="list-group-item">
  <img src="{{cardBluePath + card.fileName}}">
</div>

And my player.ts is:

import { Component, Input, OnInit } from '@angular/core';
import { Card } from './card';


@Component({
    selector: 'player',
    templateUrl: './player.html',
})

export class Player implements OnInit
{
  public cardHand: Card[];
  cardBluePath = "/assets/deck/Blue/";

  constructor
  (
    public name: string
  )
  {}

  ngOnInit()
  {

  }
}

Then in my AppComponent template, I added the block (and Imported the player.ts)

I get an error message on App.Component "inline template:69:16 caused by: No provider for String!". Of all the Google research I performed and all of the changes I tried (ViewChild, Input/Output, Reference), I could not get it to work. I don't recall exactly what I did, but at one point I was able to eliminate the error, but the card array was not getting passed to the player (I wish I had committed or stashed that code).

In my mind, I understand the task at hand, I just can't make it happen. I know I need to create the Player object and feed it the respective cardHand in order for the player html to be able to parse it. I can do that fine in AppComponent, but once I try to do it as a parent/child, I get stuck.

Can someone help get me going in the right direction?

2

2 Answers

0
votes

in your player component, if you want to access another component: 1. that component needs a import statement on top 2. within the @component section, you need to include it in Providers 3. also include it in the constructor

For more information, visit here: https://angular.io/guide/dependency-injection

0
votes

I know I need to create the Player object and feed it the respective cardHand in order for the player html to be able to parse it. I can do that fine in AppComponent, but once I try to do it as a parent/child, I get stuck.

I agree that it makes sense to create an array of player objects, each with an array of cards in their hand. Something like this:

let player1 = {name:'Player 1',hand:[]} 
let player2 = {name:'Player 2',hand:[]}
this.players = [player1, player2]

player1.hand.push(this.dealCard())
...

player2.hand.push(this.dealCard())
...

You can then create a player component to show the players (and even a card component to show their hand). In your root template you'll loop through the players, creating your player component and passing in the player data, including their hands.

<player-component *ngFor="let player of players" [player]="player"></player-component>

Make sure your player component has an input to receive the player data:

export class PlayerComponent implements OnInit {
 @Input() player: Player;
  constructor() { }

  ngOnInit() { }
}

Then in the <player-component> template loop through the player's hand and render the cards:

<p>I am {{player.name}}. My hand is:</p>
<ul>
  <li *ngFor="let card of player.hand">{{card}}</li>
</ul>

Here is a plunker showing a working demo that's a simplified version of this setup: https://plnkr.co/edit/5Hz8P7poCb9Ju5IR6MWs?p=preview You should be able to configure it to the specific setup of your game. Good luck!