fix(algorithms): render sliders only after first browser render

ngx-slider throws during prerender (no layout APIs on the server).
Its ngOnDestroy then calls disconnect() on null, so about 1 in 10
prerender builds failed. Gate fractal and pendulum sliders on a
signal set by afterNextRender.
This commit is contained in:
lobo committed 2026-10-21 08:34:39 +02:00
1 parent 82228e9141
commit 1cff9e6eb2
4 files changed
+55 -33

No files matched your search

@@ -31,7 +31,11 @@
</div>
<div style="display: flex; align-items: center; gap: 10px;">
<mat-icon>zoom_out</mat-icon>
<ngx-slider [(value)]="sliderValue" [options]="options" (valueChange)="onSliderChange($event)" ></ngx-slider>
@if (sliderReady()) {
<ngx-slider [(value)]="sliderValue" [options]="options" (valueChange)="onSliderChange($event)" ></ngx-slider>
} @else {
<div class="grow h-[60px]"></div>
}
<mat-icon>zoom_in</mat-icon>
</div>
</div>
@@ -1,4 +1,4 @@
import {Component, OnInit, signal} from '@angular/core';
import {afterNextRender, Component, OnInit, signal} from '@angular/core';
import {Information} from '../information/information';
import {MatCard, MatCardContent, MatCardHeader, MatCardTitle} from '@angular/material/card';
import {TranslatePipe} from '@ngx-translate/core';
@@ -39,6 +39,13 @@ import {clampZoom, initialView, iterationsForZoom, zoomAroundPoint} from './frac
styleUrl: './fractal.component.scss',
})
export class FractalComponent implements OnInit {
// ngx-slider throws on the server (no layout APIs), so render it in the browser only
readonly sliderReady = signal(false);
constructor() {
afterNextRender(() => this.sliderReady.set(true));
}
algoInformation: AlgorithmInformation = {
title: 'FRACTAL.EXPLANATION.TITLE',
entries: [
@@ -5,39 +5,43 @@
<mat-card-content>
<app-information [algorithmInformation]="algoInformation"/>
<div class="flex flex-col mb-4">
<div class="grid grid-cols-2 tablet:grid-cols-1 tablet:gap-4 mb-6">
<div class="flex items-center gap-4 mr-4">
<p class="w-[100px] shrink-0 m-0 text-sm">{{ 'PENDULUM.TRAIL_DECAY_TIME' | translate }}</p>
<ngx-slider class="grow" [(value)]="trailDecay" [options]="trailDecayOptions" ></ngx-slider>
</div>
<div class="flex items-center gap-4 mr-4">
<p class="w-[100px] shrink-0 m-0 text-sm">{{ 'PENDULUM.ATTRACTION' | translate }}</p>
<ngx-slider class="grow" [(value)]="g" [options]="gravityOptions" ></ngx-slider>
</div>
@if (sliderReady()) {
<div class="grid grid-cols-2 tablet:grid-cols-1 tablet:gap-4 mb-6">
<div class="flex items-center gap-4 mr-4">
<p class="w-[100px] shrink-0 m-0 text-sm">{{ 'PENDULUM.TRAIL_DECAY_TIME' | translate }}</p>
<ngx-slider class="grow" [(value)]="trailDecay" [options]="trailDecayOptions" ></ngx-slider>
</div>
<div class="flex items-center gap-4 mr-4">
<p class="w-[100px] shrink-0 m-0 text-sm">{{ 'PENDULUM.ATTRACTION' | translate }}</p>
<ngx-slider class="grow" [(value)]="g" [options]="gravityOptions" ></ngx-slider>
</div>
<div class="flex items-center gap-4 mr-4">
<p class="w-[100px] shrink-0 m-0 text-sm">{{ 'PENDULUM.L1_LENGTH' | translate }}</p>
<ngx-slider class="grow" [(value)]="l1" [options]="lengthOptions" ></ngx-slider>
</div>
<div class="flex items-center gap-4 mr-4">
<p class="w-[100px] shrink-0 m-0 text-sm">{{ 'PENDULUM.L2_LENGTH' | translate }}</p>
<ngx-slider class="grow" [(value)]="l2" [options]="lengthOptions" ></ngx-slider>
</div>
<div class="flex items-center gap-4 mr-4">
<p class="w-[100px] shrink-0 m-0 text-sm">{{ 'PENDULUM.L1_LENGTH' | translate }}</p>
<ngx-slider class="grow" [(value)]="l1" [options]="lengthOptions" ></ngx-slider>
</div>
<div class="flex items-center gap-4 mr-4">
<p class="w-[100px] shrink-0 m-0 text-sm">{{ 'PENDULUM.L2_LENGTH' | translate }}</p>
<ngx-slider class="grow" [(value)]="l2" [options]="lengthOptions" ></ngx-slider>
</div>
<div class="flex items-center gap-4 mr-4">
<p class="w-[100px] shrink-0 m-0 text-sm">{{ 'PENDULUM.M1_MASS' | translate }}</p>
<ngx-slider class="grow" [(value)]="m1" [options]="massOptions" ></ngx-slider>
</div>
<div class="flex items-center gap-4 mr-4">
<p class="w-[100px] shrink-0 m-0 text-sm">{{ 'PENDULUM.M2_MASS' | translate }}</p>
<ngx-slider class="grow" [(value)]="m2" [options]="massOptions" ></ngx-slider>
</div>
<div class="flex items-center gap-4 mr-4">
<p class="w-[100px] shrink-0 m-0 text-sm">{{ 'PENDULUM.M1_MASS' | translate }}</p>
<ngx-slider class="grow" [(value)]="m1" [options]="massOptions" ></ngx-slider>
</div>
<div class="flex items-center gap-4 mr-4">
<p class="w-[100px] shrink-0 m-0 text-sm">{{ 'PENDULUM.M2_MASS' | translate }}</p>
<ngx-slider class="grow" [(value)]="m2" [options]="massOptions" ></ngx-slider>
</div>
<div class="flex items-center gap-4 mr-4 col-span-full">
<p class="w-[100px] shrink-0 m-0 text-sm">{{ 'PENDULUM.DAMPING' | translate }}</p>
<ngx-slider class="grow" [(value)]="damping" [options]="dampingOptions" ></ngx-slider>
</div>
</div>
<div class="flex items-center gap-4 mr-4 col-span-full">
<p class="w-[100px] shrink-0 m-0 text-sm">{{ 'PENDULUM.DAMPING' | translate }}</p>
<ngx-slider class="grow" [(value)]="damping" [options]="dampingOptions" ></ngx-slider>
</div>
</div>
} @else {
<div class="mb-6 h-[200px]"></div>
}
<div class="flex flex-wrap gap-3 mb-4">
<button mat-raised-button color="primary" (click)="pushPendulum(true)">
@@ -1,4 +1,4 @@
import {Component, signal} from '@angular/core';
import {afterNextRender, Component, signal} from '@angular/core';
import {BabylonCanvas, RenderConfig, SceneEventData} from '../../../shared/components/render-canvas/babylon-canvas.component';
import {MatCard, MatCardContent, MatCardHeader, MatCardTitle} from '@angular/material/card';
@@ -33,6 +33,13 @@ import {PendulumCpuStrategy} from './strategies/pendulum-cpu.strategy';
})
class PendulumComponent {
// ngx-slider throws on the server (no layout APIs), so render it in the browser only
readonly sliderReady = signal(false);
constructor() {
afterNextRender(() => this.sliderReady.set(true));
}
algoInformation: AlgorithmInformation = {
title: 'PENDULUM.EXPLANATION.TITLE',
entries: [