Skip to content

Commit

Permalink
feat: added dbxSearchableTextFieldComponent
Browse files Browse the repository at this point in the history
- Added dbxSearchableTextFieldComponent
- Added dbxSearchableChipFieldComponent
  • Loading branch information
dereekb committed Feb 20, 2022
1 parent c2a4b89 commit 42ae14c
Show file tree
Hide file tree
Showing 29 changed files with 664 additions and 179 deletions.
64 changes: 64 additions & 0 deletions packages/date/src/lib/expires/expires.rxjs.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import { Milliseconds } from './../../../../util/src/lib/date/date';
import { timeHasExpired } from '@dereekb/date';
import { DateOrUnixDateTimeNumber } from "@dereekb/util";
import { filter, map, MonoTypeOperatorFunction, Observable, OperatorFunction, skipUntil, skipWhile, switchMap, takeWhile } from "rxjs";
import { hasExpired, toExpires, Expires } from "./expires";

/**
* Creates a new Expires object at the current time on emission that will expire in the set amount of time.
*
* @param expiresIn
* @returns
*/
export function toExpiration<T>(expiresIn: number): OperatorFunction<T, Expires> {
return map(_ => toExpires(new Date(), expiresIn));
}

/**
* Filters further emissions once the input is expired.
*/
export function skipExpired<T extends Expires>(): MonoTypeOperatorFunction<T> {
return filter(expires => !hasExpired(expires));
}

/**
* Skips the input date or timenumber until expiration occurs.
*/
export function skipUntilExpiration(expiresIn?: number): MonoTypeOperatorFunction<DateOrUnixDateTimeNumber> {
return filter(x => timeHasExpired(x, expiresIn));
}

/**
* Skips the input date or timenumber after expiration occurs.
*/
export function skipAfterExpiration(expiresIn?: number): MonoTypeOperatorFunction<DateOrUnixDateTimeNumber> {
return filter(x => !timeHasExpired(x, expiresIn));
}

/**
* Skips emissions until time since the last emission from the watch observable has elapsed.
*/
export function skipUntilTimeElapsedAfterLastEmission<T>(watch: Observable<any>, takeFor: Milliseconds): MonoTypeOperatorFunction<T> {
return (observable: Observable<T>) => {
return watch.pipe(
switchMap(() => {
const expires = toExpires(new Date(), takeFor);
return observable.pipe(takeWhile(_ => !hasExpired(expires)));
})
);
};
}

/**
* Takes emissions until time since the last emission from the watch observable has elapsed.
*/
export function takeAfterTimeElapsedSinceLastEmission<T>(watch: Observable<any>, skipFor: Milliseconds): MonoTypeOperatorFunction<T> {
return (observable: Observable<T>) => {
return watch.pipe(
switchMap(() => {
const expires = toExpires(new Date(), skipFor);
return observable.pipe(skipWhile(_ => !hasExpired(expires)));
})
);
};
}
21 changes: 14 additions & 7 deletions packages/date/src/lib/expires/expires.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { dateFromDateOrTimeNumber } from '@dereekb/date';
import { Maybe, UnixDateTimeNumber } from '@dereekb/util';
import { dateFromDateOrTimeNumber } from '../date/date.unix';
import { DateOrUnixDateTimeNumber, Maybe, UnixDateTimeNumber } from '@dereekb/util';
import { addMilliseconds, addMinutes, isPast } from 'date-fns';

/**
Expand Down Expand Up @@ -46,19 +46,26 @@ export function anyHaveExpired(expires: Maybe<Expires>[], expireIfEmpty = true):
return false;
}

export function timeNumberHasExpired(timeNumber: UnixDateTimeNumber, expiresIn?: number): boolean {
return hasExpired(timeNumberToExpires(timeNumber, expiresIn));
/**
* Convenience function for checking if the input time has expired.
*
* @param timeNumber
* @param expiresIn
* @returns
*/
export function timeHasExpired(time: Maybe<DateOrUnixDateTimeNumber>, expiresIn?: number): boolean {
return hasExpired(toExpires(time, expiresIn));
}

/**
* Creates an Expires object from the input time number.
* Creates an Expires object from the input date or time number.
*
* @param timeNumber Number to convert to a date.
* @param expiresIn If the input number is the initial date, and not the
* expiration date, this is used to find the expiresAt time.
*/
export function timeNumberToExpires(timeNumber: UnixDateTimeNumber, expiresIn?: number): Expires {
let expiresAt = dateFromDateOrTimeNumber(timeNumber);
export function toExpires(time: Maybe<DateOrUnixDateTimeNumber>, expiresIn?: number): Expires {
let expiresAt = dateFromDateOrTimeNumber(time);

if (expiresAt && expiresIn) {
expiresAt = addMilliseconds(expiresAt, expiresIn);
Expand Down
1 change: 1 addition & 0 deletions packages/date/src/lib/expires/index.ts
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
export * from './expires';
export * from './expires.rxjs';
5 changes: 2 additions & 3 deletions packages/dbx-core/src/lib/injected/injected.component.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -97,11 +97,10 @@ describe('DbxInjectedComponent', () => {
const anchorElement: HTMLElement = fixture.debugElement.query(By.css(`#${CUSTOM_CONTENT_ID}`)).nativeElement;
expect(anchorElement).not.toBeNull();
expect(anchorElement.textContent).toBe(CUSTOM_CONTENT);



});

// todo: test injecting data.

});

});
Expand Down
18 changes: 10 additions & 8 deletions packages/dbx-core/src/lib/injected/injected.instance.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import { ComponentRef, Injector, ViewContainerRef } from '@angular/core';
import { distinctUntilChanged, map, shareReplay } from 'rxjs/operators';
import { BehaviorSubject, combineLatest } from 'rxjs';
import { DbxInjectedComponentConfig, DbxInjectedTemplateConfig } from './injected';
import { Initialized, Destroyable, Maybe } from '@dereekb/util';
import { DbxInjectedComponentConfig, DbxInjectedTemplateConfig, DBX_INJECTED_COMPONENT_DATA } from './injected';
import { Initialized, Destroyable, Maybe, mergeArrayOrValueIntoArray } from '@dereekb/util';
import { SubscriptionObject, filterMaybe, skipFirstMaybe } from '@dereekb/rxjs';

/**
Expand Down Expand Up @@ -96,16 +96,18 @@ export class DbxInjectedComponentInstance<T> implements Initialized, Destroyable
private _initComponent(config: DbxInjectedComponentConfig<T>, content: ViewContainerRef): void {
content.clear();

const { init, injector: inputInjector, providers, ngModuleRef, componentClass } = config;
const { init, injector: inputInjector, providers, ngModuleRef, componentClass, data } = config;

let injector: Injector | undefined;
const parentInjector = inputInjector ?? this._injector;

if (inputInjector) {
injector = inputInjector;
} else if (providers) {
if (Boolean(providers || data)) {
injector = Injector.create({
parent: this._injector,
providers
parent: parentInjector,
providers: mergeArrayOrValueIntoArray([{
provide: DBX_INJECTED_COMPONENT_DATA,
useValue: data
}], providers ?? [])
});
}

Expand Down
23 changes: 21 additions & 2 deletions packages/dbx-core/src/lib/injected/injected.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import { Injector, NgModuleRef, StaticProvider, TemplateRef, Type, ViewRef } from "@angular/core";
import { Maybe } from "@dereekb/util";
import { InjectionToken, Injector, NgModuleRef, StaticProvider, TemplateRef, Type, ViewRef } from "@angular/core";
import { filterMaybeValues, Maybe, mergeArrays, mergeIntoArray, mergeObjects } from "@dereekb/util";

export const DBX_INJECTED_COMPONENT_DATA = new InjectionToken('DbxInjectedComponentConfigData');

export interface DbxInjectedComponentConfig<T = any> {
/**
Expand All @@ -22,6 +24,10 @@ export interface DbxInjectedComponentConfig<T = any> {
* (Optional) Custom initialization code when an instance is created.
*/
init?: (instance: T) => void;
/**
* Any optional data to inject into the component.
*/
data?: any;
}

export interface DbxInjectedTemplateConfig<T = any> {
Expand All @@ -34,3 +40,16 @@ export interface DbxInjectedTemplateConfig<T = any> {
*/
viewRef?: Maybe<ViewRef>;
}

/**
* Merges multiple configurations into a single configuration.
*
* @param configs
* @returns
*/
export function mergeDbxInjectedComponentConfigs(configs: Maybe<Partial<DbxInjectedComponentConfig>>[]): Partial<DbxInjectedComponentConfig> {
const providers = mergeArrays(filterMaybeValues(configs).map(x => x.providers));
const result = mergeObjects(configs);
result.providers = providers;
return result;
}
4 changes: 2 additions & 2 deletions packages/dbx-core/src/lib/storage/storage.accessor.simple.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { Observable } from 'rxjs';
import { map } from 'rxjs/operators';
import { timeNumberHasExpired, unixTimeNumberForNow } from '@dereekb/date';
import { timeHasExpired, unixTimeNumberForNow } from '@dereekb/date';
import { filterMaybeValuesFn, DataDoesNotExistError, DataIsExpiredError, ReadStoredData, StoredData, StoredDataStorageKey, StoredDataString, Maybe } from '@dereekb/util';
import { StorageAccessor } from './storage.accessor';

Expand Down Expand Up @@ -199,7 +199,7 @@ export class SimpleStorageAccessor<T> implements StorageAccessor<T> {
const expiresIn = this._config.expiresIn;
if (expiresIn) {
if (storeData.storedAt) {
return timeNumberHasExpired(storeData.storedAt, expiresIn);
return timeHasExpired(storeData.storedAt, expiresIn);
}

return true;
Expand Down
4 changes: 2 additions & 2 deletions packages/dbx-core/src/lib/storage/storage.di.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { InjectionToken } from '@angular/core';

export const DEFAULT_STORAGE_OBJECT_TOKEN = new InjectionToken('DEFAULT_STORAGE_OBJECT');
export const DEFAULT_STORAGE_ACCESSOR_FACTORY_TOKEN = new InjectionToken('DEFAULT_STORAGE_ACCESSOR_FACTORY');
export const DEFAULT_STORAGE_OBJECT_TOKEN = new InjectionToken('DBX_UTIL_DEFAULT_STORAGE_OBJECT');
export const DEFAULT_STORAGE_ACCESSOR_FACTORY_TOKEN = new InjectionToken('DBX_UTIL_DEFAULT_STORAGE_ACCESSOR_FACTORY');
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,40 @@


// MARK: Mixin
@mixin core() {}
@mixin core() {

.dbx-searchable-text-field-value {
width: 100%;
}

.dbx-searchable-text-field-value.mat-option,
.dbx-searchable-text-field-autocomplete .mat-option {
padding: 0;
}

.dbx-default-searchable-field-display {
padding: 0 16px;
}

.dbx-searchable-text-field-has-value.dbx-searchable-text-field-show-value {

.dbx-searchable-text-field-value {
margin-bottom: -6px;
}

// hide without clearing display, which will prevent the input from being clickable.
.dbx-searchable-text-field-input {
height: 0;
opacity: 0;
}
}

.mat-focused .dbx-searchable-text-field-has-value.dbx-searchable-text-field-show-value .dbx-searchable-text-field-input {
opacity: unset;
height: unset;
}

}

@mixin color($theme-config) {}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,8 @@
</div>

<!-- Autocomplete -->
<mat-autocomplete #auto="matAutocomplete" (optionSelected)="selected($event)">
<mat-autocomplete class="dbx-searchable-text-field-autocomplete" #auto="matAutocomplete"
(optionSelected)="selected($event)">
<mat-option *ngFor="let displayValue of (searchResults$ | async)" [value]="displayValue">
<dbx-searchable-field-autocomplete-item [displayValue]="displayValue">
</dbx-searchable-field-autocomplete-item>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,17 +1,26 @@
import { SubscriptionObject } from '@dereekb/rxjs';
import { Subject } from 'rxjs';
import { Component } from '@angular/core';
import { MatAutocompleteSelectedEvent } from '@angular/material/autocomplete';
import { MatChipInputEvent } from '@angular/material/chips';
import { AbstractDbxSearchableValueFieldDirective, SearchableValueFieldsFieldConfig, SearchableValueFieldsFormlyFieldConfig } from './searchable.field.directive';
import { COMMA, ENTER } from '@angular/cdk/keycodes';
import { skipUntilTimeElapsedAfterLastEmission } from '@dereekb/date';

export interface SearchableChipValueFieldsFieldConfig<T> extends SearchableValueFieldsFieldConfig<T> { }
export interface SearchableChipValueFieldsFormlyFieldConfig<T> extends SearchableChipValueFieldsFieldConfig<T>, SearchableValueFieldsFormlyFieldConfig<T> { }

export interface SearchableChipValueFieldsFormlyFieldConfig<T> extends SearchableValueFieldsFormlyFieldConfig<T> {
searchableField: SearchableChipValueFieldsFieldConfig<T>;
}

@Component({
templateUrl: 'searchable.chip.field.component.html'
})
export class DbxSearchableChipFieldComponent<T> extends AbstractDbxSearchableValueFieldDirective<T, SearchableChipValueFieldsFormlyFieldConfig<T>> {

private _blur = new Subject<void>();
private _blurSub = new SubscriptionObject();

readonly separatorKeysCodes: number[] = [ENTER, COMMA];

selected(event: MatAutocompleteSelectedEvent): void {
Expand All @@ -35,23 +44,23 @@ export class DbxSearchableChipFieldComponent<T> extends AbstractDbxSearchableVal
return this._addWithTextValue(text);
}

onBlur(): void {
this._tryAddCurrentInputValue();
}

_tryAddCurrentInputValue(): boolean {
let addedValue = false;
override ngOnInit(): void {
super.ngOnInit();

if (this.allowStringValues) {
const value = this.inputCtrl.value;
// Only try and add the text item as a value if a value wasn't just added (for example, clicking a value).
this._blurSub.subscription = this._blur.pipe(skipUntilTimeElapsedAfterLastEmission(this.values$, 100)).subscribe(() => {
this._tryAddCurrentInputValue();
});
}

if ((value || '').trim()) {
this._addWithTextValue(value);
addedValue = true;
}
}
override ngOnDestroy(): void {
super.ngOnDestroy();
this._blur.complete();
this._blurSub.destroy();
}

return addedValue;
onBlur(): void {
this._blur.next();
}

}
Loading

0 comments on commit 42ae14c

Please sign in to comment.