new changes filter section add new ui
This commit is contained in:
@@ -1,8 +1,13 @@
|
||||
<div class="grid grid-cols-12 gap-6">
|
||||
<div class="xl:col-span-12 col-span-12">
|
||||
<app-filter-card title="Filter" titleIcon="ti ti-filter" headerClass="!py-2" bodyClass="!px-4 !py-2.5">
|
||||
<form [formGroup]="countryFilterForm" autocomplete="off" class="grid w-full grid-cols-12 items-end gap-3">
|
||||
<div class="col-span-12 sm:col-span-5 lg:col-span-2">
|
||||
<app-data-table [columns]="columns()" [rows]="states()" [actions]="actions()"
|
||||
[totalRecords]="totalRecords()" [pageIndex]="queryState.pageIndex()" [pageSize]="queryState.pageSize()"
|
||||
tableTitle="States" buttonTitle="Add" [showSearch]="true"
|
||||
[showAddButton]="true" [showFilterButton]="true" [filterActive]="showFilters()" searchPlaceholder="Search..." [searchDebounceTime]="300"
|
||||
[emptyMessage]="emptyMessage()" [emptyDescription]="emptyDescription()"
|
||||
(addClicked)="onAddState()" (filterClicked)="onToggleFilters()" (searchChanged)="onSearch($event)" (pageChanged)="onPageChange($event)"
|
||||
(sortChanged)="onSortChange($event)" (actionClicked)="onActionClick($event)" toolTip="Add State">
|
||||
<ng-template appDataTableToolbar>
|
||||
<form [formGroup]="countryFilterForm" autocomplete="off" class="flex flex-nowrap items-end gap-3">
|
||||
<div class="w-48 shrink-0">
|
||||
<app-autocomplete
|
||||
formControlName="countryId"
|
||||
inputId="state-country-filter"
|
||||
@@ -23,24 +28,14 @@
|
||||
(itemSelected)="onCountryLookupSelected($event)"
|
||||
/>
|
||||
</div>
|
||||
<div class="col-span-12 sm:col-span-3 lg:col-span-1">
|
||||
<button type="button" class="ti-btn ti-btn-sm ti-btn-primary-full !mb-0 flex min-h-8 w-full items-center justify-center gap-1.5 !px-3 md:!w-auto" (click)="applyCountryFilter()">
|
||||
<i class="ti ti-filter" aria-hidden="true"></i>
|
||||
<span>Filter</span>
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</app-filter-card>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<app-data-table [columns]="columns()" [rows]="states()" [actions]="actions()"
|
||||
[totalRecords]="totalRecords()" [pageIndex]="queryState.pageIndex()" [pageSize]="queryState.pageSize()"
|
||||
tableTitle="States" buttonTitle="Add" [showSearch]="true"
|
||||
[showAddButton]="true" searchPlaceholder="Search..." [searchDebounceTime]="300"
|
||||
[emptyMessage]="emptyMessage()" [emptyDescription]="emptyDescription()"
|
||||
(addClicked)="onAddState()" (searchChanged)="onSearch($event)" (pageChanged)="onPageChange($event)"
|
||||
(sortChanged)="onSortChange($event)" (actionClicked)="onActionClick($event)" toolTip="Add State" />
|
||||
</ng-template>
|
||||
<ng-template appDataTableFilterActions>
|
||||
<button type="button" class="state-filter-apply-btn ti-btn ti-btn-sm ti-btn-primary-full !m-0 flex min-h-8 shrink-0 items-center justify-center gap-1.5 whitespace-nowrap !px-4" (click)="applyCountryFilter()">
|
||||
<span>Apply</span>
|
||||
</button>
|
||||
</ng-template>
|
||||
</app-data-table>
|
||||
|
||||
<app-confirm-dialog
|
||||
title="Delete State"
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
.state-filter-apply-btn {
|
||||
min-width: 3rem;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
@@ -40,7 +40,8 @@ import {
|
||||
import { FormInput } from '../../../../../shared/components/form/form-input/form-input';
|
||||
import { Modal } from '../../../../../shared/components/modal/modal';
|
||||
import { ConfirmDialog } from '../../../../../shared/components/confirm-dialog/confirm-dialog';
|
||||
import { FilterCard } from '../../../../../shared/components/filter-card/filter-card';
|
||||
import { DataTableToolbarDirective } from '../../../../../shared/directives/data-table-toolbar/data-table-toolbar.directive';
|
||||
import { DataTableFilterActionsDirective } from '../../../../../shared/directives/data-table-filter-actions/data-table-filter-actions.directive';
|
||||
|
||||
interface StateTableRow extends DataTableRecord {
|
||||
id: string;
|
||||
@@ -56,7 +57,7 @@ interface StateTableRow extends DataTableRecord {
|
||||
@Component({
|
||||
selector: 'state-list',
|
||||
standalone: true,
|
||||
imports: [DataTable, Modal, ReactiveFormsModule, FormInput, Autocomplete, ConfirmDialog, FilterCard],
|
||||
imports: [DataTable, Modal, ReactiveFormsModule, FormInput, Autocomplete, ConfirmDialog, DataTableToolbarDirective, DataTableFilterActionsDirective],
|
||||
templateUrl: './state-list.html',
|
||||
styleUrl: './state-list.scss',
|
||||
})
|
||||
@@ -80,6 +81,7 @@ export class StateList {
|
||||
readonly totalRecords = signal(0);
|
||||
readonly filteredRecords = signal(0);
|
||||
readonly saving = signal(false);
|
||||
readonly showFilters = signal(false);
|
||||
|
||||
readonly showStateModal = signal(false);
|
||||
readonly stateModalMode = signal<StateModalMode>('create');
|
||||
@@ -259,6 +261,10 @@ export class StateList {
|
||||
this.stateQueryRequests$.next(query);
|
||||
}
|
||||
|
||||
onToggleFilters(): void {
|
||||
this.showFilters.update(value => !value);
|
||||
}
|
||||
|
||||
onCountryLookupSelected(country: CountryLookupDto): void {
|
||||
this.selectedCountryLookup.set(country);
|
||||
}
|
||||
|
||||
@@ -7,6 +7,13 @@
|
||||
{{ tableTitle () }}
|
||||
</h5>
|
||||
<div class="flex flex-wrap items-center gap-3">
|
||||
@if(showFilterButton()){
|
||||
<div animate.enter="filter-btn-enter" animate.leave="filter-btn-leave">
|
||||
<app-button action="custom" icon="ti ti-filter" label="Filter" size="sm" iconClass="!text-[1rem]"
|
||||
[variant]="filterActive() ? 'primary-full' : 'light'" className="!rounded-full shadow-sm !whitespace-nowrap"
|
||||
(buttonClicked)="onFilterClick($event)" />
|
||||
</div>
|
||||
}
|
||||
@if(showAddButton()){
|
||||
<div>
|
||||
<app-button action="add" [label]="buttonTitle()" size="sm" iconClass="!text-[1rem]"
|
||||
@@ -14,11 +21,6 @@
|
||||
|
||||
</div>
|
||||
}
|
||||
<!-- @if (toolbarTemplate(); as toolbar) {
|
||||
<div>
|
||||
<ng-container [ngTemplateOutlet]="toolbar.templateRef" />
|
||||
</div>
|
||||
} -->
|
||||
@if (showSearch()) {
|
||||
<div class="search-wrapper">
|
||||
<i class="ri-search-line search-icon"></i>
|
||||
@@ -30,6 +32,25 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@if (toolbarTemplate(); as toolbar) {
|
||||
<div class="table-filter-row" [class.is-open]="filterActive()">
|
||||
<div class="table-filter-row-inner">
|
||||
<div class="table-filter-row-content" #filterRowContent>
|
||||
<ng-container [ngTemplateOutlet]="toolbar.templateRef" />
|
||||
@if (filterActionsTemplate(); as filterActions) {
|
||||
<div class="table-filter-row-actions" [class.is-pinned]="filterRowOverflowing()">
|
||||
<ng-container [ngTemplateOutlet]="filterActions.templateRef" />
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
<button type="button" class="ti-btn ti-btn-icon ti-btn-sm ti-btn-light !rounded-full !m-0 shrink-0 filter-close-btn"
|
||||
aria-label="Close filter" (click)="onFilterClick($event)">
|
||||
<i class="ti ti-x"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
<div class="box-body !p-0 modern-table-body">
|
||||
<div class="table-responsive overflow-x-auto overflow-y-visible modern-table-wrapper">
|
||||
<table [class]="tableClass()">
|
||||
|
||||
@@ -35,6 +35,159 @@
|
||||
color: var(--color-defaulttextcolor);
|
||||
}
|
||||
|
||||
/* Filter toggle button (in the toolbar) */
|
||||
.filter-btn-enter {
|
||||
animation: filterButtonIn 0.2s ease-out;
|
||||
}
|
||||
|
||||
.filter-btn-leave {
|
||||
animation: filterButtonOut 0.15s ease-in forwards;
|
||||
}
|
||||
|
||||
@keyframes filterButtonIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: scale(0.9);
|
||||
}
|
||||
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: scale(1);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes filterButtonOut {
|
||||
from {
|
||||
opacity: 1;
|
||||
transform: scale(1);
|
||||
}
|
||||
|
||||
to {
|
||||
opacity: 0;
|
||||
transform: scale(0.9);
|
||||
}
|
||||
}
|
||||
|
||||
/* Full-width filter row, joined to the table card */
|
||||
.table-filter-row {
|
||||
display: grid;
|
||||
grid-template-rows: 0fr;
|
||||
border-bottom-width: 0;
|
||||
background-color: color-mix(in srgb, var(--color-primary) 3%, transparent);
|
||||
transition: grid-template-rows 0.38s cubic-bezier(0.4, 0, 0.2, 1),
|
||||
border-bottom-width 0.38s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
}
|
||||
|
||||
.table-filter-row.is-open {
|
||||
grid-template-rows: 1fr;
|
||||
border-bottom-width: 1px;
|
||||
border-bottom-style: solid;
|
||||
border-bottom-color: var(--color-defaultborder);
|
||||
}
|
||||
|
||||
:host-context(.dark) .table-filter-row {
|
||||
border-color: color-mix(in srgb, #fff 10%, transparent);
|
||||
background-color: color-mix(in srgb, #fff 3%, transparent);
|
||||
}
|
||||
|
||||
.table-filter-row-inner {
|
||||
grid-row: 1;
|
||||
grid-column: 1;
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 0.75rem;
|
||||
padding: 0 1.25rem;
|
||||
min-height: 0;
|
||||
min-width: 0;
|
||||
opacity: 0;
|
||||
transition: opacity 0.28s ease 0.06s, padding 0.38s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
align-items: baseline;
|
||||
}
|
||||
|
||||
.table-filter-row:not(.is-open) .table-filter-row-inner {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.table-filter-row.is-open .table-filter-row-inner {
|
||||
padding: 0.875rem 1.25rem;
|
||||
opacity: 1;
|
||||
transition: opacity 0.32s ease 0.08s, padding 0.38s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
}
|
||||
|
||||
.table-filter-row-content {
|
||||
flex: 1 1 auto;
|
||||
display: flex;
|
||||
flex-wrap: nowrap;
|
||||
align-items: flex-end;
|
||||
gap: 0.75rem;
|
||||
min-width: 0;
|
||||
overflow-x: auto;
|
||||
overflow-y: hidden;
|
||||
padding-bottom: 0.5rem;
|
||||
margin-bottom: -0.5rem;
|
||||
padding-top: 10px;
|
||||
scrollbar-width: thin;
|
||||
scrollbar-color: color-mix(in srgb, var(--color-primary) 40%, transparent) transparent;
|
||||
}
|
||||
|
||||
.table-filter-row-content>* {
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
|
||||
.table-filter-row-content::-webkit-scrollbar {
|
||||
height: 2px;
|
||||
}
|
||||
|
||||
.table-filter-row-content::-webkit-scrollbar-thumb {
|
||||
background-color: color-mix(in srgb, var(--color-primary) 45%, transparent);
|
||||
border-radius: 999px;
|
||||
transition: background-color 0.2s ease;
|
||||
}
|
||||
|
||||
.table-filter-row-content:hover::-webkit-scrollbar-thumb {
|
||||
background-color: color-mix(in srgb, var(--color-primary) 65%, transparent);
|
||||
}
|
||||
|
||||
.table-filter-row-content::-webkit-scrollbar-track {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.table-filter-row-actions {
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
gap: 0.75rem;
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
|
||||
.table-filter-row-actions.is-pinned {
|
||||
position: sticky;
|
||||
right: 0;
|
||||
margin-left: auto;
|
||||
padding-left: 0.75rem;
|
||||
background: linear-gradient(to right, transparent, color-mix(in srgb, var(--color-primary) 3%, var(--color-white)) 1rem);
|
||||
}
|
||||
|
||||
:host-context(.dark) .table-filter-row-actions.is-pinned {
|
||||
background: linear-gradient(to right, transparent, color-mix(in srgb, #fff 3%, var(--color-bodybg)) 1rem);
|
||||
}
|
||||
|
||||
.table-filter-row .filter-close-btn {
|
||||
margin-top: 0.125rem;
|
||||
}
|
||||
|
||||
.filter-close-btn {
|
||||
transition: transform 0.15s ease, background-color 0.15s ease;
|
||||
border-color: #7c3deb;
|
||||
}
|
||||
|
||||
.filter-close-btn:hover {
|
||||
transform: rotate(90deg);
|
||||
}
|
||||
|
||||
.filter-close-btn:active {
|
||||
transform: rotate(90deg) scale(0.9);
|
||||
}
|
||||
|
||||
/* Search input */
|
||||
.search-wrapper {
|
||||
position: relative;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { NgTemplateOutlet } from '@angular/common';
|
||||
import { Component, DestroyRef, HostListener, computed, contentChildren, effect, inject, input, output, signal } from '@angular/core';
|
||||
import { Component, DestroyRef, ElementRef, HostListener, computed, contentChildren, effect, inject, input, output, signal, viewChild } from '@angular/core';
|
||||
import { MatPaginatorModule, PageEvent } from '@angular/material/paginator';
|
||||
import { Subject } from 'rxjs';
|
||||
import { debounceTime, distinctUntilChanged } from 'rxjs/operators';
|
||||
@@ -13,6 +13,7 @@ import { Button } from '../button/button';
|
||||
import { contentChild } from '@angular/core';
|
||||
|
||||
import { DataTableToolbarDirective } from '../../directives/data-table-toolbar/data-table-toolbar.directive';
|
||||
import { DataTableFilterActionsDirective } from '../../directives/data-table-filter-actions/data-table-filter-actions.directive';
|
||||
|
||||
@Component({
|
||||
selector: 'app-data-table',
|
||||
@@ -33,6 +34,33 @@ export class DataTable<T extends DataTableRecord = DataTableRecord> {
|
||||
|
||||
readonly cellTemplates = contentChildren(DataTableCellDirective);
|
||||
readonly toolbarTemplate = contentChild(DataTableToolbarDirective);
|
||||
readonly filterActionsTemplate = contentChild(DataTableFilterActionsDirective);
|
||||
|
||||
readonly filterRowContent = viewChild<ElementRef<HTMLElement>>('filterRowContent');
|
||||
readonly filterRowOverflowing = signal(false);
|
||||
private filterRowResizeObserver?: ResizeObserver;
|
||||
|
||||
private readonly filterRowContentEffect = effect(onCleanup => {
|
||||
const element = this.filterRowContent()?.nativeElement;
|
||||
|
||||
this.filterRowResizeObserver?.disconnect();
|
||||
|
||||
if (!element) {
|
||||
this.filterRowOverflowing.set(false);
|
||||
return;
|
||||
}
|
||||
|
||||
const checkOverflow = () => {
|
||||
this.filterRowOverflowing.set(element.scrollWidth > element.clientWidth + 1);
|
||||
};
|
||||
|
||||
checkOverflow();
|
||||
|
||||
this.filterRowResizeObserver = new ResizeObserver(checkOverflow);
|
||||
this.filterRowResizeObserver.observe(element);
|
||||
|
||||
onCleanup(() => this.filterRowResizeObserver?.disconnect());
|
||||
});
|
||||
|
||||
private readonly defaultRowClasses = [
|
||||
'table-primary',
|
||||
@@ -106,6 +134,8 @@ export class DataTable<T extends DataTableRecord = DataTableRecord> {
|
||||
showSearch = input<boolean>(false);
|
||||
showformSelect = input<boolean>(false);
|
||||
showAddButton = input<boolean>(false);
|
||||
showFilterButton = input<boolean>(false);
|
||||
filterActive = input<boolean>(false);
|
||||
searchPlaceholder = input('Search...');
|
||||
searchDebounceTime = input(300);
|
||||
emptyMessage = input('No records found');
|
||||
@@ -141,6 +171,7 @@ export class DataTable<T extends DataTableRecord = DataTableRecord> {
|
||||
actionHeaderClass = input<string>('!text-center');
|
||||
actionCellClass = input<string>('!text-center');
|
||||
addClicked = output<void>();
|
||||
filterClicked = output<void>();
|
||||
|
||||
totalVisibleColumns = computed(() => this.columns().length + (this.actions().length > 0 ? 1 : 0));
|
||||
|
||||
@@ -299,6 +330,14 @@ export class DataTable<T extends DataTableRecord = DataTableRecord> {
|
||||
this.addClicked.emit();
|
||||
}
|
||||
|
||||
onFilterClick(event: MouseEvent): void {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
|
||||
this.closeActionMenu();
|
||||
this.filterClicked.emit();
|
||||
}
|
||||
|
||||
toggleActionMenu(event: MouseEvent, row: T): void {
|
||||
event.stopPropagation();
|
||||
|
||||
|
||||
+15
@@ -0,0 +1,15 @@
|
||||
import {
|
||||
Directive,
|
||||
TemplateRef,
|
||||
inject
|
||||
} from '@angular/core';
|
||||
|
||||
@Directive({
|
||||
selector: '[appDataTableFilterActions]',
|
||||
standalone: true
|
||||
})
|
||||
export class DataTableFilterActionsDirective {
|
||||
readonly templateRef = inject(
|
||||
TemplateRef<unknown>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user