Skip to content

fix(material/tabs): enable hydration #28366

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Jan 4, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions src/material/tabs/tab-group.html
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,16 @@
}
</mat-tab-header>

<!--
We need to project the content somewhere to avoid hydration errors. Some observations:
1. This is only necessary on the server.
2. We get a hydration error if there aren't any nodes after the `ng-content`.
3. We get a hydration error if `ng-content` is wrapped in another element.
-->
@if (_isServer) {
<ng-content/>
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why is this needed? I don't follow and would like to understand more. Why isn't this needed with CSR?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The nodes that were projected have to match the nodes that are rendered out or the runtime will throw a hydration error. For the tabs specifically without the ng-content we'll end up dropping the <mat-tab> node and the anchor comment inside of it which are necessary for hydration. This workaround keeps the nodes around during SSR and then drops them once hydration is done.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ohh, I see what you're saying. I would have expected the mat-tab to be resolved as well and have it's content SSR-ed as well. That seems like a surprising requirement for projection?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is a bit weird, but it's kinda expected. If the mat-tab doesn't get projected, when the HTML makes it to the browser, it won't know that it existed when it was created on the server.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It feels surprising to me that component authors would need to take care of that. It almost sounds like a bug to me that it works in CSR like that (talking about ContentChildren matching without a ng-content`).

The change makes sense, but would love the comment to capture this as well, but no need to re-push. Also cc. @AndrewKushnir as some user-facing feedback on hydration potentially

}

<div
class="mat-mdc-tab-body-wrapper"
[class._mat-animation-noopable]="_animationMode === 'NoopAnimations'"
Expand Down
6 changes: 5 additions & 1 deletion src/material/tabs/tab-group.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import {
ViewChild,
ViewEncapsulation,
booleanAttribute,
inject,
numberAttribute,
} from '@angular/core';
import {ANIMATION_MODULE_TYPE} from '@angular/platform-browser/animations';
Expand All @@ -38,6 +39,7 @@ import {MatTabBody} from './tab-body';
import {CdkPortalOutlet} from '@angular/cdk/portal';
import {NgClass} from '@angular/common';
import {MatTabLabelWrapper} from './tab-label-wrapper';
import {Platform} from '@angular/cdk/platform';

/** Used to generate unique ID's for each tab component */
let nextId = 0;
Expand Down Expand Up @@ -75,7 +77,6 @@ const ENABLE_BACKGROUND_INPUT = true;
},
],
host: {
'ngSkipHydration': '',
'class': 'mat-mdc-tab-group',
'[class]': '"mat-" + (color || "primary")',
'[class.mat-mdc-tab-group-dynamic-height]': 'dynamicHeight',
Expand Down Expand Up @@ -248,6 +249,9 @@ export class MatTabGroup implements AfterContentInit, AfterContentChecked, OnDes

private _groupId: number;

/** Whether the tab group is rendered on the server. */
protected _isServer: boolean = !inject(Platform).isBrowser;

constructor(
readonly _elementRef: ElementRef,
private _changeDetectorRef: ChangeDetectorRef,
Expand Down
5 changes: 5 additions & 0 deletions src/material/tabs/tab.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,11 @@ export const MAT_TAB_GROUP = new InjectionToken<any>('MAT_TAB_GROUP');
exportAs: 'matTab',
providers: [{provide: MAT_TAB, useExisting: MatTab}],
standalone: true,
host: {
// This element will be rendered on the server in order to support hydration.
// Hide it so it doesn't cause a layout shift when it's removed on the client.
'hidden': '',
},
})
export class MatTab implements OnInit, OnChanges, OnDestroy {
/** whether the tab is disabled. */
Expand Down
34 changes: 29 additions & 5 deletions src/universal-app/kitchen-sink/kitchen-sink.html
Original file line number Diff line number Diff line change
Expand Up @@ -307,13 +307,37 @@ <h2>Tabs</h2>
-->
<mat-tab-group [selectedIndex]="1">
<mat-tab label="Overview">
The overview
<h3>The overview</h3>
<p>Lorem ipsum dolor sit amet consectetur adipisicing elit. Doloremque assumenda doloribus,
rerum temporibus fugit aliquid adipisci aliquam eaque sint voluptas dolore cumque voluptatibus
quam quod. Quasi adipisci officia similique in?</p>
<p>Deleniti neque placeat magnam, voluptatibus eligendi illo consectetur dolore minima dolorem
nemo suscipit dolorum accusantium? Numquam officia culpa itaque qui repudiandae nulla,
laboriosam nihil molestiae ad aut perferendis alias amet.</p>
<p>Officia esse temporibus consequatur ipsa! Veritatis alias facere amet reiciendis sint
impedit atque iste doloremque dolor? Ullam, aspernatur? Alias, fuga! At dolorum odio
molestiae laudantium nihil alias inventore veritatis voluptatum.</p>
<button mat-raised-button color="primary">See the overview</button>
</mat-tab>
<mat-tab>
<ng-template mat-tab-label>
API docs
</ng-template>
The API docs
<ng-template mat-tab-label>API docs</ng-template>
<h3>The API docs</h3>
<p>Lorem ipsum dolor sit amet consectetur adipisicing elit. Dolorum facere quasi natus rerum
quae, nisi, quis, voluptate assumenda necessitatibus labore illo. Illum ipsum consequatur,
excepturi aspernatur odio veritatis sint perferendis!</p>
<p>Dicta ex laborum repudiandae nesciunt. Ea asperiores quo totam velit! Aliquid cum laudantium
officiis molestias, excepturi odio, autem magni dignissimos perspiciatis, amet qui! Dolorem
molestiae similique necessitatibus cupiditate ipsa aspernatur?</p>
<button mat-raised-button color="accent">See the API docs</button>
</mat-tab>

<mat-tab>
<ng-template mat-tab-label>Examples</ng-template>
<h3>The examples</h3>
<p>Lorem ipsum dolor sit amet consectetur adipisicing elit. Modi animi saepe, optio sequi
accusantium, eos perspiciatis reprehenderit, nobis exercitationem sunt ducimus molestiae
laborum inventore itaque incidunt. Neque dolorum adipisci quidem.</p>
<button mat-raised-button color="warn">See the examples</button>
</mat-tab>
</mat-tab-group>

Expand Down
3 changes: 2 additions & 1 deletion tools/public_api_guard/material/tabs.md
Original file line number Diff line number Diff line change
Expand Up @@ -281,6 +281,7 @@ export class MatTabGroup implements AfterContentInit, AfterContentChecked, OnDes
_getTabLabelId(i: number): string;
_handleClick(tab: MatTab, tabHeader: MatTabGroupBaseHeader, index: number): void;
headerPosition: MatTabHeaderPosition;
protected _isServer: boolean;
// (undocumented)
static ngAcceptInputType_contentTabIndex: unknown;
// (undocumented)
Expand Down Expand Up @@ -319,7 +320,7 @@ export class MatTabGroup implements AfterContentInit, AfterContentChecked, OnDes
_tabs: QueryList<MatTab>;
updatePagination(): void;
// (undocumented)
static ɵcmp: i0.ɵɵComponentDeclaration<MatTabGroup, "mat-tab-group", ["matTabGroup"], { "color": { "alias": "color"; "required": false; }; "fitInkBarToContent": { "alias": "fitInkBarToContent"; "required": false; }; "stretchTabs": { "alias": "mat-stretch-tabs"; "required": false; }; "dynamicHeight": { "alias": "dynamicHeight"; "required": false; }; "selectedIndex": { "alias": "selectedIndex"; "required": false; }; "headerPosition": { "alias": "headerPosition"; "required": false; }; "animationDuration": { "alias": "animationDuration"; "required": false; }; "contentTabIndex": { "alias": "contentTabIndex"; "required": false; }; "disablePagination": { "alias": "disablePagination"; "required": false; }; "disableRipple": { "alias": "disableRipple"; "required": false; }; "preserveContent": { "alias": "preserveContent"; "required": false; }; "backgroundColor": { "alias": "backgroundColor"; "required": false; }; }, { "selectedIndexChange": "selectedIndexChange"; "focusChange": "focusChange"; "animationDone": "animationDone"; "selectedTabChange": "selectedTabChange"; }, ["_allTabs"], never, true, never>;
static ɵcmp: i0.ɵɵComponentDeclaration<MatTabGroup, "mat-tab-group", ["matTabGroup"], { "color": { "alias": "color"; "required": false; }; "fitInkBarToContent": { "alias": "fitInkBarToContent"; "required": false; }; "stretchTabs": { "alias": "mat-stretch-tabs"; "required": false; }; "dynamicHeight": { "alias": "dynamicHeight"; "required": false; }; "selectedIndex": { "alias": "selectedIndex"; "required": false; }; "headerPosition": { "alias": "headerPosition"; "required": false; }; "animationDuration": { "alias": "animationDuration"; "required": false; }; "contentTabIndex": { "alias": "contentTabIndex"; "required": false; }; "disablePagination": { "alias": "disablePagination"; "required": false; }; "disableRipple": { "alias": "disableRipple"; "required": false; }; "preserveContent": { "alias": "preserveContent"; "required": false; }; "backgroundColor": { "alias": "backgroundColor"; "required": false; }; }, { "selectedIndexChange": "selectedIndexChange"; "focusChange": "focusChange"; "animationDone": "animationDone"; "selectedTabChange": "selectedTabChange"; }, ["_allTabs"], ["*"], true, never>;
// (undocumented)
static ɵfac: i0.ɵɵFactoryDeclaration<MatTabGroup, [null, null, { optional: true; }, { optional: true; }]>;
}
Expand Down