angulartypescriptinputpaginationeventemitter

two Child Component share control


I have two Child Components that I want to get access to one of them from the other. I have a search input control on one of the pages. On the other, I have the pagination that I need access to the value. Is there a way to do this? I tried to use this searchItemsEvent: EventEmitter<any> but it's not working.

Child Component using input control

<div class="indication-grid-search-form-container">
    <div class="form-inline">
        <div class="form-group">
            <label for="gridSearchPatternId">Indication description</label>
            <div class="input-group">
                <input type="text" class="form-control" id="gridSearchPatternId" placeholder="Search on indication" aria-describedby="gridSearchPatternAddonId" [(ngModel)]="searchString" (ngModelChange)="onSearchStringChanged()">
                <span class="input-group-addon" id="gridSearchPatternAddonId"><span class="glyphicon glyphicon-search"></span></span>
            </div>
        </div>
        <button *ngIf="allowEdit" type="button" class="btn btn-primary add-new-grid-button" (click)="onAddNewItem()">
            <span class="glyphicon glyphicon-plus-sign" aria-hidden="true"></span> New Indication
        </button>
    </div>
</div>

@Component({
    selector: ".indication-grid-filter-container",
    templateUrl: "/Views/Components/Indication/IndicationGridFilter.html"
})
export class IndicationGridFilterComponent {

    private searchTimeout;

    @Input() allowEdit: boolean;

    @Output() addNewItemEvent = new EventEmitter();
    @Output() searchItemsEvent: EventEmitter<any> = new EventEmitter();

    searchString: string;    

    onSearchStringChanged() {
        clearTimeout(this.searchTimeout);
        this.searchTimeout = setTimeout(() => {
            this.searchItemsEvent.next(this.searchString.toLowerCase());
        }, 500);
    }

    onAddNewItem() {
        this.addNewItemEvent.next(null);
    }

}

Child Component with control pagination

<table class="table table-hover grid-table" width="100%">
    <thead class="grid-header">
        <tr style="vertical-align:top;" class="grid-header-title">
            <th width="40%">
                <div>
                    <span>Indication description</span>
                </div>
            </th>
            <th width="40%">
                <div class="dropdown indications-category-filter">
                    <span>Category&nbsp;&nbsp;</span>
                    <span>
                        <img src="/Content/images/filterActive.png" style="width: 16px" *ngIf="currentIndicationTypeId === 0" />
                        <img src="/Content/images/filterSelected.png" style="width: 16px" *ngIf="currentIndicationTypeId > 0" />
                    </span>
                    <a href="#" class="dropdown-toggle" id="dropdownMenuCategoryActive" data-toggle="dropdown" aria-haspopup="true" aria-expanded="true"><span class="caret"></span></a>
                    <ul class="dropdown-menu" aria-labelledby="dropdownMenuCategoryActive">
                        <li class="divider-bottom">
                            <table style="width:180px;" cellpadding="5" cellspacing="5">
                                <tr>
                                    <td>
                                        <span style="font-weight:bolder">&nbsp;&nbsp;Refine</span>
                                    </td>
                                    <td align="right">
                                        <span style="font-weight:normal">Results:&nbsp;{{itemsCount}}</span>
                                    </td>
                                </tr>
                            </table>
                        </li>
                        <li class="divider-bottom">
                            <a href="#" (click)="selectIndicationType(0)" class="indications-category-filter-item">
                                <span class="glyphicon glyphicon-none" aria-hidden="true"></span>
                                Show All
                                <span class="badge">
                                    {{totalCount}}
                                </span>
                            </a>
                        </li>
                        <li *ngFor="let item of stats">
                            <a href="#" (click)="selectIndicationType(item.Id)" class="indications-category-filter-item">
                                <span class="glyphicon glyphicon-ok" aria-hidden="true"
                                      *ngIf="currentIndicationTypeId == item.Id"></span>
                                <span class="glyphicon glyphicon-none" aria-hidden="true"
                                      *ngIf="currentIndicationTypeId != item.Id"></span>
                                {{item.Name}}
                                <span class="badge">
                                    {{item.Count}}
                                </span>
                            </a>
                        </li>
                    </ul>
                </div>
            </th>
            <th width="10%">
                <div>
                    <span>#Templates</span>
                </div>
            </th>
            <th width="10%"></th>
        </tr>
    </thead>
    <tbody>
        <tr *ngFor="let item of items | paginate: { id: tabId, itemsPerPage: pageSize, currentPage: currentPage, totalItems: itemsCount }" class="indication-data-row">
            <td>
                {{item.ValueName}}
            </td>
            <td>
                <span *ngFor="let category of getIndicationCategories(item)">
                    <span>{{category.Name}}</span>
                </span>
            </td>
            <td>
                {{item.TemplateCount}}
            </td>
            <td>
                <div *ngIf="allowEdit" class="indications-grid-indication-operations">
                    <span style="display: inline">
                        <a (click)="editItem(item)">Edit</a>&nbsp;|&nbsp;<a (click)="changeArchiveStatus(item)">{{archiveOperationLabel}}</a>
                    </span>
                </div>
            </td>
        </tr>
        <tr *ngIf="items.length == 0">
            <td colspan="3">
                <table width="100%">
                    <tr style="height:150px">
                        <td></td>
                    </tr>
                    <tr>
                        <td align="center">
                            <span class="middle-size-information-label">No such indication exists.</span>
                        </td>
                    </tr>
                    <tr style="height:150px">
                        <td></td>
                    </tr>
                </table>
            </td>
        </tr>
    </tbody>
</table>
<div>
    <div class="pagination-controls" (pageChange)="getPage($event)" id="{{tabId}}"></div>
</div>


    import IndicationModel = OrderTemplateTool.Web.Models.Indications.IndicationModel;
    import SentencePartModelBase = OrderTemplateTool.Core.Sentences.Indications.MultiselectSentencePartModel;
    import { IndicationGridFilterComponent } from "../Indication/IndicationGridFilter";
    import { Component, Input, Output, NgZone, AfterViewInit, Inject, EventEmitter, ViewChild, OnInit, ViewChildren, OnChanges, QueryList } from "@angular/core";
    
    import { DictionaryItem } from "../../Models/dictionaryItem";
    import { PaginationComponent } from "../Common/Pagination";
    import { IndicationManager } from "../../Bll/IndicationManager";
    import {BaseComponent} from "../baseComponent";
    import { Dictionaries } from "../../Models/DynamicDictionaries";
    import IndicationDictionaryModel = OrderTemplateTool.Web.Models.Indications.IndicationDictionaryModel;
    import { Observable } from "rxjs/Observable";
    import { Subject } from "rxjs";
    
    @Component({
        selector: "div.indications-grid-tab",
        templateUrl: "/Views/Components/Indication/IndicationsGridTab.html"
    })
    export class IndicationsGridTabComponent implements OnInit  {
    
        @Input() allowEdit: boolean;
        @Input() isActive = true;
        @Output() onEditItem = new EventEmitter();
        @Output() onChangeArchiveStatus = new EventEmitter();
    
        @Output() searchFilter: EventEmitter<any> = new EventEmitter();
    
        
        indicationTypes: DictionaryItem[];
        currentIndicationTypeId = 0;
        archiveOperationLabel: string;
    
        constructor(@Inject(IndicationManager) private indicationManager: IndicationManager, private indicationGridFilterComponent: IndicationGridFilterComponent) {
            this.indicationTypes = this.indicationManager.getIndicationTypes();
        }
    
        tabId: string;
        totalCount: number;
        items: IndicationDictionaryModel[] = [];
        itemsCount: number;
        stats: any[];    
        pageSize = IndicationManager.indicationsGridItemsPerPage;
        currentPage: number;    
    
        ngOnInit() {
            this.tabId = this.isActive ? "indications_active" : "indications_archive";
            this.loadInitialState(1);
            this.archiveOperationLabel = this.isActive ? "Archive" : "Restore";
        }
    
    
    
        loadInitialState(pageNumber: number, searchString?: string) {
            return this.indicationManager.getIndicationsGridInitialState(pageNumber, this.isActive, (response) => {
                this.stats = response.stats;
                this.totalCount = response.totalCount;
                this.itemsCount = response.itemsCount;
                this.items = response.items;
                this.currentPage = pageNumber;
            }, searchString);
        }
    
        getPage(pageNumber: number) {
            console.log('getPage');
            const SearchPattern = $("#gridSearchPatternId");
            console.log(SearchPattern.val());
            //console.log(this.searchItemsEvent);
            this.searchFilter.emit;
    
    
            this.indicationManager.getIndicationsPageByActivity(
                pageNumber,
                this.isActive,
                this.currentIndicationTypeId,
                (response) => {
                    this.itemsCount = response.ItemsCount;
                    this.items = response.Indications;
                    this.currentPage = pageNumber;
                },
                SearchPattern.val()
            );
        }
    
        getStats() {
            this.indicationManager.getIndicationCategoryStatsByActivity(            
                this.isActive,
                (response) => {
                    if (response.success) {                    
                        this.stats = response.stats;
                        this.totalCount = response.totalCount;
                    }
                }
            );
        }    
    
        selectIndicationType(id: number) {
            this.currentIndicationTypeId = id;
            this.getPage(1);
        }
    
        editItem(item) {
            this.onEditItem.next(item);
        }
    
        changeArchiveStatus(item) {
            this.onChangeArchiveStatus.next(item);
        }
    
        private getIndicationDescription(item: IndicationModel): string {
            return item.Sentence != null ?
                item.Sentence.CompiledText :
                "";
        }
    
        private getIndicationCategories(item: OrderTemplateTool.Web.Models.Indications.IndicationDictionaryModel): DictionaryItem[] {
            var result: DictionaryItem[] = [];
            var d: Dictionaries = new Dictionaries();
            for (var i = 0; i < d.IndicationType.length; i++) {
                if (d.IndicationType[i].Id == parseInt(item.DictionaryEnumValue.toString())) {
                    return [d.IndicationType[i]];
                }
            }
    
            return [];
        }
    
    }

Solution

  • Angular recommends sharing data between non parent/child relationships through services. You can place values into this service with an RxJS pipe that debounces the search input (this replaces your set/clearTimeout logic):

    For example:

    component1.ts

    class Component1 {
      constructor(private readonly searchService: SearchService) {}
      
    
      onSearchStringChanged(searchString: string) {
        this.searchService.searchNext(searchString);
      }
    }
    

    search.service.ts

    @Injectable({providedIn: 'root'})
    class SearchService {
      private search = new Subject<string>();
      searchNext = (searchString: string) => this.search.next(string);
      searchValue$ = this.search.pipe(
        map(value => value.toLowerCase()),
        debounceTime(500),
      );
    }
    

    component2.ts

    class Component2 {
      searchValue$ = this.searchService.searchValue$;
      constructor(private searchService: SearchService) {}
    }
    

    In the template you can access this observable stream of values with the async pipe, and in your code you can subscribe to the stream. (See the RxJS docs)

    <div> The search value is {{ searchValue$ | async }} </div>
    
      this.searchValue$.subscribe(value => console.log("This is called whenever a new search value is emitted"))
    

    You can access this search value anywhere in the injection scope of SearchService. Because of @Injectable({providedIn: 'root'}) this would be the same service in your entire app. If this combination of component1/2 is repeatable, and they each need their own SearchService, then remove {providedIn: 'root'} and instead add SearchService to a parent component's providers that contains only 1 comp1/comp2 combo:

    @Component({ providers: [SearchService] })
    class ParentOfBothComponent {}
    

    Another tip: With a Subject the order of .next and .subscribe matters, usually this is not a problem, but if you see some first values going missing, you can try to switch to a BehaviorSubject which will replay the last value to late subscribers:

    private search = new BehaviorSubject<string>("");