Signal updates

This commit is contained in:
Dan Percic 2024-07-09 12:11:15 +03:00
parent 01c244aa07
commit 9b1179d99a
18 changed files with 115 additions and 100 deletions

View File

@ -8,7 +8,6 @@ export * from './lib/loading';
export * from './lib/error';
export * from './lib/search';
export * from './lib/upload-file';
export * from './lib/empty-state';
export * from './lib/caching';
export * from './lib/translations';
export * from './lib/pipes';

View File

@ -0,0 +1 @@
export * from './chevron-button.component';

View File

@ -3,4 +3,3 @@ export * from './types/circle-button.type';
export * from './icon-button/icon-button.component';
export * from './circle-button/circle-button.component';
export * from './chevron-button/chevron-button.component';

View File

@ -1,9 +1,9 @@
import { booleanAttribute, Directive, Input } from '@angular/core';
import { booleanAttribute, Directive, input } from '@angular/core';
@Directive({
selector: '[iqserDisableStopPropagation]',
standalone: true,
})
export class DisableStopPropagationDirective {
@Input({ transform: booleanAttribute }) iqserDisableStopPropagation = true;
readonly iqserDisableStopPropagation = input(true, { transform: booleanAttribute });
}

View File

@ -1,6 +1,6 @@
import { booleanAttribute, Directive, HostListener, inject, Input } from '@angular/core';
import { DisableStopPropagationDirective } from './disable-stop-propagation.directive';
import { NGXLogger } from 'ngx-logger';
import { DisableStopPropagationDirective } from './disable-stop-propagation.directive';
@Directive({
selector: '[iqserStopPropagation]',
@ -13,7 +13,7 @@ export class StopPropagationDirective {
@HostListener('click', ['$event'])
onClick($event: Event) {
if (this.#disableStopPropagation?.iqserDisableStopPropagation) {
if (this.#disableStopPropagation?.iqserDisableStopPropagation()) {
this.#logger.info('[CLICK] iqserStopPropagation is disabled by iqserDisableStopPropagation');
return;
}

View File

@ -1,12 +1,5 @@
<div
[ngStyle]="{
'padding-top': verticalPadding + 'px',
'padding-left': horizontalPadding + 'px',
'padding-right': horizontalPadding + 'px',
}"
class="empty-state"
>
@if (icon) {
<div [ngStyle]="styles()" class="empty-state">
@if (icon(); as icon) {
<mat-icon [svgIcon]="icon"></mat-icon>
}
@ -14,15 +7,15 @@
<ng-content></ng-content>
</div>
<div [innerHTML]="text" class="heading-l"></div>
<div [innerHTML]="text()" class="heading-l"></div>
@if (showButton) {
@if (showButton() && this.action.observed) {
<iqser-icon-button
(action)="action.emit()"
[buttonId]="buttonId"
[icon]="buttonIcon"
[attr.help-mode-key]="helpModeKey"
[label]="buttonLabel"
[buttonId]="buttonId()"
[icon]="buttonIcon()"
[attr.help-mode-key]="helpModeKey()"
[label]="buttonLabel()"
[type]="iconButtonTypes.primary"
></iqser-icon-button>
}

View File

@ -1,32 +1,43 @@
import { ChangeDetectionStrategy, Component, EventEmitter, Input, OnInit, Output } from '@angular/core';
import { IconButtonComponent, IconButtonTypes } from '../buttons';
import { randomString } from '../utils';
import { NgStyle } from '@angular/common';
import {
booleanAttribute,
ChangeDetectionStrategy,
Component,
computed,
EventEmitter,
input,
numberAttribute,
Output,
} from '@angular/core';
import { MatIconModule } from '@angular/material/icon';
import { IconButtonComponent } from '../buttons/icon-button/icon-button.component';
import { IconButtonTypes } from '../buttons/types/icon-button.type';
import { randomString } from '../utils/functions';
@Component({
selector: 'iqser-empty-state [text]',
selector: 'iqser-empty-state',
templateUrl: './empty-state.component.html',
styleUrls: ['./empty-state.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
standalone: true,
imports: [NgStyle, MatIconModule, IconButtonComponent],
})
export class EmptyStateComponent implements OnInit {
readonly iconButtonTypes = IconButtonTypes;
export class EmptyStateComponent {
protected readonly iconButtonTypes = IconButtonTypes;
@Input() text!: string;
@Input() icon?: string;
@Input() showButton = true;
@Input() buttonIcon = 'iqser:plus';
@Input() buttonLabel?: string;
@Input() buttonId = `${randomString()}-icon-button`;
@Input() horizontalPadding = 100;
@Input() verticalPadding = 120;
@Input() helpModeKey?: string;
readonly text = input.required<string>();
readonly icon = input<string>();
readonly showButton = input(true, { transform: booleanAttribute });
readonly buttonIcon = input('iqser:plus');
readonly buttonLabel = input<string>();
readonly buttonId = input(`${randomString()}-icon-button`);
readonly horizontalPadding = input(100, { transform: numberAttribute });
readonly verticalPadding = input(120, { transform: numberAttribute });
protected readonly styles = computed(() => ({
'padding-top': this.verticalPadding() + 'px',
'padding-left': this.horizontalPadding() + 'px',
'padding-right': this.horizontalPadding() + 'px',
}));
readonly helpModeKey = input<string>();
@Output() readonly action = new EventEmitter();
ngOnInit(): void {
this.showButton = this.showButton && this.action.observed;
}
}

View File

@ -1,4 +1,4 @@
@if (errorService.connectionStatus$ | async; as status) {
@if (connectionStatus(); as status) {
<div [@animateOpenClose]="status" [ngClass]="status" class="indicator flex-align-items-center">
<span [translate]="connectionStatusTranslations[status]"></span>
</div>

View File

@ -1,6 +1,7 @@
import { ChangeDetectionStrategy, Component, inject } from '@angular/core';
import { connectionStatusTranslations } from '../../translations';
import { animate, state, style, transition, trigger } from '@angular/animations';
import { ChangeDetectionStrategy, Component, inject } from '@angular/core';
import { toSignal } from '@angular/core/rxjs-interop';
import { connectionStatusTranslations } from '../../translations';
import { ErrorService } from '../error.service';
@Component({
@ -18,6 +19,6 @@ import { ErrorService } from '../error.service';
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class ConnectionStatusComponent {
connectionStatusTranslations = connectionStatusTranslations;
protected readonly errorService = inject(ErrorService);
protected readonly connectionStatusTranslations = connectionStatusTranslations;
protected readonly connectionStatus = toSignal(inject(ErrorService).connectionStatus$);
}

View File

@ -1,7 +1,7 @@
import { ChangeDetectionStrategy, Component } from '@angular/core';
import { ChangeDetectionStrategy, Component, inject } from '@angular/core';
import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker';
import { IconButtonTypes } from '../../buttons';
import { CustomError, ErrorService, ErrorType } from '../error.service';
import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker';
@Component({
selector: 'iqser-full-page-error',
@ -10,9 +10,8 @@ import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker';
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class FullPageErrorComponent {
readonly iconButtonTypes = IconButtonTypes;
constructor(readonly errorService: ErrorService) {}
protected readonly iconButtonTypes = IconButtonTypes;
protected readonly errorService = inject(ErrorService);
errorTitle(error: ErrorType): string {
return error instanceof CustomError ? error.label : _('error.title');

View File

@ -9,7 +9,9 @@
></iqser-input-with-action>
</div>
}
<ng-container *ngTemplateOutlet="filterHeader"></ng-container>
@if (primaryFilters$ | async; as filters) {
<div class="filter-content">
@for (filter of filters; track filter) {
@ -24,11 +26,13 @@
}
</div>
}
@if (secondaryFilterGroup$ | async; as secondaryGroup) {
<div class="filter-options">
<div class="filter-menu-options">
<div class="all-caps-label" translate="filter-menu.filter-options"></div>
</div>
@for (filter of secondaryGroup.filters; track filter) {
<ng-container
[ngTemplateOutletContext]="{
@ -51,7 +55,11 @@
<ng-template #filterHeader>
@if (primaryFilterGroup$ | async; as primaryGroup) {
<div class="filter-menu-header">
<div [translateParams]="{ count: primaryGroup.filters.length }" [translate]="primaryFiltersLabel" class="all-caps-label"></div>
<div
[translateParams]="{ count: primaryGroup.filters.length }"
[translate]="primaryFiltersLabel()"
class="all-caps-label"
></div>
<div class="actions">
@if (!primaryGroup.singleSelect) {
<div
@ -61,6 +69,7 @@
translate="actions.all"
></div>
}
<div
(click)="deactivatePrimaryFilters()"
class="all-caps-label primary pointer"
@ -104,7 +113,7 @@
></ng-container>
</mat-checkbox>
<ng-container [ngTemplateOutletContext]="{ filter: filter }" [ngTemplateOutlet]="actionsTemplate"></ng-container>
<ng-container [ngTemplateOutletContext]="{ filter: filter }" [ngTemplateOutlet]="actionsTemplate()"></ng-container>
</div>
@if (filter.children?.length && filter.expanded) {
@ -122,7 +131,7 @@
[ngTemplateOutlet]="filterGroup.filterTemplate ?? defaultFilterLabelTemplate"
></ng-container>
</mat-checkbox>
<ng-container [ngTemplateOutletContext]="{ filter: child }" [ngTemplateOutlet]="actionsTemplate"></ng-container>
<ng-container [ngTemplateOutletContext]="{ filter: child }" [ngTemplateOutlet]="actionsTemplate()"></ng-container>
</div>
}
}

View File

@ -1,5 +1,5 @@
import { AsyncPipe, NgForOf, NgIf, NgTemplateOutlet } from '@angular/common';
import { ChangeDetectionStrategy, Component, ElementRef, Input, OnInit, TemplateRef } from '@angular/core';
import { AsyncPipe, NgTemplateOutlet } from '@angular/common';
import { ChangeDetectionStrategy, Component, effect, ElementRef, inject, input, numberAttribute, OnInit, TemplateRef } from '@angular/core';
import { MAT_CHECKBOX_DEFAULT_OPTIONS, MatCheckbox } from '@angular/material/checkbox';
import { MatIcon } from '@angular/material/icon';
import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker';
@ -23,7 +23,7 @@ const atLeastOneIsExpandable = pipe(
);
@Component({
selector: 'iqser-filter-card [primaryFiltersSlug]',
selector: 'iqser-filter-card',
templateUrl: './filter-card.component.html',
styleUrls: ['./filter-card.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
@ -38,38 +38,29 @@ const atLeastOneIsExpandable = pipe(
},
],
standalone: true,
imports: [
AsyncPipe,
InputWithActionComponent,
NgTemplateOutlet,
TranslateModule,
MatIcon,
MatCheckbox,
StopPropagationDirective,
NgIf,
NgForOf,
],
imports: [AsyncPipe, InputWithActionComponent, NgTemplateOutlet, TranslateModule, MatIcon, MatCheckbox, StopPropagationDirective],
})
export class FilterCardComponent implements OnInit {
@Input() primaryFiltersSlug!: string;
@Input() fileId?: string;
@Input() actionsTemplate?: TemplateRef<unknown>;
@Input() secondaryFiltersSlug = '';
@Input() primaryFiltersLabel: string = _('filter-menu.filter-types');
@Input() minWidth = 350;
readonly #filterService = inject(FilterService);
readonly #elementRef = inject(ElementRef);
protected readonly searchService = inject<SearchService<Filter>>(SearchService);
readonly primaryFiltersSlug = input.required<string>();
readonly fileId = input<string>();
readonly actionsTemplate = input<TemplateRef<unknown>>();
readonly secondaryFiltersSlug = input('');
readonly primaryFiltersLabel = input<string>(_('filter-menu.filter-types'));
readonly minWidth = input(350, { transform: numberAttribute });
primaryFilterGroup$!: Observable<IFilterGroup | undefined>;
secondaryFilterGroup$!: Observable<IFilterGroup | undefined>;
primaryFilters$!: Observable<IFilter[] | undefined>;
atLeastOneFilterIsExpandable$?: Observable<boolean>;
atLeastOneSecondaryFilterIsExpandable$?: Observable<boolean>;
constructor(
readonly filterService: FilterService,
readonly searchService: SearchService<Filter>,
private readonly _elementRef: ElementRef,
) {}
constructor() {
effect(() => {
(this.#elementRef.nativeElement as HTMLElement).style.setProperty('--filter-card-min-width', `${this.minWidth()}px`);
});
}
private get _primaryFilters$(): Observable<IFilter[]> {
return combineLatest([this.primaryFilterGroup$, this.searchService.valueChanges$]).pipe(
@ -79,39 +70,37 @@ export class FilterCardComponent implements OnInit {
}
ngOnInit() {
this.primaryFilterGroup$ = this.filterService.getGroup$(this.primaryFiltersSlug).pipe(shareLast());
this.secondaryFilterGroup$ = this.filterService.getGroup$(this.secondaryFiltersSlug).pipe(shareLast());
this.primaryFilterGroup$ = this.#filterService.getGroup$(this.primaryFiltersSlug()).pipe(shareLast());
this.secondaryFilterGroup$ = this.#filterService.getGroup$(this.secondaryFiltersSlug()).pipe(shareLast());
this.primaryFilters$ = this._primaryFilters$;
this.atLeastOneFilterIsExpandable$ = atLeastOneIsExpandable(this.primaryFilterGroup$);
this.atLeastOneSecondaryFilterIsExpandable$ = atLeastOneIsExpandable(this.secondaryFilterGroup$);
(this._elementRef.nativeElement as HTMLElement).style.setProperty('--filter-card-min-width', `${this.minWidth}px`);
}
filterCheckboxClicked(nestedFilter: INestedFilter, filterGroup: IFilterGroup, parent?: INestedFilter): void {
this.filterService.filterCheckboxClicked({
this.#filterService.filterCheckboxClicked({
nestedFilter,
filterGroup,
parent,
primaryFiltersSlug: this.primaryFiltersSlug,
primaryFiltersSlug: this.primaryFiltersSlug(),
});
this.filterService.updateFiltersInLocalStorage(this.fileId);
this.#filterService.updateFiltersInLocalStorage(this.fileId());
}
deactivatePrimaryFilters() {
this.filterService.deactivateFilters({ primaryFiltersSlug: this.primaryFiltersSlug });
this.filterService.updateFiltersInLocalStorage(this.fileId);
this.#filterService.deactivateFilters({ primaryFiltersSlug: this.primaryFiltersSlug() });
this.#filterService.updateFiltersInLocalStorage(this.fileId());
}
activatePrimaryFilters(): void {
this.filterService.setFilters(this.primaryFiltersSlug, true);
this.filterService.updateFiltersInLocalStorage(this.fileId);
this.#filterService.setFilters(this.primaryFiltersSlug(), true);
this.#filterService.updateFiltersInLocalStorage(this.fileId());
}
toggleFilterExpanded(nestedFilter: INestedFilter): void {
// eslint-disable-next-line no-param-reassign
nestedFilter.expanded = !nestedFilter.expanded;
this.filterService.refresh();
this.#filterService.refresh();
}
}

View File

@ -11,6 +11,7 @@
buttonId="{{ primaryGroup.slug }}"
></iqser-icon-button>
}
@if (!primaryGroup.icon) {
<iqser-chevron-button
[attr.aria-expanded]="expanded$ | async"
@ -21,6 +22,7 @@
[showDot]="hasActiveFilters$ | async"
></iqser-chevron-button>
}
<mat-menu
#filterMenu="matMenu"
(closed)="expanded.next(false)"

View File

@ -1,4 +1,4 @@
import { AsyncPipe, NgIf } from '@angular/common';
import { AsyncPipe } from '@angular/common';
import { ChangeDetectionStrategy, Component, Input, OnInit, TemplateRef } from '@angular/core';
import { MatMenu, MatMenuContent, MatMenuTrigger } from '@angular/material/menu';
import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker';
@ -28,7 +28,6 @@ import { IFilterGroup } from '../models/filter-group.model';
FilterCardComponent,
StopPropagationDirective,
MatMenuContent,
NgIf,
],
standalone: true,
})

View File

@ -3,7 +3,8 @@ import { MatCheckbox } from '@angular/material/checkbox';
import { MatMenuModule } from '@angular/material/menu';
import { TranslateModule } from '@ngx-translate/core';
import { ChevronButtonComponent, CircleButtonComponent, IconButtonComponent } from '../../buttons';
import { CircleButtonComponent, IconButtonComponent } from '../../buttons';
import { ChevronButtonComponent } from '../../buttons/chevron-button/chevron-button.component';
import { StopPropagationDirective } from '../../directives';
import { InputWithActionComponent } from '../../inputs/input-with-action/input-with-action.component';
import { SimpleFilterOption } from '../models/simple-filter-option';
@ -22,7 +23,6 @@ import { SimpleFilterOption } from '../models/simple-filter-option';
TranslateModule,
MatCheckbox,
IconButtonComponent,
ChevronButtonComponent,
CircleButtonComponent,
],
})

View File

@ -1,7 +1,7 @@
import { HttpClient } from '@angular/common/http';
import { PruningTranslationLoader } from '../utils';
import { getConfig, IqserConfigService } from '../services';
import { inject } from '@angular/core';
import { getConfig } from '../services';
import { PruningTranslationLoader } from '../utils';
export function pruningTranslationLoaderFactory(pathPrefix: string): PruningTranslationLoader {
const httpClient = inject(HttpClient);

View File

@ -1,10 +1,11 @@
import { provideHttpClient, withInterceptorsFromDi } from '@angular/common/http';
import { inject, InjectionToken, ModuleWithProviders, NgModule } from '@angular/core';
import { TranslateCompiler, TranslateLoader, TranslateModule, TranslateParser } from '@ngx-translate/core';
import { MissingTranslationHandler, TranslateCompiler, TranslateLoader, TranslateModule, TranslateParser } from '@ngx-translate/core';
import { TranslateMessageFormatCompiler } from 'ngx-translate-messageformat-compiler';
import { pruningTranslationLoaderFactory } from './http-loader-factory';
import { IqserTranslateModuleOptions } from './iqser-translate-module-options';
import { IqserTranslateParser } from './iqser-translate-parser.service';
import { provideHttpClient, withInterceptorsFromDi } from '@angular/common/http';
import { IqserMissingTranslationHandler } from './missing-translations-handler';
const translateLoaderToken = new InjectionToken('translateLoader');
@ -46,6 +47,10 @@ export class IqserTranslateModule {
provide: translateLoaderToken,
useFactory: () => pruningTranslationLoaderFactory(pathPrefix),
},
{
provide: MissingTranslationHandler,
useClass: IqserMissingTranslationHandler,
},
],
};
}

View File

@ -0,0 +1,8 @@
import { MissingTranslationHandler, MissingTranslationHandlerParams } from '@ngx-translate/core';
export class IqserMissingTranslationHandler implements MissingTranslationHandler {
handle(params: MissingTranslationHandlerParams) {
const missingKey = params.key;
return `?${missingKey}?`;
}
}