I have a SERVICE that emits the ID, TITLE and MESSAGE for a modal, hence using one modal for many faces; polymorphism, actually.
I can SEND the emitted message but getting it to show up in the generic.modal.component.ts is not happening as expected.
Here's my code (abridged version for brevity)
this is a snippet from my component.ts file
// Top of the code
#Output() setAddress: EventEmitter<any> = new EventEmitter();
#Output() setModalID: EventEmitter<any> = new EventEmitter();
#Output() setModalTitle: EventEmitter<any> = new EventEmitter();
#Output() setModalContent: EventEmitter<any> = new EventEmitter();
#Output() messageEvent = new EventEmitter<object>();
message = {
id: '',
title: '',
msg: '',
msgObj: {}
};
// ... some more code...
openModal(id: string, title: string): void {
console.log('Modal will open...' + id);
this.modalService.setID(id);
this.modalService.setTitle(id, title);
this.modalService.setMsg(id, "This is the Example Modal Body");
this.setModalAction(id, title, this.modalService.getMsg());
this.commonService.showHideGenericModal('button');
}
setModalAction(id: string, title: any, msg: any): void {
this.message.id = id;
this.message.title = title;
this.message.msg = msg;
// Send Message to emit
this.sendMessage();
}
// Send Message Object to be listened for
sendMessage(): void {
console.log('Sending Message: ', this.message);
this.messageEvent.emit(this.message);
}
common-svcs.service.ts
import { Injectable, OnInit } from '#angular/core';
import { SessionStorageService } from 'angular-web-storage';
import { Subject } from 'rxjs';
import { ModalService } from './modal.service';
import { VaForm21elementService } from './vaForm21.element.service';
declare global {
interface Window {
initAutocomplete: () => void;
}
}
#Injectable({
providedIn: 'root'
})
export class CommonSvcsService implements OnInit {
modalMsg: string = '';
modalID: string = '';
modalTitle: string = '';
private _messageDetails: Subject<any> = new Subject<any>();
public messageDetailsObs = this._messageDetails.asObservable();
... some more code
constructor(
private sessionStorageService: SessionStorageService,
private modalService: ModalService,
private vaform21Service: VaForm21elementService
) { }
ngOnInit() {
window.initAutocomplete = this.initAutocomplete;
if (this.isGenericModalOpen) {
this.isGenericModalOpen = false;
this.sessionStorageService.set('isGenericModalOpen', this.isGenericModalOpen);
}
}
ngOnDestroy() {
this.sessionStorageService.remove('isGenericModalOpen');
}
ngAfterViewInit() {
if (this.isGenericModalOpen) {
this.isGenericModalOpen = false;
this.sessionStorageService.set('isGenericModalOpen', this.isGenericModalOpen);
}
}
showHideGenericModal(fromwhere: string): void {
switch (fromwhere) {
case "voice":
case "button":
this.openModalDialog();
break;
case "close":
this.closeModalDialog();
break;
}
}
/**
* Open Generic Modal
*/
public openModalDialog(): void {
this.genericModal = document.getElementsByClassName('drag-block')[0] as HTMLElement;
this.isGenericModalOpen = this.sessionStorageService.get('isGenericModalOpen');
if (!this.isGenericModalOpen) {
this.isGenericModalOpen = true;
this.sessionStorageService.set('isGenericModalOpen', this.isGenericModalOpen);
console.log('Inside openGenericModal: ', this.genericModal);
this.id = this.modalService.getID();
this.modalTitle = this.modalService.getTitle();
this.modalMsg = this.modalService.getMsg();
this.genericModal.classList.add('show')
this.isGenericModalOpen = false;
this.sessionStorageService.set('isGenericModalOpen', this.isGenericModalOpen);
} else {
// ('The modal is already opened');
}
}
}
modal.service.ts
export class ModalService {
modals: any[] = [];
modalTitle!: string;
modalMsg!: string;
id!: string;
showModal: boolean = false;
theModal!: HTMLElement;
add(modal: any) {
// add modal to array of active modals
this.modals.push(modal);
}
remove(id: string) {
// remove modal from array of active modals
this.modals = this.modals.filter(x => x.id !== id);
}
open(id: string): boolean {
// open modal specified by id
const modal: any = this.modals.filter(x => x.id === id)[0];
return this.showModal = false;
}
close(id: string): boolean {
// close modal specified by id
const modal: any = this.modals.filter(x => x.id === id)[0];
return this.showModal = true;
}
setTitle(id: string, title: string) {
// Sets the title
const modal: any = this.modals.filter(x => x.id === id)[0];
this.modalTitle = title;
// modal.title = title;
}
setMsg(id: string, msg: string) {
// Sets the title
const modal: any = this.modals.filter(x => x.id === id)[0];
this.modalMsg = msg;
// modal.msg = msg;
}
setID(id: string): void {
this.id = id;
}
getID(): string {
return this.id;
}
// Get Modal Title
getTitle(): string {
return this.modalTitle;
}
// Get Modal Msg/Body
getMsg(): string {
return this.modalMsg;
}
}
Finally, the generic.modal.component.ts and html files respectively
import { Component, OnInit, Input, ViewChild, ElementRef } from '#angular/core';
import { SessionStorageService } from 'angular-web-storage';
import { CommonSvcsService } from 'src/app/services/common-svcs.service';
import { ModalService } from 'src/app/services/modal.service';
#Component({
selector: 'app-mymodal',
templateUrl: './generic-modal.component.html',
styleUrls: ['./generic-modal.component.css']
})
export class GenericModalComponent implements OnInit {
#Input() my_modal_title!: string;
#Input() my_modal_content!: string;
#Input() id: string = '';
#ViewChild('messageEvent') messageAction: any;
showModal: boolean = false;
#ViewChild('el') el: ElementRef | undefined;
private element: any;
private isGenericModalOpen: boolean = false;
private message!: object;
constructor(
private modalService: ModalService,
private sessionStorageService: SessionStorageService,
private commonService: CommonSvcsService
) { }
ngOnInit() {
const modal = this;
this.element = this.el?.nativeElement;
this.modalService.add(this);
if (this.isGenericModalOpen) {
this.isGenericModalOpen = false;
this.sessionStorageService.remove('isGenericModalOpen');
}
}
ngAfterViewInit() {
this.id = this.commonService.modalID;
this.my_modal_title = this.commonService.modalTitle;
this.my_modal_content = this.commonService.modalMsg;
}
ngAfterViewChecked() {
this.commonService.messageDetailsObs.subscribe((messageDetails) => {
console.log('Message Details: ', messageDetails)
});
}
ngOnDestroy() {
this.modalService.remove(this.id);
this.element.remove();
this.sessionStorageService.remove('isGenericModalOpen');
}
// open modal
public open(): void {
// this.element.style.display = 'block';
// document.body.classList.add('modal-open');
this.showModal = this.modalService.open(this.id);
}
// close modal
public close(): void {
// this.element.style.display = 'none';
// document.body.classList.remove('modal-open');
this.showModal = false;
this.commonService.showHideGenericModal('close');
}
// Set Modal Title
public setTitle(title: string): void {
this.my_modal_title = this.modalService.getTitle();
this.modalService.setTitle(this.id, title);
}
// Set Modal Message
public setMsg(msg: string): void {
this.my_modal_content = this.modalService.getMsg();
this.modalService.setMsg(this.id, msg);
}
// Set the ID of the modal dynamically
public setID(id: string): void {
this.id = id;
}
// Get the ID of the modal set
public getID(): string {
return this.id;
}
public receiveMessage($event: any): void {
this.message = $event;
console.log('Received Message: ', this.message);
}
}
The HTML
<div class="modal fade drag-block" (messageAction)="receiveMessage($event)" id="{{id}}" tabindex="-1" role="dialog" aria-labelledby="exampleModalLabel"
aria-hidden="true" [ngClass]="{'show': showModal}">
<div class="vertical-alignment-helper">
<div class="modal-dialog vertical-align-center" role="document">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="exampleModalLabel">{{my_modal_title}}</h5>
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true" (click)="close()">×</span>
</button>
</div>
<div class="modal-body">
{{my_modal_content}}
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-dismiss="modal">Close</button>
</div>
</div>
</div>
</div>
</div>
What I'm doing is dynamically feeding the ID, TITLE and MSG into a modal so I can send into it anything and use only one modal component. I getting is: private message!: object; is UNDEFINED
BTW: the modal OPENS and the buttons and "x" are there. Nothing is in the TITLE, MSG or ID
Thank you
UPDATE:
So, here's my minor issue after #Krenom helped me.
I'm going to reduce this for simplicity:
main-form.component.ts
main-form.component.html <-- this will fire 1 to "n" number of modals
generic-modal.component.ts
generic-modal.component.html <-- this will RECEIVE the msgObj which is outline both in my question and #Krenom's solution.
common-svc.service.ts <-- talks to any component that imports this
All of these components and service live under src/app/
The components are under /components and the services live under /services under app/
Now that that's done.
The Problem
the msgObj gets populated nicely. BUT even with me sending the messageObject via an Observable or the updateModal function that #Krenom created the fact remains, that I need to be able to change the {{title}} and {{msg}} in the generic-modal.component.ts from the common-svc.service.ts when the msgObj changes.
That's all.
I am STILL keeping #Krenoms solution, I probably just am missing something
Here is the full generic-modal.component.ts
import { Component, OnInit, Input, ViewChild, ElementRef, ChangeDetectorRef, ViewRef } from '#angular/core';
import { SessionStorageService } from 'angular-web-storage';
import { CommonSvcsService } from 'src/app/services/common-svcs.service';
import { ModalService } from 'src/app/services/modal.service';
import { GenericModalObj } from '../va-form-21/vaForm21.interface';
#Component({
selector: 'app-mymodal',
templateUrl: './generic-modal.component.html',
styleUrls: ['./generic-modal.component.css']
})
export class GenericModalComponent implements OnInit {
#Input() my_modal_title!: string;
#Input() my_modal_content!: string;
#Input() id: string = '';
#ViewChild('messageEvent') messageAction: any;
showModal: boolean = false;
#Input() public set data(value: any) {
this.message = value;
this.data();
}
#ViewChild('el') el: ElementRef | undefined;
private element: any;
private isGenericModalOpen: boolean = false;
public message!: object;
constructor(
private modalService: ModalService,
private sessionStorageService: SessionStorageService,
private commonService: CommonSvcsService,
private cdref: ChangeDetectorRef
) { }
msgObj!: GenericModalObj;
ngOnInit() {
const modal = this;
this.element = this.el?.nativeElement;
this.modalService.add(this);
if (this.isGenericModalOpen) {
this.isGenericModalOpen = false;
this.sessionStorageService.remove('isGenericModalOpen');
}
this.commonService.messageDetailsObs.subscribe(message => {
if (message !== this.message) {
this.message = message;
this.setMsgObj();
}
});
}
ngAfterViewInit() {
this.id = this.commonService.modalID;
this.my_modal_title = this.commonService.modalTitle;
this.my_modal_content = this.commonService.modalMsg;
}
ngAfterViewChecked() {
this.commonService.messageDetailsObs.subscribe((messageDetails) => {
console.log('Message Details: ', messageDetails)
});
}
ngAfterContentChecked() {
this.cdref.detectChanges();
}
ngOnDestroy() {
this.modalService.remove(this.id);
this.element.remove();
this.sessionStorageService.remove('isGenericModalOpen');
this.cdref.detach();
}
public checkRefDetectChanged(): void {
if(!(<ViewRef> this.cdref).destroyed) {
this.cdref.detectChanges();
}
}
// open modal
public open(): void {
// this.element.style.display = 'block';
// document.body.classList.add('modal-open');
this.showModal = this.modalService.open(this.id);
}
// close modal
public close(): void {
// this.element.style.display = 'none';
// document.body.classList.remove('modal-open');
this.showModal = false;
this.commonService.showHideGenericModal('close');
}
// Set Modal Title
public setTitle(title: string): void {
this.my_modal_title = this.modalService.getTitle();
this.modalService.setTitle(this.id, title);
this.msgObj.title = this.my_modal_title;
this.cdref.detectChanges();
}
// Set Modal Message
public setMsg(msg: string): void {
this.my_modal_content = this.modalService.getMsg();
this.modalService.setMsg(this.id, msg);
this.msgObj.msg = this.my_modal_content;
this.cdref.detectChanges();
}
// Set the ID of the modal dynamically
public setID(id: string): void {
this.id = id;
this.msgObj.id = this.id;
this.cdref.detectChanges();
}
// Get the ID of the modal set
public getID(): string {
return this.id;
}
public receiveMessage(event: any): void {
this.message = event;
console.log('Received Message: ', this.message);
this.my_modal_title = this.commonService.modalTitle;
this.my_modal_content = this.commonService.modalMsg;
this.msgObj.id = this.commonService.id;
this.msgObj.title = this.my_modal_title;
this.msgObj.msg = this.my_modal_content;
this.modalService.updateModal(this.msgObj);
this.cdref.detectChanges();
}
public get data(): any {
return this.msgObj;
}
public setMsgObj(): void {
this.message = this.msgObj;
}
}
Here's the GenericModalObj from the interface file that #Krenom explained
export interface GenericModalObj {
id: string;
title: string;
msg: string;
msgObj: {}
}
Finally, this is my modal.service.ts
export class ModalService {
modals: any[] = [];
modalTitle!: string;
modalMsg!: string;
id!: string;
showModal: boolean = false;
theModal!: HTMLElement;
add(modal: any) {
// add modal to array of active modals
this.modals.push(modal);
}
remove(id: string) {
// remove modal from array of active modals
this.modals = this.modals.filter(x => x.id !== id);
}
open(id: string): boolean {
// open modal specified by id
const modal: any = this.modals.filter(x => x.id === id)[0];
return this.showModal = false;
}
close(id: string): boolean {
// close modal specified by id
const modal: any = this.modals.filter(x => x.id === id)[0];
return this.showModal = true;
}
setTitle(id: string, title: string) {
// Sets the title
const modal: any = this.modals.filter(x => x.id === id)[0];
this.modalTitle = title;
// modal.title = title;
}
setMsg(id: string, msg: string) {
// Sets the title
const modal: any = this.modals.filter(x => x.id === id)[0];
this.modalMsg = msg;
// modal.msg = msg;
}
setID(id: string): void {
this.id = id;
}
getID(): string {
return this.id;
}
// Get Modal Title
getTitle(): string {
return this.modalTitle;
}
// Get Modal Msg/Body
getMsg(): string {
return this.modalMsg;
}
// In the Stackoverflow solution the options is options:
updateModal(options: GenericModalObj): void {
const modal = this.modals.find(x => x.id === options.id);
if (!modal) return;
if (options.title) modal.title = options.title;
if (options.message) modal.message = options.message;
}
}
Here's where I send the modal msgObj from the FORM COMPONENT
setModalAction(id: string, title: any, msg: any): void {
// Send Message to emit
this.messageOBJ.id = id;
this.messageOBJ.title = title;
this.messageOBJ.msg = msg;
this.sendMessage(this.messageOBJ);
}
// Send Message Object to be listened for
sendMessage(msgObj: Object): void {
console.log('Sending Message: ', msgObj);
this.messageEvent.emit(msgObj);
}
and my settings at the top of this component
#Output() setModalID: EventEmitter<any> = new EventEmitter();
#Output() setModalTitle: EventEmitter<any> = new EventEmitter();
#Output() setModalContent: EventEmitter<any> = new EventEmitter();
#Output() messageEvent = new EventEmitter<object>();
and the HTML for generic-modal.component.html
<div class="modal fade drag-block" (messageAction)="receiveMessage($event)" id="genericmodal" tabindex="-1"
role="dialog" aria-labelledby="exampleModalLabel" aria-hidden="true" [ngClass]="{'show': showModal}">
<div class="vertical-alignment-helper">
<div class="modal-dialog vertical-align-center" role="document">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title fxModalTitle">{{my_modal_title}}</h5>
<button type="button" class="close" data-dismiss="modal" aria-label="Close" (click)="close()">
<span aria-hidden="true">×</span>
</button>
</div>
<div class="modal-body fxModalBody">{{my_modal_content}}</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-dismiss="modal" (click)="close()">Close</button>
</div>
</div>
</div>
</div>
</div>
Ok, not nearly enough time in a day to try and figure out how exactly that is all hooked up (you should consider refactoring and simplifying), but I suspect it's due to your inputs.
In a typical #Input() usage, you have some values set and ready, and then you show your component. The values are ready and you have them available at ngOnInit() time.
When dealing with opening dialogs, and using services and observables and all kinds of things that make things non-synchronous, it's quite possible that you don't have them at component init time.
The most basic fix is to wrap all your template parts with *ngIf="someData", that way, if they're undefined to start with, nothing will show (and nothing will attempt to use the un-set variable) and you'll get no errors.
Once the value comes in, it'll show and all shall be good.
A slightly more advanced method, for use with the above, because you still need to prevent the template from accessing properties that aren't yet set - is to use setters.
private _data: any;
#Input() public set data(value: any) {
this._data = value;
this.onDataReceived();
}
public get data(): any { return this._data; }
private onDataReceived(): void {
//... anything you might need to do once you get some input - sorting, processing, etc.
}
Typically though, only really need this if you plan on doing something with the value when it comes in, but always handy to know you can do this.
On the topic of refactoring, I think the simplest thing you can do is remove the split of id/title/message. It's tripling the amount of code that's there making it much harder to read and understand.
All three of those things are closely coupled and related to making one of your dialogs work. Keep them together, then. You already do, in your component.ts:
message = {
id: '',
title: '',
msg: '',
msgObj: {}
};
All methods can be reduced to one update method with this one object as a param instead of several, and you only need to handle the update in one fashion.
public updateModal(options: ModalDataDto): void {
const modal = this.modals.find(x => x.id === options.id);
if (!modal) return;
modal.title = options.title;
modal.message = options.message;
}
If you don't keep track of that options object and anything could be making arbitrary guesswork updates to individual properties, then you can do checks before assigning so you don't accidentally overwrite:
public updateModal(options: ModalDataDto): void {
const modal = this.modals.find(x => x.id === options.id);
if (!modal) return;
if (options.title) modal.title = options.title;
if (options.message) modal.message = options.message;
}
That way, anything could update only a part of it:
...
this.service.updateModal({id: 'asdf', title: 'New title'});
...
I notice that you can dynamically re-set the id as well. Not a fan of changing IDs because they're identifiers, and changing them is weird and not generally done, but if you are going to, I'd then suggest an additional property in your new options:
if (options.newId) modal.id = options.newId;
Update for your update:
One thing I can see is, wrong, ish, but mostly pointless:
#Input() public set data(value: any) {
this.message = value;
this.data();
}
It's setting the message, huzzah, but then calling this.data() - which I can't see as a method in there anywhere. I can see a getter, but then usage would be this.data rather than as a method call. And it's just a getter, so that line wouldn't be doing anything.
I'll say the same thing as before - it's still too complicated to follow, especially at this time of the morning, so I'll guess.
It looks like it works this way, from what I can see:
The modals proper in ModalService is a list of your modals, right? Hard to tell without types. I assume by 'modal', we also mean actual GenericModalComponent instances? It looks like it.
You have the method to affect updates against these modals:
updateModal(options: GenericModalObj): void {
const modal = this.modals.find(x => x.id === options.id);
if (!modal) return;
if (options.title) modal.title = options.title;
if (options.message) modal.message = options.message;
}
Where it does modal.title = .... However, I can't see any public properties for title or message in GenericModalComponent.
You have a my_modal_title as an input, and you have setTitle(...) as a method to do such, but you are using neither.
Presumably, if you used if (options.title) modal.setTitle(options.title); it would work.
Though maybe not... given the weird steps that method is doing.
If I'm reading it right, when you call setTitle it does the following:
Assume the service title is currently 'Service Title', and the modal title is currently 'Modal Title' - and we're going to call the modal's setTitle('New Title') method.
Sets its #Input title (my_modal_title ) to whatever value is currently stored in the ModalService - so that is now 'Service Title'.
It calls the ModalService's setTitle method, which finds the modal, ignores it, and sets its own stored title to be the one given it - the service title is now 'New Title'.
It then sets the msgObj title to be the new one - ok, fine, but that msgObj doesn't look like it's used anywhere?
Then does detect changes - ok, fine, but we'll get to that.
So you've got a few compounding structural issues here that need to be sorted out.
Lets roll with change detection to start with, because I don't think you should ever generally need to worry about change detection unless you're doing something specific, which this doesn't feel like it is.
Assume a basic setup:
thing.component.ts
export class ThingComponent {
public valueValue: string;
public refValue: SomeObject;
public arrayValue: SomeObject[];
}
and a basic template:
<div>Value type: {{valueValue}}</div>
<div>Reference type: {{refValue}} - {{refValue.someProperty}}</div>
<div>
Array type: {{arrayValue}}
<ul>
<li *ngFor="let item of arrayValue">{{item.someProperty}}</li>
</ul>
</div>
When filled, you should see something like:
Value type: Whatever value was entered
Reference type: [Object object] - Whatever ref value was entered
Array type: [Object object, maybe Object array or whatever, I don't remember]
- Whatever ref value 1 was entered
- Whatever ref value 2 was entered
Now, any updates valueValue should cause the view to update. Value types are immutable and Angular handles checking these nps.
Any updates to refValue is different, depending on how you use it. Angular won't, generally, be checking the internals of your object, so any change to refValue.someProperty may not actually be picked up. Guaranteed to be picked up is if refValue itself is changed.
If you had a getter, e.g. public getRefValue(): string { return this.refValue.someProperty; } then changes to someProperty should be picked up if that's used in the view instead, e.g. <div>Value type: {{getRefValue}}</div>.
This is because Angular generally hits the getters during its update cycles and at that point, it's just a string, not an object.
For anything referencing that object, however (e.g. any component #Input()s that you pass it in to), they will be looking at the object reference.
In that case, you can cheat and just create a new object (beware of shallow/deep clone issues) by using the shorthand: this.refValue = {...this.refValue}; - now refValue is a copy of itself, but in a new object, so Angular's change detection will re-evaluate it.
This will trigger setter hits to your #Inputs whereas doing this.refValue.someProperty = 'asdf'; wouldn't have.
Arrays are more fun, because they're more or less the same as objects. You can change the contents of an array, but that doesn't change the array object itself. Similar to objects, you can short-hand for copying, e.g. this.arrayValue = [...this.arrayValue]; or, I think you can do this.arrayValue = this.arrayValue.splice() which also returns a copy.
Finally, after that essay, it my intended usage of the GenericModalObj object. It should be used in place of the multiple lines.
You can replace:
public id: string;
public title: string;
public message: string;
with:
public data: GenericModalObj;
And you don't need all those setId, setTitle, setMessage, and reduce down to the one update method that you, sort of, have in place. Also, any method that deals with more than one of those doesn't need multiple params, just the one. It's about reducing the code.
Id, title, message - all of these are closely coupled; you can't have a modal without them all so it suggests that, structurally, they belong together. So put them together in a type, and make use of the typing available with TS.
private doSomething(): string {...}
Tells everyone so much more than
doSomething() {...}
It's private, so I know it's not available to the template (if this is a component), or generally not available/used outside whatever class it's in.
I can see it's going to return a string, so I know it's not just a random processor method that will (possibly) alter the state of something. I mean, it might, but hopefully the naming will suggest that, but ideally it won't (one method does one thing...).
Making proper use of context like this helps to understand and follow what is happening with your code. Never write code that you understand because you wrote it, always write code so that idiots like me can read it. Others in the future, and Future You, will thank you for it.
I was able to finally get the SUBSCRIBE and SEND MESSAGE to work! This solution is a combination of what Krenom did and what I was relentless in doing.
I'm keeping what Krenom has as a solution but I'm expanding on it.
Here's what I did.
I kept the HTML for generic modal
I've modified the Generic Modal Component (SEE BELOW)
I've modified the Common Service (SEE BELOW)
I've modified the call in the FORM component slightly (SEE BELOW)
Generic Modal Component
import { Component, OnInit, Input, ViewChild, ElementRef, ChangeDetectorRef, ViewRef } from '#angular/core';
import { SessionStorageService } from 'angular-web-storage';
import { CommonSvcsService } from 'src/app/services/common-svcs.service';
import { ModalService } from 'src/app/services/modal.service';
import { GenericModalObj } from '../va-form-21/vaForm21.interface';
#Component({
selector: 'app-mymodal',
templateUrl: './generic-modal.component.html',
styleUrls: ['./generic-modal.component.css']
})
export class GenericModalComponent implements OnInit {
#Input() my_modal_title!: string;
#Input() my_modal_content!: string;
#Input() id: string = '';
#ViewChild('messageEvent') messageAction: any;
showModal: boolean = false;
#Input() public set data(value: any) {
this.message = value;
this.data();
}
#ViewChild('el') el: ElementRef | undefined;
private element: any;
private isGenericModalOpen: boolean = false;
public message!: object;
constructor(
private modalService: ModalService,
private sessionStorageService: SessionStorageService,
private commonService: CommonSvcsService,
private cdref: ChangeDetectorRef
) { }
msgObj!: GenericModalObj;
ngOnInit() {
const modal = this;
this.element = this.el?.nativeElement;
this.modalService.add(this);
if (this.my_modal_content === undefined && this.my_modal_title === undefined) {
this.my_modal_content = '';
this.my_modal_title = '';
} else {
this.my_modal_title = this.modalService.getTitle();
this.my_modal_content = this.modalService.getMsg();
}
if (this.isGenericModalOpen) {
this.isGenericModalOpen = false;
this.sessionStorageService.remove('isGenericModalOpen');
}
this.commonService.messageDetailsObs.subscribe(message => {
if (message !== this.message) {
this.message = message;
this.setMsgObj();
}
});
this.checkRefDetectChanged();
}
ngAfterViewInit() {
this.id = this.commonService.modalID;
if (this.my_modal_title && this.my_modal_title.length > 0) {
this.my_modal_title = this.modalService.getTitle();
}
if (this.my_modal_content && this.my_modal_content.length > 0) {
this.my_modal_content = this.modalService.getMsg();
}
this.checkRefDetectChanged();
}
ngAfterViewChecked() {
this.commonService.messageDetailsObs.subscribe((messageDetails) => {
console.log('Message Details: ', messageDetails);
if (this.modalService.id.length > 0 &&
this.modalService.modalTitle.length > 0 &&
this.modalService.modalMsg.length > 0) {
this.msgObj.id = this.modalService.id;
this.my_modal_title = this.modalService.modalTitle;
this.my_modal_content = this.modalService.modalMsg;
}
this.checkRefDetectChanged();
});
}
ngAfterContentChecked() {
if (this.modalService.id.length > 0 &&
this.modalService.modalTitle.length > 0 &&
this.modalService.modalMsg.length > 0) {
this.id = this.modalService.id;
this.my_modal_title = this.modalService.modalTitle;
this.my_modal_content = this.modalService.modalMsg;
}
this.cdref.detectChanges();
}
ngOnDestroy() {
this.modalService.remove(this.id);
this.element.remove();
this.sessionStorageService.remove('isGenericModalOpen');
this.cdref.detach();
this.my_modal_content = '';
this.my_modal_title = '';
this.id = '';
}
public checkRefDetectChanged(): void {
if (!(<ViewRef>this.cdref).destroyed) {
this.cdref.detectChanges();
}
}
// open modal
public open(): void {
// this.element.style.display = 'block';
// document.body.classList.add('modal-open');
this.showModal = this.modalService.open(this.id);
}
// close modal
public close(): void {
// this.element.style.display = 'none';
// document.body.classList.remove('modal-open');
this.showModal = false;
this.commonService.showHideGenericModal('close');
}
// Set Modal Title
public setTitle(title: string): void {
this.my_modal_title = this.modalService.getTitle();
this.modalService.setTitle(this.id, title);
this.msgObj.title = this.my_modal_title;
this.checkRefDetectChanged();
}
// Set Modal Message
public setMsg(msg: string): void {
this.my_modal_content = this.modalService.getMsg();
this.modalService.setMsg(this.id, msg);
this.msgObj.msg = this.my_modal_content;
this.checkRefDetectChanged();
}
// Set the ID of the modal dynamically
public setID(id: string): void {
this.id = id;
this.msgObj.id = this.id;
this.checkRefDetectChanged();
}
// Get the ID of the modal set
public getID(): string {
return this.id;
}
public receiveMessage(event: any): void {
this.message = event;
console.log('Received Message: ', this.message);
this.my_modal_title = this.commonService.modalTitle;
this.my_modal_content = this.commonService.modalMsg;
this.msgObj.id = this.commonService.id;
this.msgObj.title = this.my_modal_title;
this.msgObj.msg = this.my_modal_content;
this.checkRefDetectChanged();
}
public get data(): any {
return this.msgObj;
}
public setMsgObj(): void {
this.message = this.msgObj;
}
}
Common Service
import { Injectable, OnInit } from '#angular/core';
import { SessionStorageService } from 'angular-web-storage';
import { Subject } from 'rxjs';
import { GenericModalObj } from '../components/form/Form.interface';
import { ModalService } from './modal.service';
import { FormelementService } from './Form.element.service';
declare global {
interface Window {
initAutocomplete: () => void;
}
}
#Injectable({
providedIn: 'root'
})
export class CommonSvcsService implements OnInit {
modals: any[] = [];
modalMsg: string = '';
modalID: string = '';
modalTitle: string = '';
private _messageDetails: Subject<any> = new Subject<any>();
public messageDetailsObs = this._messageDetails.asObservable();
startDate!: Date;
endDate!: Date;
currDate!: Date;
vaFormData: any;
showModal: boolean = true;
autocomplete!: google.maps.places.Autocomplete;
address1Field!: HTMLInputElement;
address2Field!: HTMLInputElement;
postalField!: HTMLInputElement;
genericModal!: HTMLElement;
id!: string;
isGenericModalOpen: boolean = false;
constructor(
private sessionStorageService: SessionStorageService,
private modalService: ModalService,
private formService: FormelementService
) { }
ngOnInit() {
window.initAutocomplete = this.initAutocomplete;
if (this.isGenericModalOpen) {
this.isGenericModalOpen = false;
this.sessionStorageService.set('isGenericModalOpen', this.isGenericModalOpen);
}
this.formService.regForSettingModalID(this.regForSettingModalID.bind(this));
this.formService.regForSettingModalTitle(this.regForSettingModalTitle.bind(this));
this.formService.regForSettingModalMsg(this.regForSettingModalMsg.bind(this));
}
ngOnDestroy() {
this.sessionStorageService.remove('isGenericModalOpen');
}
ngAfterViewInit() {
if (this.isGenericModalOpen) {
this.isGenericModalOpen = false;
this.sessionStorageService.set('isGenericModalOpen', this.isGenericModalOpen);
}
}
checkDate(date1: Date, date2: Date): string {
let retVal = "";
this.currDate = new Date();
this.startDate = new Date(date1);
if (this.startDate.getTime() > this.currDate.getTime()) {
retVal = "Your Entry Date cannot be greater than today's Date.";
}
return retVal;
}
initAutocomplete() {
this.address1Field = document.querySelector("#homeaddress") as HTMLInputElement;
this.address2Field = document.querySelector("#hmapt") as HTMLInputElement;
this.postalField = document.querySelector("#hmpostal") as HTMLInputElement;
// Create the autocomplete object, restricting the search predictions to
// addresses in the US and Canada.
this.autocomplete = new google.maps.places.Autocomplete(this.address1Field, {
componentRestrictions: { country: ["us", "ca"] },
fields: ["address_components", "geometry"],
types: ["address"],
});
this.address1Field.focus();
// When the user selects an address from the drop-down, populate the
// address fields in the form.
this.autocomplete.addListener("place_changed", this.fillInAddress);
}
fillInAddress() {
// Get the place details from the autocomplete object.
const place = this.autocomplete.getPlace();
let address1 = "";
let postcode = "";
// Get each component of the address from the place details,
// and then fill-in the corresponding field on the form.
// place.address_components are google.maps.GeocoderAddressComponent objects
// which are documented at http://goo.gle/3l5i5Mr
for (const component of place.address_components as google.maps.GeocoderAddressComponent[]) {
// #ts-ignore remove once typings fixed
const componentType = component.types[0];
switch (componentType) {
case "hmaddy":
case "bzaddy": {
address1 = `${component.long_name} ${address1}`;
break;
}
case "route": {
address1 += component.short_name;
break;
}
case "postal_code": {
postcode = `${component.long_name}${postcode}`;
break;
}
case "postal_code_suffix": {
postcode = `${postcode}-${component.long_name}`;
break;
}
case "hmcity":
case "bzcity":
(document.querySelector("#hmcity") as HTMLInputElement).value =
component.long_name;
break;
case "administrative_area_level_1": {
(document.querySelector("#hmstate") as HTMLInputElement).value =
component.short_name;
break;
}
case "hmcountry":
case "bzcountry":
(document.querySelector("#hmcountry") as HTMLInputElement).value =
component.long_name;
break;
}
}
}
regForCheckDate(checkDateCallback: Function): void {
console.log('Check Date Callback: ', checkDateCallback);
}
regForfillInAddress(checkfillInAddressCallback: Function): void {
console.log('Check fillInAddress Callback: ', checkfillInAddressCallback);
}
regForSettingModalID(checkSettingModalIDCallback: Function): void {
console.log('Check for Modal ID', checkSettingModalIDCallback);
this.formService.regForSettingModalID(checkSettingModalIDCallback);
}
regForSettingModalTitle(checkSettingModalTitleCallback: Function): void {
console.log('Check for Modal Title', checkSettingModalTitleCallback);
this.formService.regForSettingModalTitle(checkSettingModalTitleCallback);
}
regForSettingModalMsg(checkSettingModalMsgCallback: Function): void {
console.log('Check for Modal ID', checkSettingModalMsgCallback);
this.formService.regForSettingModalMsg(checkSettingModalMsgCallback);
}
showHideGenericModal(fromwhere: string): void {
switch (fromwhere) {
case "voice":
case "button":
this.openModalDialog();
break;
case "close":
this.closeModalDialog();
break;
}
}
/**
* Open Generic Modal
*/
public openModalDialog(): void {
this.genericModal = document.getElementsByClassName('drag-block')[0] as HTMLElement;
this.isGenericModalOpen = this.sessionStorageService.get('isGenericModalOpen');
let id = '', title = '', msg = '', msgObj = {};
if (!this.isGenericModalOpen) {
this.isGenericModalOpen = true;
this.sessionStorageService.set('isGenericModalOpen', this.isGenericModalOpen);
console.log('Inside openGenericModal: ', this.genericModal);
this.id = this.modalService.getID();
this.modalTitle = this.modalService.getTitle();
this.modalMsg = this.modalService.getMsg();
this.genericModal.classList.add('show');
this.isGenericModalOpen = false;
id = 'genericmodal';
title = this.modalTitle;
msg = this.modalMsg;
msgObj = this.modalService.getMsgObj();
// Send Message
this.messageDetails({id, title, msg, msgObj});
this.updateModal({
id, title, msg,
msgObj: {}
});
this.sessionStorageService.set('isGenericModalOpen', this.isGenericModalOpen);
} else {
// ('The modal is already opened');
}
}
public closeModalDialog() {
let isGenericModalOpen;
isGenericModalOpen = false;
this.genericModal = document.getElementsByClassName('drag-block')[0] as HTMLElement;
this.sessionStorageService.set('isDebugModalOpen', isGenericModalOpen);
// console.log('Inside closeDebugModal: ', isDebugModalOpen);
this.genericModal.classList.remove('show')
}
public updateModal(options: GenericModalObj): void {
const modal = this.modals.find(x => x.id === options.id);
if (!modal) return;
modal.title = options.title;
modal.message = options.msg;
modal.msgObj = options.msgObj;
}
messageDetails(message: Object): void {
this._messageDetails.next(message);
}
}
Form Component
Snippets from the full code
...
showHideGenericModal(event: any, key: any, showmodal: boolean): void {
this.showModal = showmodal;
if (event.target.id !== 'btnDemoModal1' && event.target.id !== 'btnDemoModal2') {
return;
} else {
this.genericModalDialog = true;
setTimeout(() => {
this.genericModalDialog = false;
}, 5000)
this.openModal(key, "Opening " + key);
}
}
openModal(id: string, title: string): void {
console.log('Opening the ' + id);
let msgObj: Object = {};
this.modalService.setID(id);
this.modalService.setTitle(id, title);
this.modalService.setMsgObj(id, msgObj)
this.modalService.setMsg(id, this.getMsgBody(id));
this.setModalAction(id, title, this.modalService.getMsg(), this.modalService.getMsgObj());
// Make sure we set isGenericModalOpen to FALSE just in case it's still TRUE
this.sessionStorageService.set('isGenericModalOpen', false);
this.commonService.showHideGenericModal('button');
}
closeModal(id: string, modalname: string): void {
console.log('Modal will close...' + id);
this.modalService.id = modalname;
this.modalService.setTitle(id, "");
this.modalService.setMsg(id, "");
this.commonService.showHideGenericModal('close');
}
setModalAction(id: string, title: string, msg: string, msgobj: object): void {
// Send Message to emit
this.messageOBJ.id = id;
this.messageOBJ.title = title;
this.messageOBJ.msg = msg;
this.messageOBJ.msgObj = msgobj;
this.sendMessage(this.messageOBJ);
}
// Send Message Object to be listened for
sendMessage(msgObj: Object): void {
console.log('Sending Message: ', msgObj);
this.messageEvent.emit(msgObj);
}
getMsgBody(id: string): string {
let msgBody = '';
switch (id) {
case "genericmodal1":
msgBody = "This is the body text for modal 1";
break;
case "genericmodal2":
msgBody = "This is the body text for modal 2";
break;
default:
msgBody = "This is the default modal body text!"
break;
}
return msgBody;
}
...
Finally, you can pull the data for the modal body from a JSON object or database and modify your code accordingly.
The ngOnDestroy clears out the MODAL BODY and MODAL TITLE
ngOnDestroy() {
this.modalService.remove(this.id);
this.element.remove();
this.sessionStorageService.remove('isGenericModalOpen');
this.cdref.detach();
this.my_modal_content = '';
this.my_modal_title = '';
this.id = '';
}
NOTE: the this.id is ONLY the ID of the modal body and title NOT the id of the generic modal component. Krenom said not to dynamically change the modal ID and I heeded his warning.
Happy Coding!
I wanted to create a class with private parameters and functions to access the data I want. You can see this :
export class Product {
private name: string;
private type: string;
private longDetail: string;
private shortDetail: string;
private stock: number;
private price: number;
private linkImage: string;
private id: number;
constructor(
name: string,
type: string,
longDetail: string,
shortDetail: string,
stock: number,
price: number,
linkImage: string,
id: number
) {
this.name = name;
this.type = type;
this.longDetail = longDetail;
this.shortDetail = shortDetail;
this.stock = stock;
this.price = price;
this.linkImage = linkImage;
this.id = id;
}
getName(): string {
return this.name;
}
getType(): string {
return this.type;
}
getLongDetail(): string {
return this.longDetail;
}
getShortDetail(): string {
return this.shortDetail;
}
getStock(): number {
return this.stock;
}
getPrice(): number {
return this.price;
}
getLinkImage(): string {
return this.linkImage;
}
getId(): number {
return this.id;
}
}
And when I want to call a function in a component I am told :
ProductListComponent.html:15 ERROR TypeError: newProduct.getName is not a function
Do you have a solution ? Thank you very much in advance !
EDIT :
This is the code called after the click in front end
addProductBasket(newProduct: Product) {
const newClientBasket = this.createNewClientBasketWithAdd(
this.clientBasket.getValue(),
newProduct
)
this.clientBasket.next(newClientBasket)
console.log(newClientBasket)
}
private createNewClientBasketWithAdd(
oldClientBasket: BasketProduct[],
newProduct: Product
): BasketProduct[] {
const found = oldClientBasket.find((product) => {
if (product.getId() === newProduct.getId()) {
product.addOneProduct()
}
})
if (found === undefined) {
console.log(newProduct.getName())
oldClientBasket.push(
new BasketProduct(
newProduct.getName(),
newProduct.getType(),
newProduct.getLongDetail(),
newProduct.getShortDetail(),
newProduct.getStock(),
newProduct.getPrice(),
newProduct.getLinkImage(),
newProduct.getId()
)
)
}
return oldClientBasket
}
It's my apiservice to get data
export class ApiService {
private dataApi: BehaviorSubject<Product[]> = new BehaviorSubject<Product[]>([]);
constructor(private http: HttpClient) {
this.getDataFromApi();
}
private getDataFromApi(){
this.http
.get<Product[]>("../../assets/data.json")
.toPromise()
.then((data) => this.dataApi.next(data));
}
public getData():Observable<Product[]>{
return this.dataApi.asObservable();
}
}
You should have an instance of Product class before accessing its methods.
var newProduct = new Product();
newProduct.getName();
After David's help in commenting, I understood that I had to instantiate the data I receive in http client.
I then modified the constructor and my client http get
constructor(obj: any) {
Object.assign(this, obj);
}
and
private getDataFromApi(){
this.http
.get<Product[]>("../../assets/data.json").pipe()
.toPromise()
.then((data) => {
const productList = data.map(product => new Product(product));
this.dataApi.next(productList)});
}
Imagine I have the following interfaces
interface IMarket {
ID: number,
Name: string,
MarketDescription: string
}
interface IDepartment {
ID: number,
Name: string,
DepartmentDescription: string
}
Is there a way to store the interfaces in an object like this?
var typeMap = { Markets: IMarket, Departments: IDepartment }
I'd like to do something like this. I'd like to dynamically set the generic type for "getQueryResults" based on a string value I pass into the constructor.
export class Service {
protected baseURL = "";
protected typeName = "";
private typeMap = { Markets: IMarket, Departments: IDepartment }
constructor(typeName) {
this.baseURL = 'http://localhost/API/odata/' + typeName;
this.currentType = typeMap[typeName];
}
getQueryResults(): Promise<this.currentType> {
return new Promise<this.currentType>((resolve, reject) => {
$.getJSON(this.baseURL, function (returnValue) {
resolve(returnValue.value);
});
})
}
}
var marketService = new Service("Markets");
var topMarket = marketService.getQueryResults();
//topMarket is an instance(?) of IMarket
var departmentService = new Service("Departments");
var topDepartment = departmentServicegetQueryResults();
//topDepartment is an instance(?) of IDepartment
That can be simply solved using generics, it's exactly what it's for:
export class Service<T> {
protected baseURL = "";
constructor() {
this.baseURL = 'http://localhost/API/odata/' + typeName;
}
getQueryResults(): Promise<T> {
return new Promise<T>((resolve, reject) => {
$.getJSON(this.baseURL, function (returnValue) {
resolve(returnValue.value);
});
})
}
}
var marketService = new Service<IMarket>();
var topMarket: Promise<IMarket> = marketService.getQueryResults();
var departmentService = new Service<IDepartment>();
var topDepartment: Promise<IDepartment> = departmentService.getQueryResults();
Edit
You can use 2 more classes to "get rid" of the need to have Service<TYPE> more than once (per TYPE):
export abstract class Service<T> {
protected baseURL = "";
constructor() {
this.baseURL = 'http://localhost/API/odata/' + this.getTypeName();
}
getQueryResults(): Promise<T> {
return new Promise<T>((resolve, reject) => {
$.getJSON(this.baseURL, function (returnValue) {
resolve(returnValue.value);
});
})
}
protected abstract getTypeName(): string;
}
export class MarketsService extends Service<IMarket> {
protected getTypeName(): string {
return "Markets";
}
}
export class DepartmentsService extends Service<IDepartment> {
protected getTypeName(): string {
return "Departments";
}
}
var marketService = new MarketsService();
var topMarket: Promise<IMarket> = marketService.getQueryResults();
var departmentService = new DepartmentsService();
var topDepartment: Promise<IDepartment> = departmentService.getQueryResults();
But unlike the need to specify the type every time you use Service, these extra classes will be part of the compiled js, so it's a question of what's more important to you.
Taking a note from the TypeScript docs:
http://www.typescriptlang.org/docs/handbook/namespaces.html#namespaced-validators
namespace Validation {
export interface StringValidator {
isAcceptable(s: string): boolean;
}
It appears you would want:
namespace YourNamespace {
export interface IMarket {
ID: number,
Name: string,
MarketDescription: string
}
export interface IDepartment {
ID: number,
Name: string,
DepartmentDescription: string
}
}