Estoy usando la transclusión en angular para definir una plantilla de vista fija y definir ranuras para contenido dinámico.

El componente es app-filter-details y su plantilla es:

<div id="details-wrapper">
    <div class="details-nav-bar">
        {{ navbarContent }}
    </div>
    <div class="details-wrapper">
        <div class="details-top">
            <ng-content select="filter-details-title"></ng-content>
        </div>
        <div *ngIf="!mobile()>
            <ng-content select="filter-details-content"></ng-content>
        </div>
    </div>
</div>
<app-bottom-sheet *ngIf="mobile() && mapStateService.isMaximized()">
    <div class="details-body moute-scrollable">
        <ng-content select="filter-details-content-bottomsheet"></ng-content>
    </div>
</app-bottom-sheet>

Y luego otros componentes lo usan como este:

<app-filter-details (backClicked)="goBack()" [whiteBackground]="true">
    <filter-details-title>
        {{ contentOfDetailsTitle }}
    </filter-details-title>
    <filter-details-content>
        <app-whatever-content></app-whatever-component>
    </filter-details-content>
    <filter-details-content-bottomsheet>
        <app-whatever-content></app-whatever-component>
    </filter-details-content-bottomsheet>
</app-filter-details>

Eso básicamente carga el contenido de filter-details-title, filter-details-content y filter-details-content-bottomsheet. Y lo que hago es configurar siempre el mismo contenido en filter-details-content y filter-details-content-bottomsheet y se mostrará uno u otro dependiendo de si se ve desde una pantalla móvil:

mobile() {
    return (window.innerWidth <= 576);
}

Hasta ahora todo bien, carga el contenido de ambos y solo se muestra uno dependiendo del ancho de la ventana. El problema es que este componente del contenido app-whatever-content se carga dos veces y realiza algunas llamadas api en init. Puedo ver que todas las llamadas de API se realizan dos veces (una para el componente en la hoja inferior y otra para el otro).

Los cambios en la versión móvil no solo reordenan el componente, es un diseño completamente diferente, en el móvil muestra flotante emulando una hoja inferior (material.io/components/sheets-bottom) mientras que en no móvil se muestra dentro del componente principal .

¿Hay alguna manera de que pueda cargar solo uno de los dos componentes o evitar que haga las llamadas adicionales de la API de alguna manera?

1
peprumo 25 feb. 2020 a las 12:31

2 respuestas

La mejor respuesta

El problema

Desea que su filtro se represente en diferentes lugares de su diseño según el tamaño de la pantalla.

Su solución actual

Su control de niños se encarga del diseño: BUENO

Su control secundario solicita varias versiones de su filtro - MALO

¿Por qué es esto malo?

El segundo punto es malo porque desea mantener una sola instancia del componente de filtro por razones de rendimiento.

Si pensáramos en cómo su componente hijo tiene una API (que efectivamente lo hace), se vería así:

child: {
  title: string;
  filterTop: FilterComponent;
  filterBottom: FilterComponent;
}

Este es un diseño deficiente en cualquier idioma *, ya que al padre no le importa dónde se representa el filtro, solo le importa pasar el filtro. Su implementación se ha filtrado de su componente hijo.

* En cualquier tipo de lenguaje OO, pasaríamos la misma referencia si quisiéramos referirnos a una sola instancia. Lamentablemente estamos hablando de HTML aquí.

Entonces, ¿cuál es la solución?

La solución para pasar en una sola instancia es pasar solo en una instancia. Eso suena obvio, pero las razones detrás de esto son más interesantes. Debería pensar en simplificar la API de su componente hijo y pensar en elementos distintos.

Pregunta:

¿Qué componentes necesita mi hija?

Responder:

¡Un título y un filtro!

La respuesta probablemente no sea "Un título y un filtro por punto de interrupción".

Su componente principal debería verse un poco más así cuando haya terminado:

<app-filter-details (backClicked)="goBack()" [whiteBackground]="true">
    <filter-details-title>
        {{ contentOfDetailsTitle }}
    </filter-details-title>
    <filter-details>
        <app-whatever-content></app-whatever-component>
    </filter-details>
</app-filter-details>

Hay al menos 3 soluciones en las que puedo pensar aquí

  1. Adjunte manualmente el filtro a un padre en el DOM dependiendo del tamaño de la pantalla.

Qué asco, esto no es muy angular.

  1. CSS - pedido de flexbox + consultas de medios

Dependiendo de la complejidad de su diseño (supongo que lo ha simplificado para la pregunta), use flexbox y consultas de medios para reordenar los elementos del contenedor dentro de su componente hijo. Si está utilizando un marco CSS, es posible que ya tengan utilidades para esto.

Demostración simple de reordenamiento (usando clase en lugar de consulta de medios):

var container = document.getElementById('container');
var toggle = document.getElementById('toggle');
toggle.addEventListener('click', toggleOrder);

function toggleOrder() {
  container.classList.toggle('reorder'); 
}
.container {
  display: flex;
  flex-direction: column;
}
.reorder .first {
  order: 3;
}

.reorder .third {
  order: 1;
}
<div id="container" class="container">
<div class="first">
 1
</div>
<div class="second">
 2
</div>
<div class="third">
 3
</div>
</div>

<button id="toggle">
  Toggle order
</button>
  1. Almacene su estado de filtro en un servicio (y mantenga su diseño actual)

Esta es probablemente la solución más angular. Su servicio es responsable de almacenar el estado actual. Su control de filtro es responsable de indicar al servicio que realice nuevas solicitudes de API (después de un evento dirigido por el usuario). Su padre es responsable de indicarle al servicio que realice la primera solicitud de API en carga (si corresponde).

La clave para esto es separar la idea de que una suscripción de servicio sea directamente responsable de una solicitud HTTP. Simplemente puede usar un patrón similar a este para dividir las suscripciones de las solicitudes de datos:

export class SearchService {
  constructor(private http: HttpClient) {}

  // TODO: emit null value when parent component is destroyed?
  private search$: Subject<SearchResults> = new ReplaySubject<SearchResults>(1);

  subscribeSearchResults(): Observable<SearchResults> {
    return this.search$.asObservable();
  }

  submitSearchRequest(criteria): void {
    const url = this.getSearchUrl(criteria); // TODO: implement

    this.http.get<SearchCriteria>(url).subscribe(response => {
      this.search$.next(response);
    });
  }
}

Y su componente interactuaría con el servicio de esta manera:

results: SearchResults;

private destroyed: Subject<void> = new Subject<void>();

ngOnInit() {
  this.searchService.subscribeSearchResults().pipe(
    takeUntil(this.destroyed)
  ).subscribe(result => {
    this.results = results;
  });
}

ngOnDestroy() {
  this.destroyed.next();
  this.destroyed.complete();
}

onSearchSubmit(): void {
  this.searchService.submitSearchRequest(this.criteria);
}

Conclusión

Mi preferencia sería la solución 2. Es un problema de interfaz de usuario que se puede resolver con una solución de interfaz de usuario simple. La solución 3 requeriría una buena cantidad de código adicional y dolor potencial.

1
Kurt Hamilton 25 feb. 2020 a las 11:09

Supongo que podría crear una entrada y pasar si se muestra en el móvil.

@Input() showInMobile: boolean;

Para que pueda establecer su valor en el diseño del componente:

<app-filter-details (backClicked)="goBack()" [whiteBackground]="true">
    <filter-details-title>
        {{ contentOfDetailsTitle }}
    </filter-details-title>
    <filter-details-content>
        <app-whatever-content [showInMobile]="false"></app-whatever-component>
    </filter-details-content>
    <filter-details-content-bottomsheet>
        <app-whatever-content [showInMobile]="true"></app-whatever-component>
    </filter-details-content-bottomsheet>
</app-filter-details>

Y luego agregue una comprobación onResize ():

@HostListener('window:resize', ['$event'])
onResize() {
    if (this.showInMobile === this.mobile()) {
        if (!this.searching) {
            this.searching = true;
            // subscribe to api calls
        }
    } else {
        // unsubscribe if needed
        this.searching = false;
    }
}

Y necesita la búsqueda privada var inicializada en falso:

private searching = false;
1
jeprubio 25 feb. 2020 a las 11:53