0

I am trying to build an idle rpg game using Angular. I have a console log displaying events like Damage Dealt / Experience earned.

I have a service call MessageService where I have an array property of type Message (text, date, type).

import { Message } from "@core/models/message";
import { Injectable } from "@angular/core";
import { MESSAGE } from "@core/constant/constant";

@Injectable({
    providedIn: "root"
})
export class MessageService {
    messages: Message[] = [];

    private add(message: String, type: string) {
        this.messages.push(new Message(message, type));
    }
    addGeneralMessage(message: String) {
        this.add(message, MESSAGE.GENERAL);
    }
    addCombatMessage(message: String) {
        this.add(message, MESSAGE.COMBAT);
    }
    clear() {
        this.messages = [];
    }
    constructor() {}
}

I have buttons over my console log allowing user to "filter" all the message to only get specific type (Combat / General / System).

I can filter using : messages.filter(message => message.type == type), what i can't do is keep getting new message of the type selected.

import { Message } from "@core/models";
import { MESSAGE } from "@core/constant/constant";
import { MessageService } from "@core/services";
import { Component, OnInit } from "@angular/core";
@Component({
    selector: "app-message",
    templateUrl: "./message.component.html",
    styleUrls: ["./message.component.scss"]
})
export class MessageComponent implements OnInit {
    messages: Message[];
    constructor(public messageService: MessageService) {}

    ngOnInit() {
        this.messages = this.messageService.messages;
    }

    filterByType(type: String) {
        if (type == MESSAGE.ALL) {
            this.messages = this.messageService.messages;
        } else {
            this.messages = this.messageService.messages.filter(
                item => item.type == type
            );
        }
    }
}

Any idea ? I tried using observable without success, I think I might not have implement it well.

EDIT : My message component look like that :

<div class="log">
    <app-message-button-menu
        (filter)="filterByType($event)"
    ></app-message-button-menu>
    <app-message-chat [messages]="messages"></app-message-chat>
</div>

my app-message-button-menu like that :

<div class="menuLog">
    <app-message-button
        [text]="'All'"
        [type]="MESSAGE.ALL"
        [active]="activeButton == MESSAGE.ALL"
        (messageType)="onFilter($event)"
    ></app-message-button>
    <app-message-button
        [text]="'General'"
        [type]="MESSAGE.GENERAL"
        [active]="activeButton == MESSAGE.GENERAL"
        (messageType)="onFilter($event)"
    ></app-message-button>
    <app-message-button
        [text]="'Fight'"
        [type]="MESSAGE.COMBAT"
        [active]="activeButton == MESSAGE.COMBAT"
        (messageType)="onFilter($event)"
    ></app-message-button>
    <app-message-button
        [text]="'System'"
        [type]="MESSAGE.SYSTEM"
        [active]="activeButton == MESSAGE.SYSTEM"
        (messageType)="onFilter($event)"
    ></app-message-button>
</div>

import { Component, OnInit, Output, EventEmitter, Input } from "@angular/core";

@Component({
    selector: "app-message-button",
    templateUrl: "./message-button.component.html",
    styleUrls: ["./message-button.component.scss"]
})
export class MessageButtonComponent implements OnInit {
    @Input() type: String;
    @Input() text: String;
    @Input() active: boolean;
    @Output() messageType = new EventEmitter<String>();
    constructor() {}

    ngOnInit() {}

    filter() {
        this.messageType.emit(this.type);
    }
}
import { Component, OnInit, Output, EventEmitter, Input } from "@angular/core";

@Component({
    selector: "app-message-button",
    templateUrl: "./message-button.component.html",
    styleUrls: ["./message-button.component.scss"]
})
export class MessageButtonComponent implements OnInit {
    @Input() type: String;
    @Input() text: String;
    @Input() active: boolean;
    @Output() messageType = new EventEmitter<String>();
    constructor() {}

    ngOnInit() {}

    filter() {
        this.messageType.emit(this.type);
    }
}

my app-message-button like that :

<button [ngClass]="{ active: active == true }" (click)="filter()" type="button">
    {{ text }}
</button>

import { Component, OnInit, Output, EventEmitter, Input } from "@angular/core";
import { MESSAGE } from "@core/constant/constant";
@Component({
    selector: "app-message-button-menu",
    templateUrl: "./message-button-menu.component.html",
    styleUrls: ["./message-button-menu.component.scss"]
})
export class MessageButtonMenuComponent implements OnInit {
    MESSAGE;
    activeButton: String;
    @Output() filter = new EventEmitter<String>();
    constructor() {}

    ngOnInit(): void {
        this.MESSAGE = MESSAGE;
        this.activeButton = MESSAGE.ALL;
    }
    onFilter(type: String) {
        this.activeButton = type;
        this.filter.emit(type);
    }
}

and here is my app-message-chat :

<ul>
    <app-message-item
        *ngFor="let message of messages; trackBy: trackBy"
        [message]="message"
    ></app-message-item>
</ul>

import { Component, OnInit, Input } from "@angular/core";
import { Message } from "@core/models/message";
@Component({
    selector: "app-message-chat",
    templateUrl: "./message-chat.component.html",
    styleUrls: ["./message-chat.component.scss"]
})
export class MessageChatComponent implements OnInit {
    @Input("messages") messages: Message[];

    constructor() {}

    ngOnInit(): void {}

    trackBy(index: number, item: Message): Message {
        return item;
    }
}

EDIT Ling Vu Answer work :

import { Message } from "@core/models/message";
import { Injectable } from "@angular/core";
import { MESSAGE } from "@core/constant/constant";
import { ReplaySubject } from "rxjs";

@Injectable({
    providedIn: "root"
})
export class MessageService {
    messages: Message[] = [];
    filteredMessages: ReplaySubject<Message[]> = new ReplaySubject(1);
    filter: String;

    private add(message: String, type: string) {
        this.messages.push(new Message(message, type));
        this.filterMessages();
    }
    addGeneralMessage(message: String) {
        this.add(message, MESSAGE.GENERAL);
    }
    addCombatMessage(message: String) {
        this.add(message, MESSAGE.COMBAT);
    }
    clear() {
        this.messages = [];
    }
    setFilter(filter: String) {
        this.filter = filter;
    }
    filterMessages() {
        if (!this.filteredMessages)
            this.filteredMessages = new ReplaySubject(1);
        if (this.filter === MESSAGE.ALL) {
            this.filteredMessages.next(this.messages);
        } else {
            this.filteredMessages.next(
                this.messages.filter(item => item.type === this.filter)
            );
        }
    }
    constructor() {}
}

my message component :

export class MessageComponent implements OnInit {
    messages: Message[];

    constructor(public messageService: MessageService) {}

    ngOnInit() {
        this.messageService.setFilter(MESSAGE.ALL);
        this.messageService.filteredMessages.subscribe(
            messages => (this.messages = messages)
        );
    }
    filterByType(type: String) {
        this.messageService.setFilter(type);

        if (type === MESSAGE.ALL) {
            this.messages = this.messageService.messages;
        } else {
            this.messages = this.messageService.messages.filter(
                messages => messages.type === type
            );
        }
    }
}

sadly I didn't find how to implement Observable property in my component as he told me. I will find some lessons

Thank you Ling Vu

  • 4
    Please, consider pasting your code directly into the question instead of using images. – vicpermir Mar 03 '20 at 08:16
  • 4
    Don't post screenshots, instead you should provide minimal example – Ling Vu Mar 03 '20 at 08:16
  • 1. This is not the Observable pattern 2. What do you want to achieve? – Ling Vu Mar 03 '20 at 08:20
  • 3. There is no type `selected`. – Ling Vu Mar 03 '20 at 08:21
  • When i used Observable pattern, my site was blank without error. I rollback to an array. What I am trying to achieve is a Combat Log system that the user can filter. By default the player will be on "All" category, if he only want to see "combat" I want him to get new messages in this category. Right now if he select "combat" the messages are not refresh. – Steven Jeanne Mar 03 '20 at 08:22
  • @LingVu I do not understand your answer. I know which type of message is selected when the user clicks on the specific button. – Steven Jeanne Mar 03 '20 at 08:27
  • `import { MessageService } from "@core/services";` isn't this supposed to be something like: `import {MessageService} from` **"../services/messageService"**? – Elias Mar 03 '20 at 08:39
  • @Elias "@core" is a shortcut. In my tsconfig.json I wrote : "paths": { "@env": ["environments/environment"], "@shared/*": ["app/shared/*"], "@core/*": ["app/core/*"] }, Service is working as intended. I will try implementing observable again. – Steven Jeanne Mar 03 '20 at 08:44
  • 1
    I'll have a go at it as well :) – Elias Mar 03 '20 at 08:49
  • Where do you provide the service? Just in this component? How does the template look like? You also never add new messages – Ling Vu Mar 03 '20 at 08:54
  • @LingVu I think what he means is that he cannot filter the messages with the selected type... hopefully :D – Elias Mar 03 '20 at 08:55
  • I don't know what's the issue here. From what I see here, it should work. change detection should start when the component attributes change. – Ling Vu Mar 03 '20 at 08:56
  • When you call `filterByType` it should update. Otherwise you don't provide the same instance of the service. – Ling Vu Mar 03 '20 at 08:57
  • @LingVu if I had a type selected how could I use it to get messages of this type instead of all my array when a new messages is push into it ? Can i do a custom pipe to filter on my ngFor ? I believe it is not a best practice. – Steven Jeanne Mar 03 '20 at 08:57
  • @LingVu I provide the service in "root". GameplayService use MessageService in order to call addMessage() function. Right now when a new message is added my combat log is refresh with the new message. My services work as I want. What doesn't work is : when i click on a button to "only" get messages of type "combat" for example, new messages added in my array are not added in the combat log filtered. I do believe it is because I'm coding it badly. – Steven Jeanne Mar 03 '20 at 09:01
  • How does your template look like, can you edit your post please – Ling Vu Mar 03 '20 at 09:01
  • How about `MessageComponent implements OnChange` and then `ngOnChange() {this.messages = this.messageService.messages.filter(_ => _.type === this.selectedType)}` – Elias Mar 03 '20 at 09:03
  • Havent written angular in a long time but basically you need to re-apply the filter everything the dropdown or whatever changes – Elias Mar 03 '20 at 09:04
  • Ah! nvm. It's more like: `onTypeChange() {this.messages = this.messageService.messages.filter(_ => _.type === this.selectedType)}` and then you need to bind this function in the change listener of the component. – Elias Mar 03 '20 at 09:05
  • @Elias I think that's what I am looking for yeah, OnChange event in my component will trigger when messageService.messages change ? – Steven Jeanne Mar 03 '20 at 09:09
  • Actually, what you just added should work – Elias Mar 03 '20 at 09:09
  • @Elias What i just added is my current code the one not working :) – Steven Jeanne Mar 03 '20 at 09:14
  • Correct, but glancing over it, it **should** work xD – Elias Mar 03 '20 at 09:15
  • When i do : this.messages = this.messageService.messages; I get new messages. When i do : this.messages = this.messageService.messages.filter( item => item.type == type ); I don't get new messages. Array is not refresh. If i came back on all and call : this.messages = this.messageService.messages; I get new messages. I really think my problem is notifying my app that new messages where added while filter was apply. I will probably do a Map and display the right key but it seems overkill – Steven Jeanne Mar 03 '20 at 09:15

2 Answers2

1

Apply an observable like that:

MessageService

import { Message } from "@core/models/message";
import { Injectable } from "@angular/core";
import { MESSAGE } from "@core/constant/constant";

@Injectable({
    providedIn: "root"
})
export class MessageService {
    messages: Message[] = [];
    filteredMessages: ReplaySubject<Message[]>;
    filter: string;

    private add(message: String, type: string) {
        this.messages.push(new Message(message, type));
        this.filterMessages();
    }
    addGeneralMessage(message: String) {
        this.add(message, MESSAGE.GENERAL);
    }
    addCombatMessage(message: String) {
        this.add(message, MESSAGE.COMBAT);
    }
    clear() {
        this.messages = [];
    }

    setFilter(filter: string) {
       this.filter = filter
    }

    filterMessages() {
        if (!filteredMessages) filteredMessages = new ReplaySubject(1);

        this.filteredMessages.next(this.messageService.messages.filter(
             item => item.type === this.filter
        ));
    }
    constructor() {}
}

and subscribe to it in the component. Your attribute in the component needs to be a Observable<Message[]>. After that you can use it with an *ngIf and the async pipe

Ling Vu
  • 4,740
  • 5
  • 24
  • 45
0

So first of all, I had to focus on react lately and am not able to provide examples that follow best practices.

That said, the example below should give you a working app. Oh and one more thing, use the type string and not String Typescript: difference between String and string.

Message service:

import {Injectable} from '@angular/core';
import {Message} from './message';
import {MESSAGE_TYPE} from './messageType'

@Injectable()
export class MessageService {
  public messages: Message[] = [
    {message: 'general', type: MESSAGE_TYPE.GENERAL},
    {message: 'general1', type: MESSAGE_TYPE.GENERAL},
    {message: 'general2', type: MESSAGE_TYPE.GENERAL},
    {message: 'general3', type: MESSAGE_TYPE.GENERAL},
    {message: 'combat', type: MESSAGE_TYPE.COMBAT},
    {message: 'combat1', type: MESSAGE_TYPE.COMBAT},
    {message: 'combat2', type: MESSAGE_TYPE.COMBAT},
    {message: 'combat3', type: MESSAGE_TYPE.COMBAT},
  ];

  private add(message: string, type: MESSAGE_TYPE): void {
    this.messages.push({message, type});
  }
  public addGeneralMessage(message: string): void {
    this.add(message, MESSAGE_TYPE.GENERAL);
  }
  public addCombatMessage(message: string): void {
    this.add(message, MESSAGE_TYPE.COMBAT);
  }
  public clear(): void {
    this.messages = [];
  }
  constructor() {}
}

App Component ts

import {Component, OnInit} from '@angular/core';

import {MessageService} from './message.service';
import {Message} from './message';
import {MESSAGE_TYPE} from './messageType';

@Component({
  selector: 'my-app',
  templateUrl: './app.component.html',
  styleUrls: [ './app.component.css' ]
})
export class AppComponent implements OnInit {
  name = 'Angular';
  // "Exporting" enum so it's available in the HTML.
  // This is probably not the right way if your way is working, keep it that way.
  // I haven't written angular in a long time (had to do react) and so I can't tell you
  // best practices.
  public MESSAGE_TYPE = MESSAGE_TYPE;

  public messages: Message[] = [];
  public selectedType: MESSAGE_TYPE = MESSAGE_TYPE.GENERAL;

  public constructor(public messageService: MessageService) {}

  public ngOnInit() {
    this.goAndFilter();
  }

  public filterMessages(type: MESSAGE_TYPE) {
    this.selectedType = type;
    this.goAndFilter();
  }

  // really creative I know
  private goAndFilter(): void {
    this.messages = this.messageService.messages.filter(_ => _.type === this.selectedType);
  }
}

App Component html

<p>Selected type: {{selectedType}}<P>

<Button 
  (click)="filterMessages(MESSAGE_TYPE.COMBAT)"
>Show Combat</Button>
<Button 
  (click)="filterMessages(MESSAGE_TYPE.GENERAL)"
>Show General</Button>

<div *ngFor="let message of messages">
  <p>{{message.message}} - {{message.type}}</p>
</div>

Here is a StackBlitz but I don't know if this is persistent: https://stackblitz.com/edit/angular-vwpi6n?file=src%2Fapp%2Fapp.component.html.

Elias
  • 3,592
  • 2
  • 19
  • 42
  • Your implementation is the one I had, I don't find a way to use addMessages in your stackblitz. If i show combat, and I add a message of type combat, is it refresh ? I implemented Ling Vu solution (i manage to make it work) and it work the way i want. I'll edit my post :) Thank you Elias ! – Steven Jeanne Mar 03 '20 at 09:57
  • Well, you weren't asking for a way to add messages so I didn't implement it. But if @LingVu 's solution works for you, go for it :) (Also if a solution solves your problem, please mark it as accepted and thus reward the writer for their effort :)) – Elias Mar 03 '20 at 10:00