Angular Pivot Grid State Persistence
igxGridState 지시문을 사용하면 개발자가 그리드 상태를 쉽게 저장하고 복원할 수 있습니다. IgxGridState
지시문이 그리드에 적용되면 개발자가 모든 시나리오에서 상태 지속성을 달성하는 데 사용할 수 있는 getState
및 setState
메서드가 노출됩니다.
지원되는 기능
IgxGridState
지시문은 다음 기능의 상태 저장 및 복원을 지원합니다.
Sorting
Filtering
Cell Selection
Row Selection
Column Selection
Expansion
Pivot Configuration
Pivot Configuration properties defined by the IPivotConfiguration
interface.
피벗 차원 및 값 기능은 애플리케이션 수준 코드를 사용하여 복원됩니다. 피벗 구성 복원 섹션을 참조하세요.
피벗 행 및 열 전략도 애플리케이션 수준 코드를 사용하여 복원됩니다. 피벗 전략 복원 섹션을 참조하세요.
용법
getState
- 이 방법은 직렬화된 JSON 문자열로 그리드 상태를 반환하므로 개발자는 이를 가져와 모든 데이터 저장소(데이터베이스, 클라우드, 브라우저 localStorage 등)에 저장할 수 있습니다. 이 메소드는 첫 번째 선택적 매개변수를 허용합니다. serialize
, 여부를 결정합니다. getState
반환합니다 IGridState
객체 또는 직렬화된 JSON 문자열. 개발자는 기능 이름 또는 기능 이름이 포함된 배열을 두 번째 인수로 전달하여 특정 기능에 대한 상태만 가져오도록 선택할 수 있습니다.
const gridState = state.getState();
const gridState: IGridState = state.getState(false );
const sortingFilteringStates: IGridState = state.getState(false , ['sorting' , 'filtering' ]);
typescript
setState
-setState
메소드는 직렬화된 JSON 문자열 또는 IGridState
객체를 인수로 받아들이고 객체/JSON 문자열에 있는 각 기능의 상태를 복원합니다.
state.setState(gridState);
state.setState(sortingFilteringStates)
typescript
options
-options
개체는 IGridStateOptions
인터페이스를 구현합니다. 즉, 특정 기능의 이름인 모든 키에 대해 이 기능 상태가 추적되는지 여부를 나타내는 부울 값이 있습니다. getState
메소드는 이러한 기능의 상태를 반환된 값에 넣지 않으며 setState
메소드는 해당 기능의 상태를 복원하지 않습니다.
public options = { cellSelection : false ; sorting: false ; }
typescript
<igx-pivot-grid [igxGridState ]="options" > </igx-pivot-grid >
html
사용하기 쉬운 단일 지점 API를 사용하면 단 몇 줄의 코드만으로 전체 상태 지속성 기능을 달성할 수 있습니다. 아래 코드를 복사하여 붙여넣으세요 . 사용자가 현재 페이지를 떠날 때마다 브라우저 sessionStorage
객체에 그리드 상태가 저장됩니다. 사용자가 메인 페이지로 돌아갈 때마다 그리드 상태가 복원됩니다. 원하는 데이터를 얻기 위해 매번 복잡한 고급 필터링 및 정렬 표현식을 구성할 필요가 없습니다. 한 번만 수행하면 아래 코드가 사용자를 위해 나머지 작업을 수행하게 됩니다.
@ViewChild (IgxGridStateDirective, { static : true })
public state!: IgxGridStateDirective;
public ngOnInit ( ) {
this .router.events.pipe(take(1 )).subscribe((event: NavigationStart ) => {
this .saveGridState();
});
}
public ngAfterViewInit ( ) {
this .restoreGridState();
}
public saveGridState ( ) {
const state = this .state.getState() as string ;
window .sessionStorage.setItem('grid1-state' , state);
}
public restoreGridState ( ) {
const state = window .sessionStorage.getItem('grid1-state' );
this .state.setState(state);
}
typescript
피벗 구성 복원
IgxGridState
기본적으로 피벗 차원 함수, 값 포맷터 등을 유지하지 않습니다(limitations
참조). 이들 중 하나를 복원하는 것은 애플리케이션 수준의 코드를 사용하여 수행할 수 있습니다. IgxPivotGrid
는 구성에 포함된 사용자 정의 기능을 다시 설정하는 데 사용할 수 있는 두 가지 이벤트(dimensionInit
및 valueInit
를 노출합니다. 이를 수행하는 방법을 보여드리겠습니다.
dimensionInit
및 valueInit
이벤트에 대한 이벤트 핸들러를 할당합니다.
<igx-pivot-grid #grid1 [data ]="data" [pivotConfiguration ]="pivotConfig" [igxGridState ]="options"
(valueInit )='onValueInit($event)' (dimensionInit )='onDimensionInit($event)' >
</igx-pivot-grid >
html
pivotConfiguration
속성에 정의된 각 값과 차원에 대해 dimensionInit
및 valueInit
이벤트가 발생합니다.
valueInit
이벤트 핸들러에서 모든 사용자 정의 수집기, 포맷터 및 스타일을 설정합니다.
public onValueInit (value: IPivotValue ) {
if (value.member === 'AmountofSale' ) {
value.aggregate.aggregator = IgxTotalSaleAggregate.totalSale;
value.aggregateList?.forEach((aggr: IPivotAggregator ) => {
switch (aggr.key) {
case 'SUM' :
aggr.aggregator = IgxTotalSaleAggregate.totalSale;
break ;
case 'MIN' :
aggr.aggregator = IgxTotalSaleAggregate.totalMin;
break ;
case 'MAX' :
aggr.aggregator = IgxTotalSaleAggregate.totalMax;
break ;
}
});
} else if (value.member === 'Value' ) {
value.formatter = (value ) => value ? '$' + parseFloat (value).toFixed(3 ) : undefined ;
value.styles.upFontValue = (rowData: any , columnKey : any ): boolean => parseFloat (rowData.aggregationValues.get(columnKey.field)) > 150
value.styles.downFontValue = (rowData: any , columnKey : any ): boolean => parseFloat (rowData.aggregationValues.get(columnKey.field)) <= 150 ;
}
}
typescript
dimensionInit
이벤트 핸들러에서 모든 사용자 정의 memberFunction
구현을 설정합니다.
public onDimensionInit (dim: IPivotDimension ) {
switch (dim.memberName) {
case 'AllProducts' :
dim.memberFunction = () => 'All Products' ;
break ;
case 'ProductCategory' :
dim.memberFunction = (data ) => data.Product.Name;
break ;
case 'City' :
dim.memberFunction = (data ) => data.Seller.City;
break ;
case 'SellerName' :
dim.memberFunction = (data ) => data.Seller.Name;
break ;
}
}
typescript
import { AfterViewInit, Component, OnInit, QueryList, ViewChild, ViewChildren } from "@angular/core" ;
import { NavigationStart, Router, RouterLink } from "@angular/router" ;
import { IPivotConfiguration, PivotAggregation, IgxPivotNumericAggregate, IgxPivotDateDimension, IgxGridStateDirective, IgxPivotGridComponent, IgxCheckboxComponent, GridFeatures, IGridStateOptions, IGridState, IPivotValue, IPivotDimension, IPivotAggregator, GridColumnDataType, IgxButtonDirective, IgxIconComponent } from "igniteui-angular"
import { take } from "rxjs/operators" ;
import { SALES_DATA } from "../../data/dataToAnalyze" ;
import { NgFor } from "@angular/common" ;
export class IgxTotalSaleAggregate {
public static totalSale: PivotAggregation = (members, data: any ) =>
data.reduce((accumulator, value ) => accumulator + value.Product.UnitPrice * value.NumberOfUnits, 0 );
public static totalMin: PivotAggregation = (members, data: any ) => {
let min = 0 ;
if (data.length === 1 ) {
min = data[0 ].Product.UnitPrice * data[0 ].NumberOfUnits;
} else if (data.length > 1 ) {
const mappedData = data.map(x => x.Product.UnitPrice * x.NumberOfUnits);
min = mappedData.reduce((a, b ) => Math .min(a, b));
}
return min;
};
public static totalMax: PivotAggregation = (members, data: any ) => {
let max = 0 ;
if (data.length === 1 ) {
max = data[0 ].Product.UnitPrice * data[0 ].NumberOfUnits;
} else if (data.length > 1 ) {
const mappedData = data.map(x => x.Product.UnitPrice * x.NumberOfUnits);
max = mappedData.reduce((a, b ) => Math .max(a, b));
}
return max;
};
}
@Component ({
selector : 'app-pivot-grid-state-persistence-sample' ,
styleUrls : ['./pivot-grid-state-persistence-sample.component.scss' ],
templateUrl : './pivot-grid-state-persistence-sample.component.html' ,
imports : [IgxButtonDirective, IgxIconComponent, RouterLink, IgxCheckboxComponent, NgFor, IgxPivotGridComponent, IgxGridStateDirective]
})
export class PivotGridStatePersistenceSampleComponent implements OnInit , AfterViewInit {
@ViewChild (IgxGridStateDirective, { static : true }) public state: IgxGridStateDirective;
@ViewChild (IgxPivotGridComponent, { static : true }) public grid: IgxPivotGridComponent;
@ViewChildren (IgxCheckboxComponent) public checkboxes: QueryList<IgxCheckboxComponent>;
public data = SALES_DATA;
public serialize = true ;
public stateKey = 'grid-state' ;
public features: { key : GridFeatures; shortName: string }[] = [
{ key : 'cellSelection' , shortName : 'Cell Sel' },
{ key : 'columnSelection' , shortName : 'Cols Sel' },
{ key : 'expansion' , shortName : 'Expansion' },
{ key : 'filtering' , shortName : 'Filt' },
{ key : 'sorting' , shortName : 'Sorting' },
{ key : 'pivotConfiguration' , shortName : 'Pivot Configuration' }
];
public options: IGridStateOptions = {
cellSelection : true ,
filtering : true ,
sorting : true ,
expansion : true ,
columnSelection : true ,
pivotConfiguration : true
};
public pivotConfig: IPivotConfiguration = {
columns : [
new IgxPivotDateDimension(
{
memberName : 'Date' ,
enabled : true
},
{
months : false ,
quarters : true ,
fullDate : false
}
)
],
rows : [
{
memberFunction : () => 'All Products' ,
memberName : 'AllProducts' ,
enabled : true ,
width : "150px" ,
childLevel : {
memberFunction : (data ) => data.Product.Name,
memberName : 'ProductCategory' ,
enabled : true
}
},
{
memberName : 'City' ,
width : "150px" ,
memberFunction : (data ) => data.Seller.City,
enabled : true
}
],
values : [
{
member : 'Value' ,
aggregate : {
key : 'SUM' ,
aggregator : IgxPivotNumericAggregate.sum,
label : 'Sum'
},
aggregateList : [{
key : 'SUM' ,
aggregator : IgxPivotNumericAggregate.sum,
label : 'Sum'
}],
enabled : true ,
dataType : GridColumnDataType.Number,
styles : {
downFontValue : (rowData: any , columnKey : any ): boolean => parseFloat (rowData.aggregationValues.get(columnKey.field)) <= 150 ,
upFontValue : (rowData: any , columnKey : any ): boolean => parseFloat (rowData.aggregationValues.get(columnKey.field)) > 150
},
formatter : (value ) => value ? '$' + parseFloat (value).toFixed(3 ) : undefined
},
{
member : 'AmountofSale' ,
displayName : 'Amount of Sale' ,
aggregate : {
key : 'SUM' ,
aggregator : IgxTotalSaleAggregate.totalSale,
label : 'Sum of Sale'
},
aggregateList : [{
key : 'SUM' ,
aggregator : IgxTotalSaleAggregate.totalSale,
label : 'Sum of Sale'
}, {
key : 'MIN' ,
aggregator : IgxTotalSaleAggregate.totalMin,
label : 'Minimum of Sale'
}, {
key : 'MAX' ,
aggregator : IgxTotalSaleAggregate.totalMax,
label : 'Maximum of Sale'
}],
enabled : true ,
dataType : GridColumnDataType.Currency
}
],
filters : [
{
memberName : 'SellerName' ,
memberFunction : (data ) => data.Seller.Name,
enabled : true
}
]
};
constructor (private router: Router ) { }
public ngOnInit(): void {
this .router.events.pipe(take(1 )).subscribe((event: NavigationStart ) => {
this .saveGridState();
});
}
public ngAfterViewInit(): void {
this .restoreGridState();
}
public saveGridState ( ) {
const state = this .state.getState(this .serialize);
if (typeof state === 'string' ) {
window .sessionStorage.setItem(this .stateKey, state);
} else {
window .sessionStorage.setItem(this .stateKey, JSON .stringify(state));
}
}
public restoreGridState ( ) {
const state = window .sessionStorage.getItem(this .stateKey);
if (state) {
this .state.setState(state);
}
}
public restoreFeature (stateDirective: IgxGridStateDirective, feature: string ) {
const state = this .getFeatureState(this .stateKey, feature);
if (state) {
const featureState = {} as IGridState;
featureState[feature] = state;
stateDirective.setState(featureState);
}
}
public getFeatureState (stateKey: string , feature: string ) {
let state = window .sessionStorage.getItem(stateKey);
state = state ? JSON .parse(state)[feature] : null ;
return state;
}
public onChange (event: any , action: string ) {
if (action === 'toggleAll' ) {
this .checkboxes.forEach(cb => {
cb.checked = event.checked;
});
for (const key of Object .keys(this .options)) {
this .state.options[key] = event.checked;
}
return ;
}
this .state.options[action] = event.checked;
}
public clearStorage ( ) {
window .sessionStorage.removeItem(this .stateKey);
}
public reloadPage ( ) {
window .location.reload();
}
public onValueInit (value: IPivotValue ) {
if (value.member === 'AmountofSale' ) {
value.aggregate.aggregator = IgxTotalSaleAggregate.totalSale;
value.aggregateList?.forEach((aggr: IPivotAggregator ) => {
switch (aggr.key) {
case 'SUM' :
aggr.aggregator = IgxTotalSaleAggregate.totalSale;
break ;
case 'MIN' :
aggr.aggregator = IgxTotalSaleAggregate.totalMin;
break ;
case 'MAX' :
aggr.aggregator = IgxTotalSaleAggregate.totalMax;
break ;
}
});
} else if (value.member === 'Value' ) {
value.formatter = (value ) => value ? '$' + parseFloat (value).toFixed(3 ) : undefined ;
value.styles.upFontValue = (rowData: any , columnKey : any ): boolean => parseFloat (rowData.aggregationValues.get(columnKey.field)) > 150
value.styles.downFontValue = (rowData: any , columnKey : any ): boolean => parseFloat (rowData.aggregationValues.get(columnKey.field)) <= 150 ;
}
}
public onDimensionInit (dim: IPivotDimension ) {
switch (dim.memberName) {
case 'AllProducts' :
dim.memberFunction = () => 'All Products' ;
break ;
case 'ProductCategory' :
dim.memberFunction = (data ) => data.Product.Name;
break ;
case 'City' :
dim.memberFunction = (data ) => data.Seller.City;
break ;
case 'SellerName' :
dim.memberFunction = (data ) => data.Seller.Name;
break ;
}
}
}
ts コピー <div class ="controls-holder" >
<div class ="switches" >
<button igxButton ="contained" (click )="restoreGridState()" >
<igx-icon class ="btn-icon" > restore</igx-icon >
<span > Restore</span >
</button >
<button igxButton ="contained" (click )="saveGridState()" >
<igx-icon class ="btn-icon" > save</igx-icon >
<span > Save</span >
</button >
<button igxButton ="contained" [routerLink ]="['../pivot-state-about']" >
<igx-icon class ="btn-icon" > forward</igx-icon >
<span > Leave</span >
</button >
<button igxButton ="contained" (click )="clearStorage()" >
<igx-icon class ="btn-icon" > delete</igx-icon >
<span > Clear</span >
</button >
<button igxButton ="contained" (click )="reloadPage()" >
<igx-icon class ="btn-icon" > refresh</igx-icon >
<span > Reload</span >
</button >
</div >
<div class ="switches" >
<ul >
<li > Clicking the SAVE button or leaving the page <a
[routerLink ]="['../pivot-state-about']" > <strong > here</strong > </a > will save grid state to
localStorage.</li >
<li > Use the control buttons to SAVE / RESTORE / DELETE / grid state or LEAVE the page.</li >
<li > Select/Deselect checkboxes to control saving / restoring feature state.</li >
</ul >
</div >
<div class ="switches" >
<div class ="control-item" >
<igx-checkbox [checked ]="true" (change )="onChange($event, 'toggleAll')" > All</igx-checkbox >
</div >
<div class ="control-item" *ngFor ="let f of features" >
<igx-checkbox (change )="onChange($event, f.key)" [checked ]="options[f.key]" >
{{ f.shortName }}
</igx-checkbox >
</div >
</div >
</div >
<igx-pivot-grid #grid1 [data ]="data" [pivotConfiguration ]="pivotConfig" [rowSelection ]="'single'" [height ]="'600px'"
[superCompactMode ]="true" [defaultExpandState ]='true' [columnSelection ]="'single'" [igxGridState ]="options"
(valueInit )='onValueInit($event)' (dimensionInit )='onDimensionInit($event)' >
</igx-pivot-grid >
html コピー :host {
padding : 8px ;
display : flex;
flex-direction : column;
::ng-deep {
.upFontValue {
color: var(--ig-success-500 );
}
.downFontValue {
color : var(--ig-error-500 );
}
}
}
igx-pivot-grid {
flex : 1 ;
}
.pivot-container {
display : flex;
align-items : flex-start;
flex : 1 1 auto;
order : 0 ;
}
.controls-holder {
display : flex;
justify-content : space-between;
align-items : center;
flex-wrap : wrap;
width : 100% ;
}
.switches {
display : flex;
justify-content : flex-start;
align-items : center;
flex : 1 0 0% ;
min-width : 100% ;
padding-right : 20px ;
font-size : 0.9rem ;
margin-top : 0 ;
>button {
margin-right : 10px ;
}
}
.control-item {
display : block;
padding : 8px 0 ;
>span {
cursor : pointer;
}
margin-right : 10px ;
}
scss コピー
이 샘플이 마음에 드시나요? 전체 Ignite UI for Angular 툴킷에 액세스하고 몇 분 안에 나만의 앱을 구축해 보세요. 무료로 다운로드하세요.
피벗 전략 복원
IgxGridState
기본적으로 원격 피벗 작업이나 사용자 정의 차원 전략(자세한 내용은 피벗 그리드 원격 작업 샘플 참조)을 유지하지 않습니다(limitations
참조). 이들 중 하나를 복원하는 것은 애플리케이션 수준의 코드를 사용하여 수행할 수 있습니다. IgxGridState
는 그리드 상태가 적용되기 전에 추가로 수정하는 데 사용할 수 있는 stateParsed
라는 이벤트를 노출합니다. 이를 수행하는 방법을 보여드리겠습니다.
stateParsed
는 문자열 인수와 함께 setState
사용할 때만 방출됩니다.
사용자 정의 정렬 전략과 사용자 정의 피벗 열 및 행 차원 전략을 설정합니다.
<igx-pivot-grid #grid [data ]="data" [pivotConfiguration ]="pivotConfigHierarchy" [defaultExpandState ]='true'
[igxGridState ]="options" [sortStrategy ]="customStrategy" [pivotUI ]='{ showConfiguration: false }' [superCompactMode ]="true" [height ]="'500px'" >
</igx-pivot-grid >
html
@ViewChild (IgxGridStateDirective, { static : true })
public state!: IgxGridStateDirective;
public customStrategy = NoopSortingStrategy.instance();
public options: IGridStateOptions = {...};
public pivotConfigHierarchy: IPivotConfiguration = {
columnStrategy : NoopPivotDimensionsStrategy.instance(),
rowStrategy : NoopPivotDimensionsStrategy.instance(),
columns : [...],
rows : [...],
values : [...],
filters : [...]
};
typescript
sessionStorage
에서 상태를 복원하고 사용자 정의 전략을 적용하는 것은 다음과 같습니다.
public restoreState ( ) {
const state = window .sessionStorage.getItem('grid-state' );
this .state.stateParsed.pipe(take(1 )).subscribe(parsedState => {
parsedState.sorting.forEach(x => x.strategy = NoopSortingStrategy.instance());
parsedState.pivotConfiguration.rowStrategy = NoopPivotDimensionsStrategy.instance();
parsedState.pivotConfiguration.columnStrategy = NoopPivotDimensionsStrategy.instance();
});
this .state.setState(state as string );
}
typescript
import { AfterViewInit, Component, ViewChild } from "@angular/core" ;
import { IPivotConfiguration, IgxPivotNumericAggregate, NoopPivotDimensionsStrategy, IgxPivotGridComponent, NoopSortingStrategy, IGridState, IGridStateOptions, IgxGridStateDirective, IgxButtonDirective, IgxIconComponent } from "igniteui-angular"
import { PivotDataService } from "../../services/pivotRemoteData.service" ;
import { take } from 'rxjs/operators' ;
@Component ({
selector : 'app-pivot-grid-noop-persistence-sample' ,
styleUrls : ['./pivot-grid-noop-persistence-sample.component.scss' ],
templateUrl : './pivot-grid-noop-persistence-sample.component.html' ,
providers : [PivotDataService],
imports : [IgxButtonDirective, IgxIconComponent, IgxPivotGridComponent, IgxGridStateDirective]
})
export class PivotGridNoopPersistenceSampleComponent implements AfterViewInit {
@ViewChild ('grid' , { static : true })
public grid: IgxPivotGridComponent;
@ViewChild (IgxGridStateDirective, { static : true })
public state!: IgxGridStateDirective;
public customStrategy = NoopSortingStrategy.instance();
public data: any [];
public options: IGridStateOptions = {
cellSelection : true ,
rowSelection : true ,
filtering : true ,
sorting : true ,
expansion : true ,
columnSelection : true ,
pivotConfiguration : true
};
public pivotConfigHierarchy: IPivotConfiguration = {
columnStrategy : NoopPivotDimensionsStrategy.instance(),
rowStrategy : NoopPivotDimensionsStrategy.instance(),
columns : [
{
memberName : 'Country' ,
enabled : true
}
],
rows : [
{
memberFunction : () => 'All' ,
memberName : 'AllProducts' ,
enabled : true ,
childLevel : {
memberFunction : (data ) => data.ProductCategory,
memberName : 'ProductCategory' ,
enabled : true
}
},
{
memberName : 'AllSeller' ,
memberFunction : () => 'All Sellers' ,
enabled : true ,
childLevel : {
enabled : true ,
memberName : 'SellerName'
}
}
],
values : [
{
member : 'UnitsSold' ,
aggregate : {
aggregator : IgxPivotNumericAggregate.sum,
key : 'sum' ,
label : 'Sum'
},
enabled : true ,
formatter : (value ) => value ? value : 0
}
],
filters : null
};
constructor (private _remoteService: PivotDataService ) {
}
ngAfterViewInit(): void {
this .grid.isLoading = true ;
this ._remoteService.getData().subscribe((data: any ) => {
this .grid.isLoading = false ;
this .data = data;
});
}
public saveState ( ) {
const state = this .state.getState() as string ;
window .sessionStorage.setItem('grid-state' , state);
}
public restoreState ( ) {
const state = window .sessionStorage.getItem('grid-state' );
this .state.stateParsed.pipe(take(1 )).subscribe(parsedState => {
parsedState.sorting.forEach(expression => expression.strategy = NoopSortingStrategy.instance());
parsedState.pivotConfiguration.rowStrategy = NoopPivotDimensionsStrategy.instance();
parsedState.pivotConfiguration.columnStrategy = NoopPivotDimensionsStrategy.instance();
});
this .state.setState(state as string );
}
public clearStorage ( ) {
window .sessionStorage.removeItem('grid-state' );
}
}
ts コピー <div class ="switches" >
<button igxButton ="contained" (click )="restoreState()" >
<igx-icon class ="btn-icon" > restore</igx-icon >
<span > Restore</span >
</button >
<button igxButton ="contained" (click )="saveState()" >
<igx-icon class ="btn-icon" > save</igx-icon >
<span > Save</span >
</button >
<button igxButton ="contained" (click )="clearStorage()" >
<igx-icon class ="btn-icon" > delete</igx-icon >
<span > Clear</span >
</button >
</div >
<igx-pivot-grid #grid [data ]="data" [pivotConfiguration ]="pivotConfigHierarchy" [defaultExpandState ]='true'
[igxGridState ]="options" [sortStrategy ]="customStrategy" [pivotUI ]='{ showConfiguration: false }' [superCompactMode ]="true" [height ]="'500px'" >
</igx-pivot-grid >
html コピー :host {
display : block;
padding : 8px ;
}
.switches {
display : flex;
justify-content : flex-start;
align-items : center;
flex : 1 0 0% ;
min-width : 100% ;
padding-right : 20px ;
font-size : 0.9rem ;
margin-top : 0 ;
margin-bottom : 10px ;
>button {
margin-right : 10px ;
}
}
scss コピー
제한 사항
API 참조
추가 리소스