UI Input,select,form-control Libs

This commit is contained in:
Lorenz Hilpert
2020-12-15 16:48:12 +01:00
parent 30cb62cd06
commit 5026db9aaf
30 changed files with 735 additions and 169 deletions

View File

@@ -0,0 +1,86 @@
<form *ngIf="control" [formGroup]="control">
<ng-container formGroupName="organisation">
<ui-form-control label="Firmenname" requiredMark="*">
<input uiInput type="text" formControlName="name" tabindex="1" />
</ui-form-control>
<div class="control-row">
<ui-form-control label="Abteilung" [clearable]="false">
<input uiInput type="text" formControlName="department" tabindex="2" />
</ui-form-control>
<ui-form-control label="USt-ID" [clearable]="false">
<input uiInput type="text" formControlName="vatId" tabindex="3" />
</ui-form-control>
</div>
</ng-container>
<div class="control-row">
<ui-form-control label="Anrede" [clearable]="false">
<ui-select formControlName="gender" tabindex="4">
<ui-select-option [value]="2" label="Herr"></ui-select-option>
<ui-select-option [value]="4" label="Frau"></ui-select-option>
</ui-select>
</ui-form-control>
<ui-form-control label="Titel">
<ui-select formControlName="title" tabindex="5">
<ui-select-option value="Dr." label="Dr."></ui-select-option>
<ui-select-option value="Prof." label="Prof."></ui-select-option>
<ui-select-option value="Prof. Dr." label="Prof. Dr."></ui-select-option>
</ui-select>
</ui-form-control>
</div>
<div class="control-row">
<ui-form-control label="Nachname" [clearable]="false">
<input uiInput type="text" formControlName="lastName" tabindex="6" />
</ui-form-control>
<ui-form-control label="Vorname" [clearable]="false">
<input uiInput type="text" formControlName="firstName" tabindex="7" />
</ui-form-control>
</div>
<ng-container formGroupName="address">
<div class="control-row">
<ui-form-control label="Straße" requiredMark="*">
<input uiInput type="text" formControlName="street" tabindex="8" />
</ui-form-control>
<ui-form-control label="Hausnummer" requiredMark="*">
<input uiInput type="text" formControlName="streetNumber" tabindex="9" />
</ui-form-control>
</div>
<div class="control-row">
<ui-form-control label="PLZ" requiredMark="*">
<input uiInput type="text" formControlName="zipCode" tabindex="10" />
</ui-form-control>
<ui-form-control label="Ort" requiredMark="*">
<input uiInput type="text" formControlName="city" tabindex="11" />
</ui-form-control>
</div>
<ui-form-control label="Adresszusatz" [clearable]="false">
<input uiInput type="text" formControlName="info" tabindex="12" />
</ui-form-control>
<ui-form-control label="Land" [clearable]="false">
<ui-select formControlName="country" tabindex="13">
<ui-select-option value="DEU" label="Deutschland"></ui-select-option>
</ui-select>
</ui-form-control>
</ng-container>
<ng-container formGroupName="communicationDetails">
<ui-form-control label="E-Mail" [clearable]="false">
<input uiInput type="mail" formControlName="email" tabindex="14" />
</ui-form-control>
<div class="control-row">
<ui-form-control label="Festnetznummer">
<input uiInput type="tel" formControlName="phone" tabindex="15" />
</ui-form-control>
<ui-form-control label="Mobilnummer" [clearable]="false">
<input uiInput type="tel" formControlName="mobile" tabindex="16" />
</ui-form-control>
</div>
</ng-container>
</form>

View File

@@ -0,0 +1,7 @@
.control-row {
@apply flex flex-row gap-8;
ui-form-control {
width: 50%;
}
}

View File

@@ -0,0 +1,49 @@
import { Component, ChangeDetectionStrategy, Input } from '@angular/core';
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
import { CustomerDTO } from '@swagger/crm';
@Component({
selector: 'page-create-b2b',
templateUrl: 'create-b2b-form.component.html',
styleUrls: ['create-b2b-form.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class CreateB2BComponent {
@Input()
customer: CustomerDTO;
control: FormGroup;
constructor(private fb: FormBuilder) {
this.initForm();
}
initForm() {
const { fb } = this;
this.control = fb.group({
organisation: fb.group({
name: fb.control('', [Validators.required]),
department: fb.control(''),
vatId: fb.control(''),
}),
gender: fb.control(0),
title: fb.control(''),
firstName: fb.control(''),
lastName: fb.control(''),
address: fb.group({
street: fb.control('', [Validators.required]),
streetNumber: fb.control('', [Validators.required]),
zipCode: fb.control('', [Validators.required]),
city: fb.control('', [Validators.required]),
info: fb.control(''),
country: fb.control('DEU', [Validators.required]),
}),
communicationDetails: fb.group({
email: fb.control(''),
phone: fb.control(''),
mobile: fb.control(''),
}),
});
}
}

View File

@@ -0,0 +1,14 @@
<div class="card">
<h1>Kundendaten erfassen</h1>
<p>
Für eine B2B Bestellung benötigen Sie <br />
einen Firmenaccount. Wir legen diesen <br />
gerne direkt für Sie an.
</p>
<page-customer-type-selector [(ngModel)]="type" (ngModelChange)="setType($event)"></page-customer-type-selector>
<div class="router-outlet-wrapper">
<router-outlet></router-outlet>
</div>
</div>

View File

@@ -0,0 +1,33 @@
:host {
@apply flex flex-col box-border;
}
.card {
@apply bg-white rounded-card p-card;
}
h1 {
@apply m-0;
font-size: 26px;
margin-top: 27px;
}
p {
@apply m-0;
font-size: 22px;
margin-top: 10px;
}
.card > h1,
.card > p {
@apply text-center;
}
page-customer-type-selector {
margin-top: 45px;
}
.router-outlet-wrapper {
max-width: 650px;
@apply mx-auto;
}

View File

@@ -0,0 +1,26 @@
import { Component, ChangeDetectionStrategy, OnInit } from '@angular/core';
import { ActivatedRoute, Router } from '@angular/router';
import { Observable } from 'rxjs';
import { map } from 'rxjs/operators';
@Component({
selector: 'page-customer-create',
templateUrl: 'customer-create.component.html',
styleUrls: ['customer-create.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class CustomerCreateComponent implements OnInit {
type: string;
constructor(private activatedRoute: ActivatedRoute, private router: Router) {}
ngOnInit() {
this.type = this.activatedRoute.snapshot.queryParams.type || 'guest';
}
setType(type: string) {
this.router.navigate(['./', type], {
relativeTo: this.activatedRoute,
});
}
}

View File

@@ -0,0 +1,18 @@
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { CustomerCreateComponent } from './customer-create.component';
import { CustomerTypeSelectorComponent } from './customer-type-selector/customer-type-selector.component';
import { UiFormControlModule } from '@ui/form-control';
import { UiInputModule } from '@ui/input';
import { CreateB2BComponent } from './create-b2b-form/create-b2b-form.component';
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
import { RouterModule } from '@angular/router';
import { UiSelectModule } from '@ui/select';
@NgModule({
imports: [CommonModule, UiFormControlModule, UiInputModule, FormsModule, ReactiveFormsModule, RouterModule, UiSelectModule],
exports: [CustomerCreateComponent, CreateB2BComponent],
declarations: [CustomerCreateComponent, CustomerTypeSelectorComponent, CreateB2BComponent],
})
export class CustomerCreateModule {}

View File

@@ -0,0 +1,9 @@
<ui-form-control label="Gastkunde">
<input type="radio" name="customerType" value="guest" uiInput [ngModel]="value" (ngModelChange)="setValue($event)" />
</ui-form-control>
<ui-form-control label="Onlinekonto">
<input type="radio" name="customerType" value="online" uiInput [ngModel]="value" (ngModelChange)="setValue($event)" />
</ui-form-control>
<ui-form-control label="B2B Kunde">
<input type="radio" name="customerType" value="b2b" uiInput [ngModel]="value" (ngModelChange)="setValue($event)" />
</ui-form-control>

View File

@@ -0,0 +1,11 @@
:host {
@apply flex flex-row justify-center gap-6;
}
ui-form-control {
@apply text-card-sub;
}
input {
@apply text-card-sub;
}

View File

@@ -0,0 +1,52 @@
import { Component, ChangeDetectionStrategy, Input, Output, EventEmitter, ChangeDetectorRef, forwardRef } from '@angular/core';
import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms';
@Component({
selector: 'page-customer-type-selector',
templateUrl: 'customer-type-selector.component.html',
styleUrls: ['customer-type-selector.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
providers: [
{
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => CustomerTypeSelectorComponent),
multi: true,
},
],
})
export class CustomerTypeSelectorComponent implements ControlValueAccessor {
@Input()
value: 'guest' | 'online' | 'b2b' = 'guest';
@Output()
valueChange = new EventEmitter<'guest' | 'online' | 'b2b'>();
@Input()
disabled: boolean;
private onChange = (value: 'guest' | 'online' | 'b2b') => {};
private onTouched = () => {};
constructor(private cdr: ChangeDetectorRef) {}
writeValue(obj: any): void {
this.value = obj;
}
registerOnChange(fn: any): void {
this.onChange = fn;
}
registerOnTouched(fn: any): void {
this.onTouched = fn;
}
setDisabledState?(isDisabled: boolean): void {
this.disabled = isDisabled;
}
setValue(value: 'guest' | 'online' | 'b2b') {
this.onChange(value);
this.onTouched();
}
}

View File

@@ -1,6 +1,6 @@
<div class="control-field">
<ui-form-control label="Anrede" variant="inline">
<ui-select #genderControl [(value)]="customer.gender" [disabled]="!genderToggle.toggled">
<ui-select #genderControl [(ngModel)]="customer.gender" [disabled]="!genderToggle.toggled">
<ui-select-option [value]="2" label="Herr"></ui-select-option>
<ui-select-option [value]="4" label="Frau"></ui-select-option>
</ui-select>

View File

@@ -1,10 +1,7 @@
<a class="card-create-customer">
<a class="card-create-customer" [routerLink]="['/customer', 'create']">
<span class="title"> Kundendaten erfassen </span>
</a>
<div
class="card-search-customer"
*ngIf="search.searchState$ | async as searchState"
>
<div class="card-search-customer" *ngIf="search.searchState$ | async as searchState">
<h1 class="title">Kundensuche</h1>
<p class="info">
Wie lautet Ihr Name oder <br />

View File

@@ -24,3 +24,7 @@
.card-search-customer {
height: calc(100vh - 380px);
}
a {
@apply no-underline;
}

View File

@@ -1,5 +1,7 @@
import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import { CreateB2BComponent } from './customer-create/create-b2b-form/create-b2b-form.component';
import { CustomerCreateComponent } from './customer-create/customer-create.component';
import { CustomerDetailsComponent } from './customer-details/customer-details.component';
import { CustomerSearchComponent } from './customer-search/customer-search.component';
import { CustomerSearchMainComponent } from './customer-search/search-main/search-main.component';
@@ -20,6 +22,15 @@ const routes: Routes = [
{ path: ':customerId/details', component: null },
],
},
{
path: 'create',
component: CustomerCreateComponent,
children: [
{ path: 'guest', component: CreateB2BComponent },
{ path: 'online', component: CreateB2BComponent },
{ path: 'b2b', component: CreateB2BComponent },
],
},
{
path: ':customerId',
component: CustomerDetailsComponent,

View File

@@ -6,9 +6,19 @@ import { PageCustomerRoutingModule } from './page-customer-routing.module';
import { ShellBreadcrumbModule } from '@shell/breadcrumb';
import { CustomerSearchModule } from './customer-search/customer-search.module';
import { CustomerDetailsModule } from './customer-details/customer-details.module';
import { CustomerCreateModule } from './customer-create/customer-create.module';
import { UiInputModule } from '@ui/input';
@NgModule({
imports: [CommonModule, PageCustomerRoutingModule, ShellBreadcrumbModule, CustomerSearchModule, CustomerDetailsModule],
imports: [
CommonModule,
PageCustomerRoutingModule,
ShellBreadcrumbModule,
CustomerSearchModule,
CustomerDetailsModule,
CustomerCreateModule,
UiInputModule,
],
exports: [PageCustomerComponent],
declarations: [PageCustomerComponent],
})

View File

@@ -1,5 +1,14 @@
<label *ngIf="label">{{ label }}</label>
<ng-content select="input, ui-select"></ng-content>
<button *ngIf="clearable && control.value && !control.disabled" class="ui-form-control-clear clear" (click)="control.clear()">
<div class="input-wrapper" [class.empty]="!ngControl.value" [class.focused]="uiControl.focused | async">
<ng-content select="input[uiInput], ui-select"></ng-content>
<label *ngIf="label" (click)="uiControl.focus()">{{ label }}{{ requiredMark }}</label>
</div>
<span class="hint" *ngIf="ngControl.touched && ngControl.errors">
{{ ngControl.errors | uiFormControlFirstError: label }}
</span>
<button
*ngIf="clearable && ngControl?.value && !ngControl?.disabled && !(uiControl.type === 'radio' || uiControl.type === 'checkbox')"
class="ui-form-control-clear clear"
(click)="uiControl.clear()"
>
<ui-icon icon="close"></ui-icon>
</button>

View File

@@ -1,20 +1,116 @@
:host {
@apply flex flex-row gap-4 bg-white items-center box-border;
}
:host ::ng-deep ui-select,
:host ::ng-deep input {
@apply font-bold flex-grow;
}
:host ::ng-deep input {
@apply outline-none border-none font-bold text-regular;
&:disabled {
@apply bg-white text-black;
}
@apply flex flex-row gap-4 bg-transparent items-center box-border;
}
button.clear {
@apply bg-transparent text-ucla-blue border-none outline-none font-bold text-regular;
}
.hint {
@apply text-brand text-x-small font-bold;
}
:host ::ng-deep {
ui-select,
input {
@apply font-bold flex-grow;
}
input[type='text'],
input[type='password'],
input[type='tel'],
input[type='mail'] {
@apply outline-none border-none font-bold text-regular;
&:disabled {
@apply bg-white text-black;
}
}
input[type='radio'],
input[type='checkbox'] {
@apply appearance-none bg-current;
height: 1em;
width: 1em;
mask: url('/assets/checkbox.svg') no-repeat;
&:checked {
mask: url('/assets/checkbox_checked.svg') no-repeat;
}
}
}
:host[type='radio'] ::ng-deep,
:host[type='checkbox'] ::ng-deep {
input {
@apply flex-grow-0 m-0;
}
input:checked ~ label {
@apply font-bold;
}
label {
@apply flex-grow -ml-2;
}
}
.input-wrapper {
@apply flex flex-row-reverse gap-4 flex-grow items-center;
}
:host[type='radio'],
:host[type='checkbox'] {
.input-wrapper {
@apply flex-row;
}
}
:host[variant='default'] {
::ng-deep {
input[type='text'],
input[type='password'],
input[type='tel'],
input[type='mail'],
ui-select {
@apply border-t-0 border-l-0 border-r-0 border-solid pt-px-20 pb-px-10;
border-bottom-width: 2px;
border-color: #e1ebf5;
}
input.ng-touched.ng-invalid,
ui-select.ng-touched.ng-invalid {
@apply border-brand;
}
}
}
:host[variant='default']:not([type='radio']):not([type='checkbox']) {
@apply relative mt-px-20;
label {
@apply absolute left-0 font-bold text-cta-l pointer-events-none text-ucla-blue;
top: 20px;
transition: 250ms all ease-in-out;
}
.hint {
@apply absolute right-0;
bottom: -21px;
}
// ::ng-deep input:focus ~ label,
// ::ng-deep input:not(.empty) ~ label,
// ::ng-deep ui-select.toggled ~ label,
// ::ng-deep ui-select:not(.empty) ~ label {
// @apply top-0 text-small font-semibold;
// // font-size: 10px;
// }
.input-wrapper.focused,
.input-wrapper:not(.empty) {
label {
@apply top-0 text-small font-semibold;
}
}
}

View File

@@ -8,6 +8,7 @@ import {
OnDestroy,
HostBinding,
} from '@angular/core';
import { NgControl } from '@angular/forms';
import { combineLatest, Subscription } from 'rxjs';
import { UiFormControlDirective } from './ui-form-control.directive';
@@ -21,24 +22,35 @@ export class UiFormControlComponent implements AfterContentInit, OnDestroy {
private subscriptions = new Subscription();
@Input()
@HostBinding('class')
@HostBinding('attr.variant')
variant: 'inline' | 'default' = 'default';
@HostBinding('attr.type')
get controlType() {
return this.uiControl?.type;
}
@Input()
label: string;
@Input()
clearable = true;
requiredMark: string;
@Input()
clearable = false;
@ContentChild(NgControl, { read: NgControl })
ngControl: NgControl;
@ContentChild(UiFormControlDirective, { read: UiFormControlDirective })
control: UiFormControlDirective<any>;
uiControl: UiFormControlDirective<any>;
constructor(private cdr: ChangeDetectorRef) {}
ngAfterContentInit() {
if (this.control) {
if (this.ngControl) {
this.subscriptions.add(
this.control.changes.subscribe(() => {
combineLatest([this.ngControl.control.statusChanges, this.ngControl.control.valueChanges]).subscribe((value) => {
this.cdr.markForCheck();
})
);

View File

@@ -1,17 +1,12 @@
import { Directive, EventEmitter } from '@angular/core';
import { NgControl } from '@angular/forms';
@Directive()
export abstract class UiFormControlDirective<T> {
changes = new EventEmitter<string>();
focused = new EventEmitter<boolean>();
value: T | null;
abstract type: string;
valueChange = new EventEmitter<T>();
disabled?: boolean;
disabledChange = new EventEmitter<T>();
abstract get valueEmpty(): boolean;
abstract clear(): void;

View File

@@ -3,10 +3,11 @@ import { CommonModule } from '@angular/common';
import { UiFormControlComponent } from './ui-form-control.component';
import { UiIconModule } from '@ui/icon';
import { UiFormControlFirstErrorPipe } from './ui-form-first-error.pipe';
@NgModule({
imports: [CommonModule, UiIconModule],
exports: [UiFormControlComponent],
declarations: [UiFormControlComponent],
declarations: [UiFormControlComponent, UiFormControlFirstErrorPipe],
})
export class UiFormControlModule {}

View File

@@ -0,0 +1,19 @@
import { Pipe, PipeTransform } from '@angular/core';
import { ValidationErrors } from '@angular/forms';
@Pipe({
name: 'uiFormControlFirstError',
})
export class UiFormControlFirstErrorPipe implements PipeTransform {
transform(errors: ValidationErrors, label: string): string {
console.log(errors);
if (errors) {
const error = Object.keys(errors)[0];
switch (error) {
case 'required':
return `${label} wird benötigt`;
}
}
return undefined;
}
}

View File

@@ -1,20 +1,9 @@
import {
Directive,
ElementRef,
EventEmitter,
forwardRef,
HostBinding,
HostListener,
Input,
OnChanges,
Output,
SimpleChanges,
} from '@angular/core';
import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms';
import { Directive, ElementRef, forwardRef, HostBinding, HostListener, Input, Renderer2, Self } from '@angular/core';
import { ControlValueAccessor, NgControl, NG_VALUE_ACCESSOR } from '@angular/forms';
import { UiFormControlDirective } from '@ui/form-control';
@Directive({
selector: 'input[uiInput]',
selector: 'input[uiInput]:not([type=radio]):not([type=checkbox])',
providers: [
{
provide: NG_VALUE_ACCESSOR,
@@ -28,71 +17,80 @@ import { UiFormControlDirective } from '@ui/form-control';
],
exportAs: 'uiInput',
})
export class UiInputDirective implements UiFormControlDirective<any>, OnChanges, ControlValueAccessor {
@Output()
changes = new EventEmitter<string>();
export class UiInputDirective extends UiFormControlDirective<any> implements ControlValueAccessor {
@Input()
@HostBinding('value')
value: any;
@HostBinding('attr.type')
type: string;
@Output()
valueChange = new EventEmitter<any>();
private currentValue: any;
@Input()
@HostBinding('disabled')
disabled?: boolean;
get value() {
return this.currentValue;
}
@Output()
disabledChange = new EventEmitter<any>();
get valueEmpty(): boolean {
return !!this.value;
}
private onChange = (value: any) => {};
private onTouched = () => {};
constructor(private elementRef: ElementRef) {}
ngOnChanges({ disabled }: SimpleChanges): void {
if (disabled) {
this.changes.emit('disabled');
}
constructor(private elementRef: ElementRef, private renderer: Renderer2) {
super();
}
writeValue(obj: any): void {
console.log(obj);
this.value = obj || '';
this.changes.emit('value');
this.setValue(obj, false);
}
registerOnChange(fn: any): void {
this.onChange = fn;
}
registerOnTouched(fn: any): void {
this.onTouched = fn;
}
setDisabledState?(isDisabled: boolean): void {
this.disabled = isDisabled;
this.changes.emit('disabled');
if (isDisabled) {
this.renderer.setAttribute(this.elementRef.nativeElement, 'disabled', '');
} else {
this.renderer.removeAttribute(this.elementRef.nativeElement, 'disabled');
}
}
clear(): void {
this.setValue('');
this.setValue(undefined);
}
focus(): void {
setTimeout(() => {
this.elementRef?.nativeElement?.focus();
this.elementRef?.nativeElement?.click().focus();
}, 0);
}
@HostListener('keyup', ['$event.target.value'])
setValue(value: any): void {
console.log(value);
if (value !== this.value) {
this.value = value || '';
this.onChange(this.value);
this.valueChange.next(this.value);
this.changes.emit('value');
setValue(value: any, emitEvent = true) {
if (this.value !== value) {
this.currentValue = value;
this.renderer.setAttribute(this.elementRef.nativeElement, 'value', this.value);
if (emitEvent) {
this.onChange(value);
this.onTouched();
}
}
}
@HostListener('focus')
onFocus() {
this.focused.emit(true);
}
@HostListener('blur')
onBlur() {
this.onTouched();
this.focused.emit(false);
}
}

View File

@@ -1,11 +1,13 @@
import { CommonModule } from '@angular/common';
import { NgModule } from '@angular/core';
import { UiInputDirective } from './ui-input.directive';
import { UiRadioInputDirective } from './ui-radio-input.directive';
@NgModule({
imports: [],
exports: [UiInputDirective],
declarations: [UiInputDirective],
imports: [CommonModule],
exports: [UiInputDirective, UiRadioInputDirective],
declarations: [UiInputDirective, UiRadioInputDirective],
providers: [],
})
export class UiInputModule {}

View File

@@ -0,0 +1,82 @@
import { Directive, ElementRef, EventEmitter, forwardRef, HostBinding, HostListener, Input, Output, Renderer2 } from '@angular/core';
import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms';
import { UiFormControlDirective } from '@ui/form-control';
@Directive({
selector: 'input[uiInput][type="radio"], input[uiInput][type="checkbox"]',
providers: [
{
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => UiRadioInputDirective),
multi: true,
},
{
provide: UiFormControlDirective,
useExisting: UiRadioInputDirective,
},
],
exportAs: 'uiInput',
})
export class UiRadioInputDirective extends UiFormControlDirective<any> implements ControlValueAccessor {
@Input()
value: string;
@Input()
@HostBinding('attr.type')
type: string;
private onChange = (value: any) => {};
private onTouched = () => {};
get valueEmpty(): boolean {
return false;
}
constructor(private elementRef: ElementRef, private renderer: Renderer2) {
super();
}
writeValue(obj: any): void {
this.check(this.value === obj, false);
}
registerOnChange(fn: any): void {
this.onChange = fn;
}
registerOnTouched(fn: any): void {
this.onTouched = fn;
}
setDisabledState?(isDisabled: boolean): void {
if (isDisabled) {
this.renderer.setAttribute(this.elementRef.nativeElement, 'disabled', '');
} else {
this.renderer.removeAttribute(this.elementRef.nativeElement, 'disabled');
}
}
clear(): void {
this.check(false);
}
focus(): void {}
@HostListener('click')
click(): void {
this.check(true);
}
check(value: boolean, emitEvent = true) {
if (value) {
this.renderer.setAttribute(this.elementRef.nativeElement, 'checked', '');
} else {
this.renderer.removeAttribute(this.elementRef.nativeElement, 'checked');
}
if (emitEvent) {
this.onChange(this.value);
}
this.onTouched();
}
}

View File

@@ -1 +1,3 @@
{{ label }}
<button #selectButton (click)="select()" tabIndex="-1">
{{ label }}
</button>

View File

@@ -1,4 +1,4 @@
import { Component, ChangeDetectionStrategy, Input, HostListener, EventEmitter, Output } from '@angular/core';
import { Component, ChangeDetectionStrategy, Input, HostListener, EventEmitter, Output, ElementRef, ViewChild } from '@angular/core';
@Component({
selector: 'ui-select-option',
@@ -15,16 +15,29 @@ export class UiSelectOptionComponent {
onSelect = (value: any) => {};
constructor() {}
@ViewChild('selectButton', { read: ElementRef })
selectButton: ElementRef;
get focused() {
const host: HTMLElement = this.elementRef.nativeElement;
return !!host.querySelector(':focus');
}
constructor(private elementRef: ElementRef) {}
registerOnSelect(fn: any) {
this.onSelect = fn;
}
@HostListener('click')
select() {
if (typeof this.onSelect === 'function') {
this.onSelect.call(null, this.value);
}
}
focus() {
setTimeout(() => {
this.selectButton.nativeElement.focus();
}, 1);
}
}

View File

@@ -1,5 +1,5 @@
<div class="ui-input-wrapper">
<div class="ui-select-value" (click)="toggle()">{{ label }}</div>
<div class="ui-select-value">{{ label }}</div>
<button class="ui-select-toggle" (click)="toggle()" [disabled]="disabled">
<ui-icon icon="arrow_head"></ui-icon>
</button>

View File

@@ -2,12 +2,18 @@
@apply flex flex-col box-border text-regular;
}
:host:focus {
@apply outline-none;
}
.ui-input-wrapper {
@apply flex flex-row bg-white px-px-10;
@apply flex flex-row bg-white;
}
.ui-select-value {
@apply flex-grow;
@apply flex-grow text-cta-l;
height: 21px;
line-height: 21px;
}
.ui-select-toggle {
@@ -31,6 +37,7 @@
transition: 250ms all ease-in-out;
@apply z-dropdown overflow-auto left-0 right-0;
max-height: 0rem;
top: 10px;
}
:host.toggled .ui-select-dropdown-wrapper .ui-select-options {
@@ -38,13 +45,14 @@
}
.ui-select-dropdown-wrapper .ui-select-options {
@apply flex flex-col absolute bg-white shadow-xl rounded-b-card;
@apply flex flex-col absolute bg-white shadow-card rounded-card;
}
:host ::ng-deep ui-select-option {
@apply py-px-10 px-px-10 cursor-pointer;
:host ::ng-deep ui-select-option button {
@apply py-px-10 px-px-10 cursor-pointer w-full text-left text-regular bg-white border-0 outline-none font-bold;
&:hover {
&:hover,
&:focus {
@apply bg-glitter;
}
}

View File

@@ -8,15 +8,9 @@ import {
AfterContentInit,
ChangeDetectorRef,
forwardRef,
EventEmitter,
Output,
Self,
Optional,
ElementRef,
OnChanges,
SimpleChanges,
HostListener,
} from '@angular/core';
import { ControlValueAccessor, NgControl, NG_VALUE_ACCESSOR } from '@angular/forms';
import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms';
import { UiFormControlDirective } from '@ui/form-control';
import { UiSelectOptionComponent } from './ui-select-option.component';
@@ -26,67 +20,71 @@ import { UiSelectOptionComponent } from './ui-select-option.component';
styleUrls: ['ui-select.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
providers: [
{
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => UiSelectComponent),
multi: true,
},
{ provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(() => UiSelectComponent), multi: true },
{
provide: UiFormControlDirective,
useExisting: UiSelectComponent,
},
],
})
export class UiSelectComponent implements UiFormControlDirective<any>, ControlValueAccessor, AfterContentInit, OnChanges {
@Output()
changes = new EventEmitter<string>();
export class UiSelectComponent extends UiFormControlDirective<any> implements ControlValueAccessor, AfterContentInit {
readonly type = 'ui-select';
@Input()
value: any;
@Output()
valueChange = new EventEmitter<any>();
get label() {
return this.options.find((o) => o.value === this.value)?.label;
}
@Input()
disabled: boolean;
@Output()
disabledChange = new EventEmitter<boolean>();
disabled = false;
@ContentChildren(UiSelectOptionComponent, { read: UiSelectOptionComponent })
options: QueryList<UiSelectOptionComponent>;
@Input()
get valueEmpty(): boolean {
return !this.value;
}
@HostBinding('class.toggled')
toggled: boolean;
toggled = false;
private onChange = (value: any) => {};
private onTouched = () => {};
onChange = (_: any) => {};
constructor(private cdr: ChangeDetectorRef) {}
onTouched = () => {};
ngOnChanges({ disabled }: SimpleChanges): void {
if (disabled && disabled.currentValue) {
this.close();
}
constructor(private cdr: ChangeDetectorRef) {
super();
}
clear(): void {
this.setValue(undefined);
}
focus() {
@HostListener('focus')
focus(): void {
this.open();
}
ngAfterContentInit() {
this.registerOptionsSelect();
this.options.changes.subscribe((_) => {
this.registerOptionsSelect();
});
}
private registerOptionsSelect() {
this.options.forEach((option) => {
option.registerOnSelect((value) => {
this.setValue(value);
this.close();
});
});
}
writeValue(obj: any): void {
this.value = obj;
this.cdr.markForCheck();
this.changes.emit('value');
this.setValue(obj, false);
}
registerOnChange(fn: any): void {
@@ -99,58 +97,60 @@ export class UiSelectComponent implements UiFormControlDirective<any>, ControlVa
setDisabledState?(isDisabled: boolean): void {
this.disabled = isDisabled;
if (isDisabled) {
this.close();
}
setValue(value: any, emitEvent: boolean = true) {
if (this.value !== value) {
this.value = value;
if (emitEvent) {
this.onChange(this.value);
}
}
this.cdr.markForCheck();
this.changes.emit('disabled');
this.onTouched();
}
toggle() {
if (!this.disabled) {
this.toggled = !this.toggled;
this.cdr.markForCheck();
this.changes.emit('toggled');
if (this.toggled) {
this.close();
} else {
this.open();
}
}
}
open() {
this.toggled = true;
this.options.first?.focus();
this.cdr.markForCheck();
this.changes.emit('toggled');
}
close() {
this.toggled = false;
this.cdr.markForCheck();
this.changes.emit('toggled');
}
ngAfterContentInit() {
this.registerOptionsSelect();
this.options.changes.subscribe((_) => {
this.registerOptionsSelect();
});
}
setValue(value: any) {
if (value !== this.value) {
this.value = value;
this.onChange(value);
this.valueChange.next(value);
this.changes.emit('value');
}
this.onTouched();
this.cdr.markForCheck();
}
private registerOptionsSelect() {
this.options.forEach((option) => {
option.registerOnSelect((value) => {
this.setValue(value);
this.close();
});
});
@HostListener('keyup', ['$event'])
keyup(event: KeyboardEvent) {
let optionIndex = this.options.toArray().findIndex((o) => o.focused);
if (event.key === 'ArrowUp') {
optionIndex--;
if (optionIndex < 0) {
return;
}
} else if (event.key === 'ArrowDown') {
optionIndex++;
if (this.options.length - 1 < optionIndex) {
return;
}
} else {
return;
}
this.options.toArray()[optionIndex]?.focus();
}
}

View File

@@ -21,6 +21,8 @@ module.exports = {
},
extend: {
fontSize: {
'x-small': '12px',
small: '14px',
regular: '16px',
'cta-l': '18px',
'card-heading': '22px',