1010 * Directions that can be used when setting sticky positioning.
1111 * @docs -private
1212 */
13+ import { afterNextRender , Injector } from '@angular/core' ;
1314import { Direction } from '@angular/cdk/bidi' ;
1415import { _CoalescedStyleScheduler } from './coalesced-style-scheduler' ;
1516import { StickyPositioningListener } from './sticky-position-listener' ;
@@ -41,6 +42,7 @@ export class StickyStyler {
4142 private _stickyColumnsReplayTimeout : number | null = null ;
4243 private _cachedCellWidths : number [ ] = [ ] ;
4344 private readonly _borderCellCss : Readonly < { [ d in StickyDirection ] : string } > ;
45+ private _destroyed = false ;
4446
4547 /**
4648 * @param _isNativeHtmlTable Whether the sticky logic should be based on a table
@@ -55,6 +57,7 @@ export class StickyStyler {
5557 * the component stylesheet for _stickCellCss.
5658 * @param _positionListener A listener that is notified of changes to sticky rows/columns
5759 * and their dimensions.
60+ * @param _tableInjector The table's Injector.
5861 */
5962 constructor (
6063 private _isNativeHtmlTable : boolean ,
@@ -64,6 +67,7 @@ export class StickyStyler {
6467 private _isBrowser = true ,
6568 private readonly _needsPositionStickyOnElement = true ,
6669 private readonly _positionListener ?: StickyPositioningListener ,
70+ private readonly _tableInjector ?: Injector ,
6771 ) {
6872 this . _borderCellCss = {
6973 'top' : `${ _stickCellCss } -border-elem-top` ,
@@ -92,17 +96,16 @@ export class StickyStyler {
9296 continue ;
9397 }
9498
95- elementsToClear . push ( row ) ;
96- for ( let i = 0 ; i < row . children . length ; i ++ ) {
97- elementsToClear . push ( row . children [ i ] as HTMLElement ) ;
98- }
99+ elementsToClear . push ( row , ...( Array . from ( row . children ) as HTMLElement [ ] ) ) ;
99100 }
100101
101102 // Coalesce with sticky row/column updates (and potentially other changes like column resize).
102- this . _coalescedStyleScheduler . schedule ( ( ) => {
103- for ( const element of elementsToClear ) {
104- this . _removeStickyStyle ( element , stickyDirections ) ;
105- }
103+ this . _afterNextRender ( {
104+ write : ( ) => {
105+ for ( const element of elementsToClear ) {
106+ this . _removeStickyStyle ( element , stickyDirections ) ;
107+ }
108+ } ,
106109 } ) ;
107110 }
108111
@@ -147,53 +150,61 @@ export class StickyStyler {
147150 }
148151
149152 // Coalesce with sticky row updates (and potentially other changes like column resize).
150- this . _coalescedStyleScheduler . schedule ( ( ) => {
151- const firstRow = rows [ 0 ] ;
152- const numCells = firstRow . children . length ;
153- const cellWidths : number [ ] = this . _getCellWidths ( firstRow , recalculateCellWidths ) ;
154-
155- const startPositions = this . _getStickyStartColumnPositions ( cellWidths , stickyStartStates ) ;
156- const endPositions = this . _getStickyEndColumnPositions ( cellWidths , stickyEndStates ) ;
157-
158- const lastStickyStart = stickyStartStates . lastIndexOf ( true ) ;
159- const firstStickyEnd = stickyEndStates . indexOf ( true ) ;
160-
161- const isRtl = this . direction === 'rtl' ;
162- const start = isRtl ? 'right' : 'left' ;
163- const end = isRtl ? 'left' : 'right' ;
164-
165- for ( const row of rows ) {
166- for ( let i = 0 ; i < numCells ; i ++ ) {
167- const cell = row . children [ i ] as HTMLElement ;
168- if ( stickyStartStates [ i ] ) {
169- this . _addStickyStyle ( cell , start , startPositions [ i ] , i === lastStickyStart ) ;
170- }
171-
172- if ( stickyEndStates [ i ] ) {
173- this . _addStickyStyle ( cell , end , endPositions [ i ] , i === firstStickyEnd ) ;
153+ const firstRow = rows [ 0 ] ;
154+ const numCells = firstRow . children . length ;
155+
156+ const isRtl = this . direction === 'rtl' ;
157+ const start = isRtl ? 'right' : 'left' ;
158+ const end = isRtl ? 'left' : 'right' ;
159+
160+ const lastStickyStart = stickyStartStates . lastIndexOf ( true ) ;
161+ const firstStickyEnd = stickyEndStates . indexOf ( true ) ;
162+
163+ let cellWidths : number [ ] ;
164+ let startPositions : number [ ] ;
165+ let endPositions : number [ ] ;
166+
167+ this . _afterNextRender ( {
168+ earlyRead : ( ) => {
169+ cellWidths = this . _getCellWidths ( firstRow , recalculateCellWidths ) ;
170+
171+ startPositions = this . _getStickyStartColumnPositions ( cellWidths , stickyStartStates ) ;
172+ endPositions = this . _getStickyEndColumnPositions ( cellWidths , stickyEndStates ) ;
173+ } ,
174+ write : ( ) => {
175+ for ( const row of rows ) {
176+ for ( let i = 0 ; i < numCells ; i ++ ) {
177+ const cell = row . children [ i ] as HTMLElement ;
178+ if ( stickyStartStates [ i ] ) {
179+ this . _addStickyStyle ( cell , start , startPositions [ i ] , i === lastStickyStart ) ;
180+ }
181+
182+ if ( stickyEndStates [ i ] ) {
183+ this . _addStickyStyle ( cell , end , endPositions [ i ] , i === firstStickyEnd ) ;
184+ }
174185 }
175186 }
176- }
177187
178- if ( this . _positionListener ) {
179- this . _positionListener . stickyColumnsUpdated ( {
180- sizes :
181- lastStickyStart === - 1
182- ? [ ]
183- : cellWidths
184- . slice ( 0 , lastStickyStart + 1 )
185- . map ( ( width , index ) => ( stickyStartStates [ index ] ? width : null ) ) ,
186- } ) ;
187- this . _positionListener . stickyEndColumnsUpdated ( {
188- sizes :
189- firstStickyEnd === - 1
190- ? [ ]
191- : cellWidths
192- . slice ( firstStickyEnd )
193- . map ( ( width , index ) => ( stickyEndStates [ index + firstStickyEnd ] ? width : null ) )
194- . reverse ( ) ,
195- } ) ;
196- }
188+ if ( this . _positionListener ) {
189+ this . _positionListener . stickyColumnsUpdated ( {
190+ sizes :
191+ lastStickyStart === - 1
192+ ? [ ]
193+ : cellWidths
194+ . slice ( 0 , lastStickyStart + 1 )
195+ . map ( ( width , index ) => ( stickyStartStates [ index ] ? width : null ) ) ,
196+ } ) ;
197+ this . _positionListener . stickyEndColumnsUpdated ( {
198+ sizes :
199+ firstStickyEnd === - 1
200+ ? [ ]
201+ : cellWidths
202+ . slice ( firstStickyEnd )
203+ . map ( ( width , index ) => ( stickyEndStates [ index + firstStickyEnd ] ? width : null ) )
204+ . reverse ( ) ,
205+ } ) ;
206+ }
207+ } ,
197208 } ) ;
198209 }
199210
@@ -214,63 +225,66 @@ export class StickyStyler {
214225 return ;
215226 }
216227
228+ // If positioning the rows to the bottom, reverse their order when evaluating the sticky
229+ // position such that the last row stuck will be "bottom: 0px" and so on. Note that the
230+ // sticky states need to be reversed as well.
231+ const rows = position === 'bottom' ? rowsToStick . slice ( ) . reverse ( ) : rowsToStick ;
232+ const states = position === 'bottom' ? stickyStates . slice ( ) . reverse ( ) : stickyStates ;
233+
234+ // Measure row heights all at once before adding sticky styles to reduce layout thrashing.
235+ const stickyOffsets : number [ ] = [ ] ;
236+ const stickyCellHeights : ( number | undefined ) [ ] = [ ] ;
237+ const elementsToStick : HTMLElement [ ] [ ] = [ ] ;
238+
217239 // Coalesce with other sticky row updates (top/bottom), sticky columns updates
218240 // (and potentially other changes like column resize).
219- this . _coalescedStyleScheduler . schedule ( ( ) => {
220- // If positioning the rows to the bottom, reverse their order when evaluating the sticky
221- // position such that the last row stuck will be "bottom: 0px" and so on. Note that the
222- // sticky states need to be reversed as well.
223- const rows = position === 'bottom' ? rowsToStick . slice ( ) . reverse ( ) : rowsToStick ;
224- const states = position === 'bottom' ? stickyStates . slice ( ) . reverse ( ) : stickyStates ;
225-
226- // Measure row heights all at once before adding sticky styles to reduce layout thrashing.
227- const stickyOffsets : number [ ] = [ ] ;
228- const stickyCellHeights : ( number | undefined ) [ ] = [ ] ;
229- const elementsToStick : HTMLElement [ ] [ ] = [ ] ;
230-
231- for ( let rowIndex = 0 , stickyOffset = 0 ; rowIndex < rows . length ; rowIndex ++ ) {
232- if ( ! states [ rowIndex ] ) {
233- continue ;
234- }
241+ this . _afterNextRender ( {
242+ earlyRead : ( ) => {
243+ for ( let rowIndex = 0 , stickyOffset = 0 ; rowIndex < rows . length ; rowIndex ++ ) {
244+ if ( ! states [ rowIndex ] ) {
245+ continue ;
246+ }
235247
236- stickyOffsets [ rowIndex ] = stickyOffset ;
237- const row = rows [ rowIndex ] ;
238- elementsToStick [ rowIndex ] = this . _isNativeHtmlTable
239- ? ( Array . from ( row . children ) as HTMLElement [ ] )
240- : [ row ] ;
248+ stickyOffsets [ rowIndex ] = stickyOffset ;
249+ const row = rows [ rowIndex ] ;
250+ elementsToStick [ rowIndex ] = this . _isNativeHtmlTable
251+ ? ( Array . from ( row . children ) as HTMLElement [ ] )
252+ : [ row ] ;
241253
242- const height = this . _retrieveElementSize ( row ) . height ;
243- stickyOffset += height ;
244- stickyCellHeights [ rowIndex ] = height ;
245- }
254+ const height = this . _retrieveElementSize ( row ) . height ;
255+ stickyOffset += height ;
256+ stickyCellHeights [ rowIndex ] = height ;
257+ }
258+ } ,
259+ write : ( ) => {
260+ const borderedRowIndex = states . lastIndexOf ( true ) ;
246261
247- const borderedRowIndex = states . lastIndexOf ( true ) ;
262+ for ( let rowIndex = 0 ; rowIndex < rows . length ; rowIndex ++ ) {
263+ if ( ! states [ rowIndex ] ) {
264+ continue ;
265+ }
248266
249- for ( let rowIndex = 0 ; rowIndex < rows . length ; rowIndex ++ ) {
250- if ( ! states [ rowIndex ] ) {
251- continue ;
267+ const offset = stickyOffsets [ rowIndex ] ;
268+ const isBorderedRowIndex = rowIndex === borderedRowIndex ;
269+ for ( const element of elementsToStick [ rowIndex ] ) {
270+ this . _addStickyStyle ( element , position , offset , isBorderedRowIndex ) ;
271+ }
252272 }
253273
254- const offset = stickyOffsets [ rowIndex ] ;
255- const isBorderedRowIndex = rowIndex === borderedRowIndex ;
256- for ( const element of elementsToStick [ rowIndex ] ) {
257- this . _addStickyStyle ( element , position , offset , isBorderedRowIndex ) ;
274+ if ( position === 'top' ) {
275+ this . _positionListener ?. stickyHeaderRowsUpdated ( {
276+ sizes : stickyCellHeights ,
277+ offsets : stickyOffsets ,
278+ elements : elementsToStick ,
279+ } ) ;
280+ } else {
281+ this . _positionListener ?. stickyFooterRowsUpdated ( {
282+ sizes : stickyCellHeights ,
283+ offsets : stickyOffsets ,
284+ elements : elementsToStick ,
285+ } ) ;
258286 }
259- }
260-
261- if ( position === 'top' ) {
262- this . _positionListener ?. stickyHeaderRowsUpdated ( {
263- sizes : stickyCellHeights ,
264- offsets : stickyOffsets ,
265- elements : elementsToStick ,
266- } ) ;
267- } else {
268- this . _positionListener ?. stickyFooterRowsUpdated ( {
269- sizes : stickyCellHeights ,
270- offsets : stickyOffsets ,
271- elements : elementsToStick ,
272- } ) ;
273- }
287+ } ,
274288 } ) ;
275289 }
276290
@@ -286,19 +300,30 @@ export class StickyStyler {
286300 }
287301
288302 // Coalesce with other sticky updates (and potentially other changes like column resize).
289- this . _coalescedStyleScheduler . schedule ( ( ) => {
290- const tfoot = tableElement . querySelector ( 'tfoot' ) ! ;
291-
292- if ( tfoot ) {
293- if ( stickyStates . some ( state => ! state ) ) {
294- this . _removeStickyStyle ( tfoot , [ 'bottom' ] ) ;
295- } else {
296- this . _addStickyStyle ( tfoot , 'bottom' , 0 , false ) ;
303+ this . _afterNextRender ( {
304+ write : ( ) => {
305+ const tfoot = tableElement . querySelector ( 'tfoot' ) ! ;
306+
307+ if ( tfoot ) {
308+ if ( stickyStates . some ( state => ! state ) ) {
309+ this . _removeStickyStyle ( tfoot , [ 'bottom' ] ) ;
310+ } else {
311+ this . _addStickyStyle ( tfoot , 'bottom' , 0 , false ) ;
312+ }
297313 }
298- }
314+ } ,
299315 } ) ;
300316 }
301317
318+ /** Triggered by the table's OnDestroy hook. */
319+ destroy ( ) {
320+ if ( this . _stickyColumnsReplayTimeout ) {
321+ clearTimeout ( this . _stickyColumnsReplayTimeout ) ;
322+ }
323+
324+ this . _destroyed = true ;
325+ }
326+
302327 /**
303328 * Removes the sticky style on the element by removing the sticky cell CSS class, re-evaluating
304329 * the zIndex, removing each of the provided sticky directions, and removing the
@@ -516,6 +541,10 @@ export class StickyStyler {
516541 }
517542
518543 this . _stickyColumnsReplayTimeout = setTimeout ( ( ) => {
544+ if ( this . _destroyed ) {
545+ return ;
546+ }
547+
519548 for ( const update of this . _updatedStickyColumnsParamsToReplay ) {
520549 this . updateStickyColumns (
521550 update . rows ,
@@ -530,6 +559,21 @@ export class StickyStyler {
530559 } , 0 ) ;
531560 }
532561 }
562+
563+ /**
564+ * Invoke afterNextRender with the table's injector, falling back to CoalescedStyleScheduler
565+ * if the injector was not provided.
566+ */
567+ private _afterNextRender ( spec : { earlyRead ?: ( ) => void ; write : ( ) => void } ) {
568+ if ( this . _tableInjector ) {
569+ afterNextRender ( spec , { injector : this . _tableInjector } ) ;
570+ } else {
571+ this . _coalescedStyleScheduler . schedule ( ( ) => {
572+ spec . earlyRead ?.( ) ;
573+ spec . write ( ) ;
574+ } ) ;
575+ }
576+ }
533577}
534578
535579function isCell ( element : Element ) {
0 commit comments