How to Create a Reusable Component in Angular

Asked

Viewed 1,211 times

2

I am with a system where all modules have a standardized record listing screen with pagination. I adapted a pagination code I found on the Internet to make pagination on demand. However, instead of repeating this code on all listing screens I would like to create a component to just be called on these screens. I do it this way:

Adapted Service that makes the pagination

import { Injectable } from '@angular/core';

@Injectable({
  providedIn: 'root'
})
export class PagerService {

  getPager(totalItems: number, currentPage: number = 0, pageSize: number = 10) {
    // calculate total pages
    let totalPages = Math.ceil(totalItems / pageSize);

    // ensure current page isn't out of range
    if (currentPage < 0) {
      currentPage = 0;
    } else if (currentPage > totalPages) {
      currentPage = totalPages;
    }

    let startPage: number, endPage: number;
    if (totalPages <= 10) {
      // less than 10 total pages so show all
      startPage = 1;
      endPage = totalPages;
    } else {
      // more than 10 total pages so calculate start and end pages
      if (currentPage <= 5) {
        startPage = 1;
        endPage = 10;
      } else if (currentPage + 3 >= totalPages) {
        startPage = totalPages - 9;
        endPage = totalPages;
      } else {
        startPage = currentPage - 4;
        endPage = currentPage + 3;
      }
    }

    // calculate start and end item indexes
    let startIndex = (currentPage) * pageSize;
    let endIndex = Math.min(startIndex + pageSize - 1, totalItems - 1);

    // create an array of pages to ng-repeat in the pager control
    let pages = Array.from(Array((endPage + 1) - startPage).keys()).map(i => startPage + i);

    // return object with all pager properties required by the view
    return {
      totalItems: totalItems,
      currentPage: currentPage,
      pageSize: pageSize,
      totalPages: totalPages,
      startPage: startPage,
      endPage: endPage,
      startIndex: startIndex,
      endIndex: endIndex,
      pages: pages
    };
  }
}

Listing Component

import { AppSettings } from './../../settings/app-settings';
import { PageEquipamento } from './../entity/page-equipamento';
import { Component, OnInit } from '@angular/core';
import AppHelper from '../../helper/AppHelper';
import { LoadingHugeComponent } from '../../loading/huge/loading-huge.component';
import { Title } from '@angular/platform-browser';
import { SharedDataService } from '../../services/SharedDataService';
import { Router, ActivatedRoute } from '@angular/router';
import { EquipamentosService } from '../services/equipamentos.service';
import { ToastrService } from 'ngx-toastr';
import { environment } from '../../../environments/environment';
import { Equipamento } from '../../entity/equipamento';
import { PagerService } from '../../services/pager.service';
import { Observable, of } from 'rxjs';
declare var $: any;

@Component({
  selector: 'app-listar-equipamentos',
  templateUrl: './listar-equipamentos.component.html',
  styleUrls: ['./listar-equipamentos.component.css'],
  preserveWhitespaces: true
})
export class ListarEquipamentosComponent implements OnInit {

  //Rotas
  private appHelper: AppHelper = new AppHelper();
  public loadingHugeComponent = LoadingHugeComponent;
  public listUrls;
  //Equipamento Selecionado
  public indexEquipamentoSelecionado: number;
  public nomeSelecionado: string;
  //Paginação
  pager: any = {};
  pagedItems: any[];
  //Page Equipamentos  
  private subPageEquipamento;
  public pageEquipamento: PageEquipamento = new PageEquipamento();


  constructor(private titleService: Title, private sharedDataService: SharedDataService,
    private router: Router,
    private route: ActivatedRoute,
    private equipamentosService: EquipamentosService,
    private toastrService: ToastrService,
    private pagerService: PagerService
  ) {
    const allowed = ['listarEquipamentos', 'gravarEquipamento'];
    this.listUrls = this.appHelper.urlNotAllowed(allowed, environment.urls);

  }

  ngOnInit() {
    this.loadRegistrosPaginados();
  }

  ngOnDestroy() {
    this.subPageEquipamento.unsubscribe();
  }

  confirmaExcluirEquipamento(i: number) {
    this.indexEquipamentoSelecionado = i;
    this.nomeSelecionado = this.pageEquipamento.listaEquipamentos[i].nome;
    $('#modalLista').modal('show');
  }

  excluirEquipamento() {
    setTimeout(() => {
      this.equipamentosService.excluirEquipamento(this.pageEquipamento.listaEquipamentos[this.indexEquipamentoSelecionado].id).subscribe(
        resp => {
          this.toastrService.info(this.pageEquipamento.listaEquipamentos[this.indexEquipamentoSelecionado].nome, ' Excluido com sucesso!');
          this.pageEquipamento.listaEquipamentos.splice(this.indexEquipamentoSelecionado, 1);
          this.loadRegistrosPaginados();
        }, error => {
          this.toastrService.error('Não foi possível excluir');
        }
      );
    }, 900);
  }

  //Carrega os registros paginados
  loadRegistrosPaginados() {
    this.subPageEquipamento = this.equipamentosService.getEquipamentosPage(AppSettings.RESGISTROS_POR_PAGINA)
      .subscribe(resp => {
        this.pageEquipamento.listaEquipamentos = resp.content;
        this.pageEquipamento.totalElements = resp.totalElements;
        //Gera o HTML da Paginação
        this.pager = this.pagerService.getPager(this.pageEquipamento.totalElements, 1, AppSettings.RESGISTROS_POR_PAGINA);
        this.pagedItems = this.pageEquipamento.listaEquipamentos.slice(this.pager.startIndex, this.pager.endIndex + 1);
      });
  }

  //Método chamado pela paginação
  setPage(page: number) {
    this.subPageEquipamento = this.equipamentosService.getEquipamentosPage(AppSettings.RESGISTROS_POR_PAGINA, page - 1)
      .subscribe(resp => {
        this.pageEquipamento.listaEquipamentos = resp.content;
        this.pageEquipamento.totalElements = resp.totalElements
      });

    this.pager = this.pagerService.getPager(this.pageEquipamento.totalElements, page, AppSettings.RESGISTROS_POR_PAGINA);
    this.pagedItems = this.pageEquipamento.listaEquipamentos.slice(this.pager.startIndex, this.pager.endIndex + 1);
  }

}

HTML of the Pagination

  <!--Paginação-->
  <div *ngIf="!(pageProposta.listaEquipamentos?.length===0)">
    <div class="container">
      <div class="text-center">
        <!-- items being paged -->
        <div *ngFor="let item of pagedItems">{{item.name}}</div>

        <!-- pager -->
        <ul *ngIf="pager.pages && pager.pages.length" class="pagination">
          <li [ngClass]="{disabled:pager.currentPage === 1}">
            <a [routerLink]="" (click)="setPage(1)">Primeiro</a>
          </li>
          <li *ngFor="let page of pager.pages" [ngClass]="{active:pager.currentPage === page}">
            <a [routerLink]="" (click)="setPage(page)">{{page}}</a>
          </li>
          <li [ngClass]="{disabled:pager.currentPage === pager.totalPages}">
            <a [routerLink]="" (click)="setPage(pager.totalPages)">Último</a>
          </li>
        </ul>
      </div>
    </div>
  </div>

Using this logic it is possible to adapt it to create a reusable component for all system listing screens?

Thank you

  • you just add the <app-list-equipment></app-list-equipment> tag where you want to use this component of yours

  • You want to compose the pager ?

  • If you want a componentized table take a look at my example: https://stackblitz.com/edit/tabela-html?file=src%2Fapp%2Fapp.component.html I created a table that can be reused in a very simple way using ng-template and vc can adapt to leave with paging and item deletion control and everything else...

  • The problem I’m having is with the subscribe inside the setPage() method, because when this method is called by HTML I reload the equipment list (this.pageEquipment.listsEquipments) if I put this method in the separate component I won’t be able to reload it...

2 answers

1

André, I advise you to create a module (Shared) and within this module you create all the components that will be shared with all or most of the system, for example, in my application I use the Shared module to share error validations messages.

It would work more or less as follows:

module Shared has the components list-records and validate-form

Then to call on other components is just you use the selector of your component list-records.

Note: don’t forget to export the module and import if necessary!

  • I understand, but the problem I’m facing is in the setPage method of the new component I’m going to create. For example, I would create the paging component inside the Shared folder, ok. In the html of this component I would put the HTML I posted above, however it would call the setPage() method, only that as the paging is on demand I make a new request within this method and as the equipment list is in another Component it would not be possible to load it... I wanted to see if there was a way to fix it...

-1

There are many ways to write, and it may be difficult. First, let me give you some general reminders:

  • A service is a state container. It has only data and no template;

  • A component is a state container + an html template. Since this state container can be delegated to a service (when the template accesses service variables);

  • What makes a service special is its dependency injection behavior; A service has its life cycle controlled by where you put it as "Provider" (read the documentation dependency injection section).

  • The "providedIn: 'root'" injects the service automatically as Singleton (global variable). Try taking that line from the service note, it’ll be a mistake. The error will only be solified when you inject the service as "Provider" into another component or module. That is: a service is not necessarily a service, it can be coupled to anything.

If you put a service on providers of a component it is created and destroyed with that component. The same thing if put in the providers of a module. And there is also the viewProviders, which makes the service available also for all child components.

Okay understanding how dependency injection works, let’s go to its component:

  • Think of your component as a black box. Its responsibility is to make pagination of ANYTHING. That is, rule number 1 is that it cannot have any specific business rule of its application.

  • Ask how far the responsibility of your "component" goes (not the angular component, the "thing" you want to create). You can decide, for example, that your "component" should not worry about generating HTML and CSS, but your consumer should take care of it. Greater responsibilities bring less flexibility, but more ease of use. Smaller responsibilities bring more flexibility, but less ready stuff.

  • If your component is not generating html and CSS, it can be a service that exposes useful variables and functions to a consumer then use them in a component. Example: export: "Current Page", "Nexmapágina()", "irParaPágina()". And the consumer who decides whether to put it on a button or anything else.

  • In the other case, when creating components, the use of @Input and @Output are important keys for decoupling. For example, you can write a pagination component that has no responsibility to specify HOW the page will be updated. This component can emit a @Output when clicking on a button, hence the parent component will decide what to do with that event. In this way, its pagination component becomes more generic, better obeying the principle of sole responsibility.

  • Both the use of Providers, Output or Input refer to the same problem: Which component should I save my status on? Which component should which variable be on? On the parent? On the child? This is an essential problem of all 3 famous frameworks at the moment. Learning this in Angular, you break learn important things to use any other framework.

Browser other questions tagged

You are not signed in. Login or sign up in order to post.