Skip to content
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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ Format dựa trên [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). Maj

### Added

- `sd-switch` supports `size="sm | md | lg"` (default `md`) with matching track, handle, icon and focus-layer dimensions. Switches rendered inside `sd-table`, including consumer cell templates, use the compact `sm` appearance automatically.
- Sidebar V3 supports independent expand/collapse of menu groups at every depth, with keyboard-accessible disclosure buttons. Group state survives search and sidebar rail toggles; navigation opens the active branch, and selecting a group icon in the rail opens that group.
- Form builder groups support `properties.collapsible` (default `false`), matching `sd-section`. Enable it to expand/collapse groups in preview and form render while preserving child values and validation. Group conditions remain limited to visibility; disabled-when rules belong to child fields.

Expand Down
6 changes: 3 additions & 3 deletions showcase/src/app/docs/core/documentation.registry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -775,13 +775,13 @@ const FORM_PAGES = [
category: 'forms',
slug: 'switch',
title: 'Switch',
description: 'Công tắc boolean với theme colors và các trạng thái disabled/viewed.',
description: 'Công tắc boolean với ba kích thước, theme colors và các trạng thái disabled/viewed.',
selector: 'sd-switch',
importPath: '@sdcorejs/angular/forms/switch',
publishedDocId: 'forms/switch/sd-switch',
keywords: ['toggle', 'boolean', 'on off', 'settings'],
keywords: ['toggle', 'boolean', 'on off', 'settings', 'size'],
status: 'stable',
demoSectionCount: 5,
demoSectionCount: 7,
loadComponent: () => import('../../pages/forms/switch/switch-demo.component').then(m => m.SwitchDemoComponent),
}),
defineDocPage({
Expand Down
16 changes: 16 additions & 0 deletions showcase/src/app/docs/generated/example-manifest.generated.ts
Original file line number Diff line number Diff line change
Expand Up @@ -281,9 +281,11 @@ export type ShowcaseExampleSourceKey =
| "forms/select/example-snippet-mau"
| "forms/select/example-trang-thai"
| "forms/select/example-validator"
| "forms/switch/example-ben-trong-bang"
| "forms/switch/example-che-do-xem"
| "forms/switch/example-co-ban"
| "forms/switch/example-danh-sach-cau-hinh"
| "forms/switch/example-kich-thuoc"
| "forms/switch/example-mau-sac"
| "forms/switch/example-trang-thai"
| "forms/textarea/example-chinh-sua-noi-tuyen"
Expand Down Expand Up @@ -2329,6 +2331,20 @@ export const SHOWCASE_EXAMPLE_MANIFEST = [
title: "Cơ bản",
description: "Bind hai chiều, hiển thị trạng thái ngay bên cạnh.",
},
{
sourceKey: "forms/switch/example-kich-thuoc",
pageKey: "forms/switch",
sectionId: "example-kich-thuoc",
title: "Kích thước",
description: "Mặc định md. Track lần lượt 36×20px, 52×32px và 60×36px.",
},
{
sourceKey: "forms/switch/example-ben-trong-bang",
pageKey: "forms/switch",
sectionId: "example-ben-trong-bang",
title: "Bên trong bảng",
description: "Switch trong sd-table tự hiển thị cỡ sm, kể cả khi cell template không truyền size.",
},
{
sourceKey: "forms/switch/example-danh-sach-cau-hinh",
pageKey: "forms/switch",
Expand Down
76 changes: 74 additions & 2 deletions showcase/src/app/docs/generated/example-sources.generated.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11437,13 +11437,14 @@ export class SelectDemoComponent {
import { FormGroup, FormsModule, ReactiveFormsModule } from '@angular/forms';
import { DemoPageComponent, DemoSectionComponent } from '../../../shared/demo-page.component';
import { SdSwitch } from '@sdcorejs/angular/forms/switch';
import { SdTable, SdTableCellDefDirective, SdTableOption } from '@sdcorejs/angular/components/table';

@Component({
selector: 'app-switch-demo',
standalone: true,
imports: [DemoPageComponent, DemoSectionComponent, FormsModule, ReactiveFormsModule, SdSwitch],
imports: [DemoPageComponent, DemoSectionComponent, FormsModule, ReactiveFormsModule, SdSwitch, SdTable, SdTableCellDefDirective],
template: \`
<demo-page #demoPage title="Switch" description="sd-switch – công tắc bật/tắt boolean. Hỗ trợ màu chủ đề, disabled / viewed.">
<demo-page #demoPage title="Switch" description="sd-switch – công tắc bật/tắt boolean. Ba kích thước, màu chủ đề, disabled / viewed.">
@if (!demoPage.focusedSectionId || demoPage.focusedSectionId === 'example-co-ban') {
<demo-section heading="Cơ bản" [props]="[{ name: '[(model)]', value: 'two-way' }]" note="Bind hai chiều, hiển thị trạng thái ngay bên cạnh.">
<div style="display:flex; flex-direction:column; gap:8px; width:100%">
Expand All @@ -11455,6 +11456,35 @@ import { SdSwitch } from '@sdcorejs/angular/forms/switch';
</demo-section>
}

@if (!demoPage.focusedSectionId || demoPage.focusedSectionId === 'example-kich-thuoc') {
<demo-section heading="Kích thước" [props]="[{ name: 'size', value: 'sm / md / lg' }]" note="Mặc định md. Track lần lượt 36×20px, 52×32px và 60×36px.">
<div style="display:flex; flex-wrap:wrap; align-items:center; gap:24px">
<div style="display:flex; flex-direction:column; gap:12px">
<sd-switch size="sm" label="sm · Bật" [model]="true" />
<sd-switch size="sm" label="sm · Tắt" [model]="false" />
</div>
<div style="display:flex; flex-direction:column; gap:12px">
<sd-switch size="md" label="md · Bật" [model]="true" />
<sd-switch size="md" label="md · Tắt" [model]="false" />
</div>
<div style="display:flex; flex-direction:column; gap:12px">
<sd-switch size="lg" label="lg · Bật" [model]="true" />
<sd-switch size="lg" label="lg · Tắt" [model]="false" />
</div>
</div>
</demo-section>
}

@if (!demoPage.focusedSectionId || demoPage.focusedSectionId === 'example-ben-trong-bang') {
<demo-section heading="Bên trong bảng" [props]="[{ name: 'sdTableCellDef', value: 'template' }]" note="Switch trong sd-table tự hiển thị cỡ sm, kể cả khi cell template không truyền size.">
<sd-table [option]="switchTable" style="width:100%">
<ng-template sdTableCellDef="active" let-item="item">
<sd-switch label="Hoạt động" [(model)]="item.active" hideInlineError />
</ng-template>
</sd-table>
</demo-section>
}

@if (!demoPage.focusedSectionId || demoPage.focusedSectionId === 'example-danh-sach-cau-hinh') {
<demo-section heading="Danh sách cấu hình" note="Mỗi switch điều khiển một option độc lập.">
<div style="display:flex; flex-direction:column; gap:6px">
Expand Down Expand Up @@ -11502,6 +11532,19 @@ import { SdSwitch } from '@sdcorejs/angular/forms/switch';
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class SwitchDemoComponent {
readonly switchRows = [
{ id: 1, name: 'Thông báo email', active: true },
{ id: 2, name: 'Tự động lưu', active: false },
];
readonly switchTable: SdTableOption<(typeof this.switchRows)[number]> = {
type: 'local',
items: () => this.switchRows,
columns: [
{ field: 'name', title: 'Tính năng', type: 'string' },
{ field: 'active', title: 'Trạng thái', type: 'boolean' },
],
};

form = new FormGroup({});

notify = signal<boolean>(true);
Expand Down Expand Up @@ -19162,6 +19205,16 @@ export const SHOWCASE_EXAMPLE_SOURCES = {
</div>
</demo-section>`,
},
"forms/switch/example-ben-trong-bang": {
...SHOWCASE_PAGE_SOURCES["forms/switch"],
html: `<demo-section heading="Bên trong bảng" [props]="[{ name: 'sdTableCellDef', value: 'template' }]" note="Switch trong sd-table tự hiển thị cỡ sm, kể cả khi cell template không truyền size.">
<sd-table [option]="switchTable" style="width:100%">
<ng-template sdTableCellDef="active" let-item="item">
<sd-switch label="Hoạt động" [(model)]="item.active" hideInlineError />
</ng-template>
</sd-table>
</demo-section>`,
},
"forms/switch/example-che-do-xem": {
...SHOWCASE_PAGE_SOURCES["forms/switch"],
html: `<demo-section heading="Chế độ xem" [props]="[{ name: 'viewed', value: 'true' }, { name: 'viewed', value: 'inline' }]" note="viewed=true hiện chữ Bật/Tắt; 'inline' vẫn gạt được, disabled+inline thì xem tĩnh.">
Expand Down Expand Up @@ -19196,6 +19249,25 @@ export const SHOWCASE_EXAMPLE_SOURCES = {
</div>
</demo-section>`,
},
"forms/switch/example-kich-thuoc": {
...SHOWCASE_PAGE_SOURCES["forms/switch"],
html: `<demo-section heading="Kích thước" [props]="[{ name: 'size', value: 'sm / md / lg' }]" note="Mặc định md. Track lần lượt 36×20px, 52×32px và 60×36px.">
<div style="display:flex; flex-wrap:wrap; align-items:center; gap:24px">
<div style="display:flex; flex-direction:column; gap:12px">
<sd-switch size="sm" label="sm · Bật" [model]="true" />
<sd-switch size="sm" label="sm · Tắt" [model]="false" />
</div>
<div style="display:flex; flex-direction:column; gap:12px">
<sd-switch size="md" label="md · Bật" [model]="true" />
<sd-switch size="md" label="md · Tắt" [model]="false" />
</div>
<div style="display:flex; flex-direction:column; gap:12px">
<sd-switch size="lg" label="lg · Bật" [model]="true" />
<sd-switch size="lg" label="lg · Tắt" [model]="false" />
</div>
</div>
</demo-section>`,
},
"forms/switch/example-mau-sac": {
...SHOWCASE_PAGE_SOURCES["forms/switch"],
html: `<demo-section heading="Màu sắc" [props]="[{ name: 'color', value: 'primary / success / warning / error' }]" note="Thuộc tính color thay đổi accent track.">
Expand Down
47 changes: 45 additions & 2 deletions showcase/src/app/pages/forms/switch/switch-demo.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,14 @@ import { ChangeDetectionStrategy, Component, signal } from '@angular/core';
import { FormGroup, FormsModule, ReactiveFormsModule } from '@angular/forms';
import { DemoPageComponent, DemoSectionComponent } from '../../../shared/demo-page.component';
import { SdSwitch } from '@sdcorejs/angular/forms/switch';
import { SdTable, SdTableCellDefDirective, SdTableOption } from '@sdcorejs/angular/components/table';

@Component({
selector: 'app-switch-demo',
standalone: true,
imports: [DemoPageComponent, DemoSectionComponent, FormsModule, ReactiveFormsModule, SdSwitch],
imports: [DemoPageComponent, DemoSectionComponent, FormsModule, ReactiveFormsModule, SdSwitch, SdTable, SdTableCellDefDirective],
template: `
<demo-page #demoPage title="Switch" description="sd-switch – công tắc bật/tắt boolean. Hỗ trợ màu chủ đề, disabled / viewed.">
<demo-page #demoPage title="Switch" description="sd-switch – công tắc bật/tắt boolean. Ba kích thước, màu chủ đề, disabled / viewed.">
@if (!demoPage.focusedSectionId || demoPage.focusedSectionId === 'example-co-ban') {
<demo-section heading="Cơ bản" [props]="[{ name: '[(model)]', value: 'two-way' }]" note="Bind hai chiều, hiển thị trạng thái ngay bên cạnh.">
<div style="display:flex; flex-direction:column; gap:8px; width:100%">
Expand All @@ -20,6 +21,35 @@ import { SdSwitch } from '@sdcorejs/angular/forms/switch';
</demo-section>
}

@if (!demoPage.focusedSectionId || demoPage.focusedSectionId === 'example-kich-thuoc') {
<demo-section heading="Kích thước" [props]="[{ name: 'size', value: 'sm / md / lg' }]" note="Mặc định md. Track lần lượt 36×20px, 52×32px và 60×36px.">
<div style="display:flex; flex-wrap:wrap; align-items:center; gap:24px">
<div style="display:flex; flex-direction:column; gap:12px">
<sd-switch size="sm" label="sm · Bật" [model]="true" />
<sd-switch size="sm" label="sm · Tắt" [model]="false" />
</div>
<div style="display:flex; flex-direction:column; gap:12px">
<sd-switch size="md" label="md · Bật" [model]="true" />
<sd-switch size="md" label="md · Tắt" [model]="false" />
</div>
<div style="display:flex; flex-direction:column; gap:12px">
<sd-switch size="lg" label="lg · Bật" [model]="true" />
<sd-switch size="lg" label="lg · Tắt" [model]="false" />
</div>
</div>
</demo-section>
}

@if (!demoPage.focusedSectionId || demoPage.focusedSectionId === 'example-ben-trong-bang') {
<demo-section heading="Bên trong bảng" [props]="[{ name: 'sdTableCellDef', value: 'template' }]" note="Switch trong sd-table tự hiển thị cỡ sm, kể cả khi cell template không truyền size.">
<sd-table [option]="switchTable" style="width:100%">
<ng-template sdTableCellDef="active" let-item="item">
<sd-switch label="Hoạt động" [(model)]="item.active" hideInlineError />
</ng-template>
</sd-table>
</demo-section>
}

@if (!demoPage.focusedSectionId || demoPage.focusedSectionId === 'example-danh-sach-cau-hinh') {
<demo-section heading="Danh sách cấu hình" note="Mỗi switch điều khiển một option độc lập.">
<div style="display:flex; flex-direction:column; gap:6px">
Expand Down Expand Up @@ -67,6 +97,19 @@ import { SdSwitch } from '@sdcorejs/angular/forms/switch';
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class SwitchDemoComponent {
readonly switchRows = [
{ id: 1, name: 'Thông báo email', active: true },
{ id: 2, name: 'Tự động lưu', active: false },
];
readonly switchTable: SdTableOption<(typeof this.switchRows)[number]> = {
type: 'local',
items: () => this.switchRows,
columns: [
{ field: 'name', title: 'Tính năng', type: 'string' },
{ field: 'active', title: 'Trạng thái', type: 'boolean' },
],
};

form = new FormGroup({});

notify = signal<boolean>(true);
Expand Down
2 changes: 1 addition & 1 deletion versions/v19/SYNC-STATUS.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
|-----|-------|
| Angular Major | 19 |
| Legacy Source Commit | d12478a1 |
| Updated At | 2026-09-11 15:55:24 |
| Updated At | 2026-09-11 16:48:14 |
| Origin | repo-owned versions/v19 (final legacy sync vn-angular@d12478a1) |
| Workspace Flow | versions/v19 |
| Development Mode | repo-owned independent pack |
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -425,6 +425,10 @@ command: {
}
```

### Switches in cells

Switches (`sd-switch`) rendered within the table use the compact `sm` appearance (36×20px track), including projected consumer cell templates and mobile cards. This table styling takes precedence over the switch size; switches outside the table still default to `md`.

## Inputs (the host element)

| Name | Type | Default | Notes |
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import { Component } from '@angular/core';
import { fakeAsync, flush, TestBed, tick } from '@angular/core/testing';
import { NoopAnimationsModule } from '@angular/platform-browser/animations';
import { SdSwitch } from '@sdcorejs/angular/forms/switch';
import { SdTable } from './table.component';
import { SdTableCellDefDirective } from './directives/sd-table-cell-def.directive';
import { SdTableOption } from './models/table-option.model';

@Component({
imports: [SdTable, SdTableCellDefDirective, SdSwitch],
template: `
<sd-switch label="Outside table" />
<sd-table [option]="option">
<ng-template sdTableCellDef="active" let-item="item">
<sd-switch label="Active" [(model)]="item.active" hideInlineError />
<sd-switch size="lg" label="Also compact" [model]="item.active" hideInlineError />
</ng-template>
</sd-table>
`,
})
class SwitchTableHost {
row = { id: 1, active: false };
option: SdTableOption<typeof this.row> = {
type: 'local',
items: () => [this.row],
columns: [{ field: 'active', title: 'Active', type: 'boolean' }],
};
}

describe('SdTable switch sizing', () => {
it('compacts a consumer cell switch to sm without affecting an outside switch or its model binding', fakeAsync(() => {
TestBed.configureTestingModule({ imports: [SwitchTableHost, NoopAnimationsModule] });
const fixture = TestBed.createComponent(SwitchTableHost);
fixture.detectChanges();
tick(800);
flush();
fixture.detectChanges();
const inside = fixture.nativeElement.querySelector('sd-table sd-switch') as HTMLElement;
const outside = fixture.nativeElement.querySelector('sd-switch') as HTMLElement;
expect(inside).not.toBeNull();
expect(inside.querySelector('.mdc-switch__track')!.getBoundingClientRect().width).toBe(36);
expect(inside.querySelector('.mdc-switch__track')!.getBoundingClientRect().height).toBe(20);
const explicitLarge = fixture.nativeElement.querySelector('sd-table sd-switch[size="lg"]') as HTMLElement;
expect(explicitLarge.querySelector('.mdc-switch__track')!.getBoundingClientRect().width).toBe(36);
expect(outside.querySelector('.mdc-switch__track')!.getBoundingClientRect().width).toBe(52);
inside.querySelector<HTMLButtonElement>('button[role="switch"]')!.click();
fixture.detectChanges();
expect(fixture.componentInstance.row.active).toBeTrue();
fixture.destroy();
flush();
}));
});
Loading
Loading