Skip to content

Commit

Permalink
feat(select/config): module configuration
Browse files Browse the repository at this point in the history
  • Loading branch information
matheusdavidson committed May 22, 2022
1 parent bac492e commit 1037fd3
Show file tree
Hide file tree
Showing 10 changed files with 795 additions and 85 deletions.
49 changes: 21 additions & 28 deletions projects/ng-tw/src/modules/option/option.component.html
Original file line number Diff line number Diff line change
@@ -1,30 +1,23 @@
<div
class="text-gray-900 cursor-default select-none relative py-2 hover:bg-gray-50"
[ngClass]="{'pl-9 pr-3': selectedIndicatorSide === 'left', 'pl-3 pr-9': selectedIndicatorSide === 'right', 'bg-gray-100': selected === true, 'bg-gray-50': active === true && selected !== true}"
role="option"
>
<div #content>
<ng-content></ng-content>
</div>
<div #content>
<ng-content></ng-content>
</div>

<span
class="text-primary-600 absolute inset-y-0 items-center pr-4"
[ngClass]="{'left-0': selectedIndicatorSide === 'left', 'right-0': selectedIndicatorSide === 'right', 'flex': selected === true}"
[hidden]="selected === false || !value"
<span
[class]="getIndicatorClasses()"
[hidden]="selected === false || !value || !indicator"
>
<!-- Heroicon name: solid/check -->
<svg
class="h-5 w-5"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 20 20"
fill="currentColor"
aria-hidden="true"
>
<!-- Heroicon name: solid/check -->
<svg
class="h-5 w-5"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 20 20"
fill="currentColor"
aria-hidden="true"
>
<path
fill-rule="evenodd"
d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z"
clip-rule="evenodd"
/>
</svg>
</span>
</div>
<path
fill-rule="evenodd"
d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z"
clip-rule="evenodd"
/>
</svg>
</span>
112 changes: 106 additions & 6 deletions projects/ng-tw/src/modules/option/option.component.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
import { ENTER, hasModifierKey, SPACE } from '@angular/cdk/keycodes';
import { Component, ElementRef, EventEmitter, Input, OnInit, Output, ViewChild } from '@angular/core';
import * as _ from 'lodash';
import { difference } from 'lodash';
import { TwSelectConfig } from '../select/select-config.interface';
import { TwSelectConfigService } from '../select/select-config.service';

/**
* Option IDs need to be unique across components, so this counter exists outside of
Expand All @@ -25,26 +28,35 @@ export class OptionSelectionChange<T = any> {
templateUrl: './option.component.html',
styleUrls: ['./option.component.css'],
host: {
'(click)': 'select(true)',
'[attr.id]': 'id',
'[attr.role]': 'option',
'[class]': 'getClasses()',
'(click)': 'select(true)',
},
})
export class OptionComponent<T = any> implements OnInit {
public selected: boolean = false;
public active: boolean = false;

private _config: TwSelectConfig['option'] = this.selectConfig.config.option;

@Input() public class: string = '';
@Input() public ignore: string = '';
@Input() public activeClass: string = this._config.activeClass;
@Input() public selectedClass: string = this._config.selectedClass;
@Input() public indicatorClass: string = this._config.indicatorClass;
@Input() public value: any;
@Input() public disabled: boolean = false;
@Input() public id: string = `tw-option-${_uniqueIdCounter++}`;

@Input() public useSelectedIndicator: boolean = true;
@Input() public selectedIndicatorSide: 'left' | 'right' = 'right';
@Input() public indicator: 'left' | 'right' | null = 'right';

@Output() readonly onSelectionChange = new EventEmitter<OptionSelectionChange<T>>();

@ViewChild('content') public contentElement!: ElementRef;

public selected: boolean = false;
public active: boolean = false;

constructor(private readonly element: ElementRef<HTMLElement>) {}
constructor(private readonly element: ElementRef<HTMLElement>, private readonly selectConfig: TwSelectConfigService) {}

ngOnInit(): void {}

Expand Down Expand Up @@ -124,4 +136,92 @@ export class OptionComponent<T = any> implements OnInit {
this.scrollIntoView();
});
}

getClasses() {
//
// Hold classes
let classes: string[] = [];

//
// Set global config and classes
const config: any = this._config;
const globalClasses: string[] = config.class ? config.class.split(' ').filter((item: string) => item) : [];

//
// Get @input classes if available
const inputClasses: string[] = this.class?.split(' ').filter((item: string) => item) || [];
const inputIgnoreClasses: string[] = this?.ignore ? this.ignore.split(' ').filter((item: string) => item) : [];

//
// Add global classes
classes = [...globalClasses];

//
// Filter global classes using global and @input ignore
classes = difference(classes, inputClasses, inputIgnoreClasses);

//
// Get active and selected classes
const activeClasses: string[] = this.activeClass ? this.activeClass.split(' ').filter((item: string) => item) : [];
const selectedClasses: string[] = this.selectedClass ? this.selectedClass.split(' ').filter((item: string) => item) : [];

//
// Apply selected/active
if (this.active === true) {
classes = [...classes, ...activeClasses];
}

if (this.selected === true) {
classes = [...classes, ...selectedClasses];
}

//
// Indicator classes
if (this.indicator === 'left') {
classes = [...classes, ...['pl-9', 'pr-3']];
} else if (this.indicator === 'right') {
classes = [...classes, ...['pl-3', 'pr-9']];
}

return classes?.length ? classes.join(' ') : '';
}

getIndicatorClasses() {
//
// Validate indicator
if (!this.indicator) return '';

//
// Hold classes
let classes: string[] = [];

//
// Set global config and classes
const config: any = this._config;
const globalClasses: string[] = config.indicatorClass ? config.indicatorClass.split(' ').filter((item: string) => item) : [];
const globalMandatoryClasses: string[] = config.indicatorMandatoryClass
? config.indicatorMandatoryClass.split(' ').filter((item: string) => item)
: [];
const inputClasses: string[] = this.indicatorClass ? this.indicatorClass.split(' ').filter((item: string) => item) : [];

//
// Add global classes
classes = [...globalClasses, ...globalMandatoryClasses, ...inputClasses];

//
// Left/right
if (this.indicator === 'left') {
classes = [...classes, ...['left-0']];
} else if (this.indicator === 'right') {
classes = [...classes, ...['right-0']];
}

//
// Selected
if (this.selected === true) {
classes = [...classes, ...['flex']];
}

return classes?.length ? classes.join(' ') : '';
}
}
21 changes: 21 additions & 0 deletions projects/ng-tw/src/modules/select/select-config.interface.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
export interface TwSelectConfig {
select: {
host: {
class: string;
ignore: string;
};
panel: {
class: string;
mandatoryClass: string;
ignore: string;
};
};
option: {
class: string;
ignore: string;
indicatorClass: string;
indicatorMandatoryClass: string;
activeClass: string;
selectedClass: string;
};
}
54 changes: 54 additions & 0 deletions projects/ng-tw/src/modules/select/select-config.service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import { Inject, Injectable, InjectionToken, Optional } from '@angular/core';
import { isEmpty, merge } from 'lodash';
import { TwSelectConfig } from './select-config.interface';

/**
* This is not a real service, but it looks like it from the outside.
* It's just an InjectionTToken used to import the config object, provided from the outside
*/
export const TwSelectSetup = new InjectionToken<TwSelectConfig>('TwSelectConfig');

@Injectable({
providedIn: 'root',
})
export class TwSelectConfigService {
public config: TwSelectConfig = {
select: {
host: {
class: 'bg-white relative border border-gray-300 rounded-md shadow-sm pl-3 pr-10 py-2 text-left cursor-default focus:outline-none focus:ring-1 focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm',
ignore: '',
},
panel: {
class: 'bg-white shadow-lg max-h-60 rounded-md py-1 text-base ring-1 ring-black ring-opacity-5',
mandatoryClass: 'absolute z-10 mt-1 w-full overflow-auto focus:outline-none sm:text-sm tw-option-panel-scroll',
ignore: '',
},
},
option: {
class: 'block py-2 hover:bg-gray-50 cursor-default select-none relative',
ignore: '',
indicatorClass: 'text-primary-600 pr-4',
indicatorMandatoryClass: 'absolute inset-y-0 items-center',
activeClass: 'font-bold',
selectedClass: 'bg-gray-100',
},
};

constructor(@Optional() @Inject(TwSelectSetup) public options: TwSelectConfig) {
//
// Validate
if (isEmpty(options)) return;

//
// Merge panel config
if (options.select) {
this.config.select = merge(this.config.select, options.select);
}

//
// Merge items config
if (options.option) {
this.config.option = merge(this.config.option, options.option);
}
}
}
73 changes: 31 additions & 42 deletions projects/ng-tw/src/modules/select/select.component.html
Original file line number Diff line number Diff line change
@@ -1,61 +1,50 @@
<button
type="button"
class="bg-white relative w-full border border-gray-300 rounded-md shadow-sm pl-3 pr-10 py-2 text-left cursor-default focus:outline-none focus:ring-1 focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm"
aria-haspopup="listbox"
aria-expanded="true"
aria-labelledby="listbox-label"
cdkOverlayOrigin
#trigger
(click)="openPanel()"
<span
class="block truncate"
[ngClass]="{'block': !innerValue || !htmlValue}"
[hidden]="innerValue && htmlValue"
>
<span
class="block truncate"
[ngClass]="{'block': !innerValue || !htmlValue}"
[hidden]="innerValue && htmlValue"
>
{{ placeholder}}
</span>
{{ placeholder}}
</span>

<span
class="block truncate"
[innerHtml]="htmlValue"
[ngClass]="{'block': innerValue && htmlValue}"
[hidden]="!innerValue || !htmlValue"
></span>
<span
class="block truncate"
[innerHtml]="htmlValue"
[ngClass]="{'block': innerValue && htmlValue}"
[hidden]="!innerValue || !htmlValue"
></span>

<span class="absolute inset-y-0 right-0 flex items-center pr-2 pointer-events-none">
<!-- Heroicon name: solid/selector -->
<svg
class="h-5 w-5 text-gray-400"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 20 20"
fill="currentColor"
aria-hidden="true"
>
<path
fill-rule="evenodd"
d="M10 3a1 1 0 01.707.293l3 3a1 1 0 01-1.414 1.414L10 5.414 7.707 7.707a1 1 0 01-1.414-1.414l3-3A1 1 0 0110 3zm-3.707 9.293a1 1 0 011.414 0L10 14.586l2.293-2.293a1 1 0 011.414 1.414l-3 3a1 1 0 01-1.414 0l-3-3a1 1 0 010-1.414z"
clip-rule="evenodd"
/>
</svg>
</span>
</button>
<span class="absolute inset-y-0 right-0 flex items-center pr-2 pointer-events-none">
<!-- Heroicon name: solid/selector -->
<svg
class="h-5 w-5 text-gray-400"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 20 20"
fill="currentColor"
aria-hidden="true"
>
<path
fill-rule="evenodd"
d="M10 3a1 1 0 01.707.293l3 3a1 1 0 01-1.414 1.414L10 5.414 7.707 7.707a1 1 0 01-1.414-1.414l3-3A1 1 0 0110 3zm-3.707 9.293a1 1 0 011.414 0L10 14.586l2.293-2.293a1 1 0 011.414 1.414l-3 3a1 1 0 01-1.414 0l-3-3a1 1 0 010-1.414z"
clip-rule="evenodd"
/>
</svg>
</span>

<ng-template
cdkConnectedOverlay
cdkConnectedOverlayHasBackdrop
cdkConnectedOverlayLockPosition
[cdkConnectedOverlayBackdropClass]="'cdk-overlay-transparent-backdrop'"
[cdkConnectedOverlayOrigin]="trigger"
[cdkConnectedOverlayOrigin]="elementRef.nativeElement"
[cdkConnectedOverlayOpen]="isOpen"
[cdkConnectedOverlayWidth]="overlayWidth || trigger.offsetWidth"
[cdkConnectedOverlayWidth]="overlayWidth || elementRef.nativeElement.offsetWidth"
(backdropClick)="backdropClick()"
(keydown)="handleKeydown($event)"
(detach)="closePanel()"
>
<div
[attr.id]="id + '-panel'"
[class]="'absolute z-10 mt-1 w-full bg-white shadow-lg max-h-60 rounded-md py-1 text-base ring-1 ring-black ring-opacity-5 overflow-auto focus:outline-none sm:text-sm tw-option-panel-scroll'"
[class]="getPanelClass()"
tabindex="-1"
role="listbox"
>
Expand Down
Loading

0 comments on commit 1037fd3

Please sign in to comment.